From 4288b42c7ea739433e49584ddc3650bdfca67334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lena=20K=C3=A4ufel?= Date: Wed, 26 Mar 2025 17:14:11 +0100 Subject: [PATCH 01/41] thesis-pbt-effekt: add property-based testing support to stdlib testing module, along with examples --- examples/stdlib/test/list_examples_pbt.effekt | 166 ++++++ examples/stdlib/test/tree_examples_pbt.effekt | 107 ++++ libraries/common/test.effekt | 497 +++++++++++++++++- 3 files changed, 759 insertions(+), 11 deletions(-) create mode 100644 examples/stdlib/test/list_examples_pbt.effekt create mode 100644 examples/stdlib/test/tree_examples_pbt.effekt diff --git a/examples/stdlib/test/list_examples_pbt.effekt b/examples/stdlib/test/list_examples_pbt.effekt new file mode 100644 index 000000000..6af173551 --- /dev/null +++ b/examples/stdlib/test/list_examples_pbt.effekt @@ -0,0 +1,166 @@ +import test + +def main() = { + println(suite("PBT: List Functions and Other Simple Examples") { + + // example unit tests to show that mixed test suites work + test("1 + 1 == 2") { + assertEqual(1 + 1, 2) + } + + test("2 + 2 == 3") { + assertEqual(2 + 2, 3) + } + + // reverse[x] === [x] + with arbitraryInt; + forall[Int]("reverse-singleton", 100){ x => + assertEqual( + reverse([x]), + [x] + ) + } + + // reverse[x] === [x], with explicit passing of the generator + with def g = arbitraryInt; + forall[Int]("reverse-singleton, explicitely passing the generator", 100){ g } + { x => + assertEqual( + reverse([x]), + [x] + ) + } + + // intented mistake: reverse[x] === [6] + forall[Int]("reverse-singleton mistake", 100) + { x => + assertEqual( + reverse([x]), + [6] + ) + } + + // shows that exists prints tried test cases and found examples correctly + exists[Int]("Is the Integer 2 among the generated values?", 100) + { x => + assertEqual( + x, + 2 + ) + } + + // reverse(xs ++ ys) === reverse(ys) ++ reverse(xs) + with arbitraryList[Int](3) + forall[List[Int], List[Int]]("reverse: distributivity over append", 100) + { (xs, ys) => + assertEqual( + reverse(xs.append(ys)), + reverse(ys).append(reverse(xs)) + ) + } + + // intended mistake: reverse(xs ++ ys) === reverse(xs) ++ reverse(ys) + forall[List[Int], List[Int]]("reverse: distributivity mistake - swapped order", 20) + { (xs, ys) => + assertEqual( + reverse(xs.append(ys)), + reverse(xs).append(reverse(ys)) + ) + } + + with arbitraryChar; + with arbitraryList[Char](4); + forall[List[Char], List[Int]]("|zip(xs,ys)| === min(|xs|,|ys|)",10) + { (xs, ys) => + assertEqual( + zip(xs, ys).size, + min(xs.size, ys.size) + ) + } + + with arbitraryChar; + with arbitraryList[Char](6); + forall[List[Char], List[Int]]("intended mistake: |zip(xs,ys)| != max(|xs|,|ys|)",10) + { (xs, ys) => + assertEqual( + zip(xs, ys).size, + max(xs.size, ys.size) + ) + } + + // unzip(zip(xs,ys)) === (xs.take(m), ys.take(m)) where m = min(|xs|,|ys|) + with arbitraryChar; + with arbitraryString(4); + with arbitraryList[String](2); + with arbitraryInt; + with arbitraryList[Int](3) + forall[List[Int], List[String]]("unzip-zip-take relation", 10) + { (xs, ys) => + val m = min(xs.size, ys.size) + assertEqual( + unzip(zip(xs, ys)), + (xs.take(m), ys.take(m)) + ) + } + + // Dropping elements from the concatenation of two lists is equivalent to dropping elements from the first list, + // and then (if necessary) dropping the remaining count from the second list. + // (xs ++ ys).drop(n) === if n <= len(xs) then (xs.drop(n)) ++ ys else ys.drop(n - len(xs)) + forall[List[Int], List[Int], Int]("drop: concatenation ", 10) + { (xs, ys, n) => + val res = if(n <= xs.size) { + xs.drop(n).append(ys) + } + else { ys.drop(n - xs.size) } + assertEqual( + (xs.append(ys).drop(n)), + res + ) + } + + // xs.drop(n) === xs.slice(n, x.size) + forall[List[Int], Int]("drop-slice relation", 10) + { (xs, n) => + assertEqual( + xs.drop(n), + xs.slice(n, xs.size) + ) + } + + // reverseOnto(reverse(xs), ys) === append(xs, ys) + forall[List[Int], List[Int]]("reverseOnto-reverse-append relation ", 10) + { (xs, ys) => + assertEqual( + reverseOnto(reverse(xs), ys), + append(xs, ys) + ) + } + + // size(xs) === foldLeft(xs, 0) { (acc, _) => acc + 1 } + forall[List[Int]]("size-foldLeft relation", 20) + { xs => + assertEqual( + size(xs), + foldLeft(xs, 0){(acc, _) => acc + 1} + ) + } + + //xs.take(n) ++ xs.drop(n) === xs + forall[Int, List[Int]]("take-drop: inversion over concatenation", 20) + { (n, xs) => + assertEqual( + append(xs.take(n), xs.drop(n)), + xs + ) + } + + // example for a property-based test with multiple assertions + with evenNumbers; + forall[Int]("even numbers are even and smaller than 4", 20) + { n => + assertTrue(n.mod(2) == 0) + assertTrue(n <= 4) + } + }) +} + \ No newline at end of file diff --git a/examples/stdlib/test/tree_examples_pbt.effekt b/examples/stdlib/test/tree_examples_pbt.effekt new file mode 100644 index 000000000..4e1190165 --- /dev/null +++ b/examples/stdlib/test/tree_examples_pbt.effekt @@ -0,0 +1,107 @@ +import map +import stream + +import test + +// user-defined generator for arbitrary, unique Integers in ascending order +def uniqueInt{body: => Unit / Generator[Int]}: Unit = { + try body() with Generator[Int]{ + def generate() = resume{ + var next = randomInt(-100, 100) + do emit(next) + while(true){ + next = next + randomInt(1, 5) + do emit(next) + } + } + def shrink(v) = <> + } +} + +// precondition: provided handler for Generator[K] needs to produce unique keys in ascending order! +// otherwise the generated trees aren't valid binary search trees +def arbitraryBinTree[K, V] (numKeys: Int) { body: => Unit / Generator[internal::Tree[K, V]] }: Unit / {Generator[K], Generator[V]} = { + def buildTree[K, V](pairs: List[(K, V)]): internal::Tree[K,V] = { + if (pairs.isEmpty) { + internal::Tip() + } else { + val midIdx = pairs.size() / 2 + with on[OutOfBounds].panic; + val midEl = pairs.get(midIdx) + val leftTree = buildTree(pairs.take(midIdx)) + val rightTree = buildTree(pairs.drop(midIdx + 1)) + val size = 1 + internal::size(leftTree) + internal::size(rightTree) + + internal::Bin(size, midEl.first, midEl.second, leftTree, rightTree) + } + } + + try body() with Generator[internal::Tree[K, V]] { + def generate() = resume { + while(true) { + val l1 = collectList[K]{with boundary; with limit[K](numKeys); do generate[K]} + val l2 = collectList[V]{with boundary; with limit[V](numKeys); do generate[V]} + val sortedPairs = zip(l1,l2) + do emit(buildTree[K, V](sortedPairs)) + //TODO try to use zip again + } + } + def shrink(v) = <> + } +} + +def main()= { + with arbitraryChar; + with arbitraryString(5); + with uniqueInt; + with arbitraryBinTree[Int, String](8); + + println(suite("PBT: Tree Map Examples") { + + // get-put law: get(put(M, K, V), K) = V + forall[String, internal::Tree[Int, String], Int]("get-put law", 100) + { (v, t, k) => + val newT = internal::put(t, compareInt, k, v) + internal::get(newT, compareInt, k) match { + case Some(value) => assertEqual(value, v) + case None() => do assert(false, "Key not in the tree") + } + } + + // put-put law: get(put(put(M, K, V1), K, V2), K) = V2 + forall[String, internal::Tree[Int, String], Int]("put-put law", 100) + { (v1, t, k) => + val v2 = v1 ++ show(k) ++ v1 + val newT1 = internal::put(t, compareInt, k, v1) + val newT2 = internal::put(newT1, compareInt, k, v2) + internal::get(newT2, compareInt, k) match { + case Some(v) => assertEqual(v, v2) + case None() => do assert(false, "Key in the tree") + } + } + + // put-get law: put(M, K, get(M, K)) = M + forall[internal::Tree[Int, String]]("put-get law", 100) + { t => + // put-get law only make sense if K is present in the tree ~> get the keys of the tree + var keys = Nil() + t.internal::foreach[Int, String] { (k, _v) => keys = Cons(k, keys)} + + with on[OutOfBounds].panic + val k = keys.get(randomInt(0, keys.size())) // only check the property for key's present in the tree (= non trivial test case) + internal::get(t, compareInt, k) match { + case Some(v) => { + val newT = internal::put(t, compareInt, k, v) + assertEqual(t, newT)} + case None() => do assert(false, "Key not in the tree") + } + } + + // Law: `m.forget === m.map { (_k, _v) => () } + forall[internal::Tree[Int, String]]("forget-map relation", 100){t => + val tForget = internal::forget(t) + val tMap = internal::map(t){ (_k, _v) => ()} + assertEqual(tForget, tMap) + } + }) +} \ No newline at end of file diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index d5cafc894..b2174f074 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -1,6 +1,19 @@ import tty import process import bench +import stream + +// FFI for random Integer generation for property testing +extern js""" + function getRandomInt(min, max) { + const minCeiled = Math.ceil(min); + const maxFloored = Math.floor(max); + return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive + } +""" + +extern def randomInt(minInclusive: Int, maxExclusive: Int): Int = + js"getRandomInt(${minInclusive}, ${maxExclusive})" interface Assertion { def assert(condition: Bool, msg: String): Unit @@ -56,10 +69,240 @@ def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: // NOTE: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Here's an accidental capture! Can we prevent this somehow nicely? +/// Represents push stream based generators with manual shrinking +interface Generator[T] { + /// Generates a push stream of values of type `T` + def generate(): Unit/emit[T] + + /// Produces a puh stream of simplified (shrunk) versions of a given value of type `T` + def shrink(v: T): Unit/emit[T] +} + +def arbitraryInt { body: => Unit / Generator[Int] }: Unit = + try body() with Generator[Int] { + def generate() = resume { + //[0, -1, 1, int.minVal, int.maxVal, -2, 2].each // This can reduce the need for shrinking by emitting common edge cases first + while (true) { + do emit(randomInt(-50, 50)) + } + } + + def shrink(v: Int) = resume { + var shrunk: Int = v/2 + while (shrunk != 0 && shrunk != v) { + do emit(shrunk) + do emit(neg(shrunk)) + shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 ) + } + do emit(0) + } + } + +// same as other arbitraryInt but for quantifier version that explicitely takes the generator as input +def arbitraryInt { body: {Generator[Int]} => Unit }: Unit = { + try body{g} with g: Generator[Int] { + def generate() = resume { + while (true) { + do emit(randomInt(-50, 50)) + } + } + + def shrink(v: Int) = resume { + var shrunk: Int = v/2 + while (shrunk != 0 && shrunk != v) { + do emit(shrunk) + do emit(neg(shrunk)) + shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 ) + } + do emit(0) + } + } +} + +def chooseInt(minInclusive: Int, maxExclusive: Int) { body: => Unit / Generator[Int] }: Unit = + try body() with Generator[Int] { + def generate() = resume { + while (true) { + do emit(randomInt(minInclusive, maxExclusive)) + } + } + + def shrink(v: Int) = resume { + var shrunk: Int = v/2 + while (shrunk != 0 && shrunk != v) { + do emit(shrunk) + do emit(neg(shrunk)) + shrunk = shrunk / 2 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 ) + } + do emit(0) + } + } + +def arbitraryBool { body: => Unit/Generator[Bool] }: Unit = + try body() with Generator[Bool]{ + def generate() = resume { + while (true) { + do emit(random() > 0.5) + } + } + + def shrink(v: Bool) = resume { + var out = v + if(v){ + out = false + }else { + out = true + } + do emit(out) + } + } + +def arbitraryDouble { body: => Unit/Generator[Double] }: Unit = + try body() with Generator[Double]{ + def generate() = resume { + val min = -1000.0 // TODO use double min value + val max = 1000.0 // TODO use double max value + while (true) { + do emit(min + (max - min) * random()) + } + } + + def shrink(v: Double) = resume { + var shrink: Double = v/2.0 + while (shrink != 0.0 && shrink != v) { + do emit(shrink) + do emit(neg(shrink)) + shrink = shrink / 2.0 // in case of odd numbers, rounds to the smaller half (e.g. 5 -> round(2.5) = 2 ) + } + do emit(0.0) + } + } + + +def arbitraryChar { body: => Unit / Generator[Char] }: Unit = + try body() with Generator[Char] { + def generate() = resume { + // ASCII printable characters range from 32 to 126 + while (true) { + do emit(randomInt(32, 127).toChar) + } + } + + def shrink(v: Char) = resume { <> } + } + +def arbitraryString { body: => Unit / Generator[String] }: Unit / Generator[Char] = { + try body() with Generator[String] { + def generate() = resume { + while (true) { + val string: String = collectString { + with boundary; + with limit[Char](randomInt(1, 20)) + do generate[Char]() + } + do emit(string) + } + } + + def shrink(v: String) = resume { <> } + } +} + +def arbitraryString(len: Int) { body: => Unit / Generator[String] }: Unit / Generator[Char] = { + try body() with Generator[String] { + def generate() = resume { + while (true) { + val string: String = collectString { + with boundary; + with limit[Char](len) + do generate[Char]() + } + do emit(string) + } + } + + def shrink(v: String) = resume { <> } + } +} + +def arbitraryList[T] { body: => Unit / Generator[List[T]]} : Unit / Generator[T] = { + try body() with Generator[List[T]]{ + def generate() = resume { + while (true) { + val list = collectList[T]{ + with boundary; + with limit[T](randomInt(1, 10)) + do generate[T]() + } + do emit(list) + } + } + + def shrink(v: List[T]) = resume { <> } + } +} + +def arbitraryList[T](len: Int) { body: => Unit/Generator[List[T]]} : Unit / Generator[T] = { + try body() with Generator[List[T]]{ + def generate() = resume { + while (true) { + val list = collectList[T]{ + with boundary; + with limit[T](len) + do generate[T]() + } + do emit(list) + } + } + + def shrink(v: List[T]) = resume { <> } + } +} + +def evenNumbers { body: => Unit / Generator[Int] }: Unit = + try body() with Generator[Int] { + def generate() = resume { + while(true) { + val nextEmit = randomInt(-50, 50) + if(nextEmit.mod(2) == 0) + do emit(nextEmit) + } + } + + def shrink(v: Int) = resume { + // we know v is even because the generator only produces even numbers + var shrink = v - 2 + while(shrink != 0 && shrink != v){ + do emit(shrink) + shrink = shrink / 2 + } + do emit(0) + } + } + +def alphabeticChar { body: => Unit / Generator[Char] }: Unit = { + try body() with Generator[Char] { + def generate() = resume { + // ASCII printable characters range from 32 to 126 + val min = 32 + val max = 127 // max is exclusive in randomInt + while (true) { + do emit(randomInt(65, 91).toChar) + } + } + + def shrink(v: Char) = resume { <> } + } +} + interface Test { def success(name: String, duration: Int): Unit + def successForall(name: String, passed: Int, duration: Int): Unit + def successExists(name: String, tried: Int, msg: String, duration: Int): Unit def failure(name: String, msg: String, duration: Int): Unit + def failureForall(name: String, passed: Int, msg: String, duration: Int): Unit + def failureExists(name: String, tried: Int, duration: Int): Unit } /// Runs the `body` as a test under the given `name` @@ -80,6 +323,185 @@ def test(name: String) { body: => Unit / Assertion } = { } } +/// forall quantifier for property-based testing +/// version that uses the closest generator in scope (fully value-based generation) +def forall[A](name: String, n: Int) + { body: A => Unit / Assertion }: Unit / { Test, Generator[A] } = { + + val startTime = bench::relativeTimestamp() + var successCounter = 0 + with boundary; + with val x = for[A] { with limit[A](n + 1); do generate[A]() } + + try { + body(x) + if(successCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successForall(name, n, duration) + } + } with Assertion { + def assert(condition, msg) = + if (condition) { + successCounter = successCounter + 1 + resume(())} + else { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureForall(name, successCounter, msg ++ "\n failed on input:\n 1. " ++ genericShow(x), duration) + do stop() + } + } +} + +/// forall quantifier for property-based testing +/// version that that explicitely gets the generators as inputs (type-based-style generation) +def forall[A](name: String, n: Int) + { g: Generator[A]} + { body: A => Unit / Assertion }: Unit / {Test} = { + + val startTime = bench::relativeTimestamp() + var successCounter = 0 + with boundary + with val x = for[A] {with limit[A](n + 1); g.generate[A]()} + + try { + body(x) + if(successCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successForall(name, n, duration) + } + } with Assertion { + def assert(condition, msg) = + if(condition) { + successCounter = successCounter + 1 + resume(()) + } + else { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureForall(name, successCounter, msg ++ "\n failed on input:\n 1. " ++ genericShow(x), duration) + do stop() + } + } +} + +def forall[A, B](name: String, n: Int) + { body: (A, B) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B]} = { + + val startTime = bench::relativeTimestamp() + var successCounter = 0 + with boundary; + with val x = for[A] {with limit[A](n + 1); do generate[A]()} + with val y = for[B] {with limit[B](n + 1); do generate[B]()} + + try { + body(x, y) + if(successCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successForall(name, n, duration) + do stop() + } + } with Assertion { + def assert(condition, msg) = + if(condition) { + successCounter = successCounter + 1 + resume(()) + } + else { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureForall(name, successCounter, msg ++ "\n failed on inputs:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y), duration) + do stop() + } + } +} + +def forall[A, B, C](name: String, n: Int) + { body: (A, B, C) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B], Generator[C]} = { + + val startTime = bench::relativeTimestamp() + var successCounter = 0 + with boundary; + with val x = for[A] {with limit[A](n + 1); do generate[A]()} + with val y = for[B] {with limit[B](n + 1); do generate[B]()} + with val z = for[C] {with limit[C](n + 1); do generate[C]()} + + try { + body(x, y, z) + if(successCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successForall(name, n, duration) + do stop() + } + } with Assertion { + def assert(condition, msg) = + if(condition) { + successCounter = successCounter + 1 + resume(()) + } + else { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureForall(name, successCounter, msg ++ "\n failed on inputs:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y) ++ "\n 3. " ++ genericShow(z), duration) + do stop() + } + } +} + +/// exists quantifier for property-based testing +def exists[A](name: String, n: Int) + { body: A => Unit / Assertion }: Unit / {Test, Generator[A]} = { + + val startTime = bench::relativeTimestamp() + var triedCounter = 0 + with boundary + with val x = for[A] {with limit[A](n + 1); do generate[A]()} + + try { + body(x) + if(triedCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureExists(name, triedCounter, duration) + } + } with Assertion { + def assert(condition, msg) = + if (condition) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successExists(name, triedCounter, " example value:\n 1. " ++ genericShow(x), duration) + do stop() + } + else { + triedCounter = triedCounter + 1 + resume(()) + } + } +} + +def exists[A, B](name: String, n: Int) + { body: (A, B) => Unit / Assertion }: Unit / {Test, Generator[A], Generator[B]} = { + + val startTime = bench::relativeTimestamp() + var triedCounter = 0 + with boundary + with val x = for[A] {with limit[A](n + 1); do generate[A]()} + with val y = for[B] {with limit[B](n + 1); do generate[B]()} + + try { + body(x, y) + if(triedCounter == n) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do failureExists(name, triedCounter, duration) + } + } with Assertion { + def assert(condition, msg) = + if(condition) { + val duration = Duration::diff(startTime, bench::relativeTimestamp()) + do successExists(name, triedCounter, "\n example values:\n 1. " ++ genericShow(x) ++ "\n 2. " ++ genericShow(y), duration) + do stop() + } + else { + triedCounter = triedCounter + 1 + resume(()) + } + } +} + /// Run a test suite with a given `name`. /// - If `printTimes` is `true` (or missing), prints out time in milliseconds. /// - Formats automatically using ANSI escapes. @@ -91,9 +513,17 @@ def test(name: String) { body: => Unit / Assertion } = { /// test("1 + 1 == 2") { /// assertEqual(1 + 1, 2) /// } +/// with arbitraryInt; +/// forall[Int]("reverse-singleton", 100){ x => +/// assertEqual( +/// reverse([x]), +/// [x] +/// ) +/// } /// } /// ``` def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = { + println("here") with Formatted::formatting; def ms(duration: Int): String / Formatted = @@ -107,8 +537,13 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } if (n == 0) { dim(s) } else { colorIfNonZero(s) } - var failed = 0 - var passed = 0 + var failedUnit = 0 + var passedUnit = 0 + var failedForall = 0 + var passedForall = 0 + var failedExists = 0 + var passedExists = 0 + // TODO check if computing time works correctly // 1) Print the name of the test println(name.bold) @@ -116,31 +551,71 @@ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } // 2) Run the tests, timing them val totalDuration = timed { try { body() } with Test { - // 2a) Handle a passing test on success + // 2a) Handle a passing unit test on success def success(name, duration) = { - passed = passed + 1 + passedUnit = passedUnit + 1 println("✓".green ++ " " ++ name ++ duration.ms) resume(()) } - // 2b) Handle a failing test on failure, additionally printing its message + // 2b) Handle a failing unit test on failure, additionally printing its message def failure(name, msg, duration) = { - failed = failed + 1 + failedUnit = failedUnit + 1 + println("✕".red ++ " " ++ name ++ duration.ms) + println(" " ++ msg.red) + resume(()) + } + + // 2c) Handle a passing forall test on success + def successForall(name, passed, duration) = { + passedForall = passedForall + 1 + println("✓".green ++ " " ++ name ++ ", passed " ++ passed.show ++ " tests " ++ duration.ms) + resume(()) + } + + // 2d) Handle a failing forall test on failure, additionally printing its message + def failureForall(name, passed, msg, duration) = { + failedForall = failedForall + 1 println("✕".red ++ " " ++ name ++ duration.ms) + if(passed == 1){ + println(" ! Falsified after " ++ show(passed).red ++ " passed test:" ++ duration.ms) + } else { println(" ! Falsified after " ++ show(passed).red ++ " passed tests:" ++ duration.ms) } println(" " ++ msg.red) resume(()) } + + // 2e) Handle a passing exists test on success + def successExists(name, tried, msg, duration) = { + passedExists = passedExists + 1 + //println("✓".green ++ " " ++ name ++ ", found after " ++ tried.show ++ " tests " ++ duration.ms) + println("✓".green ++ " " ++ name ++ duration.ms) + if(tried == 1){ + println(" Verified after " ++ tried.show ++ " tried input") + } else { println(" Verified after " ++ tried.show ++ " tried inputs") } + println(" " ++ msg.green) + resume(()) + } + + // 2f) Handle a failing exists test on failure, additionally printing its message + def failureExists(name, tried, duration) = { + failedExists = failedExists + 1 + println("✕".red ++ " " ++ name ++ duration.ms) + println(" ! Tried " ++ tried.show ++ " different inputs but all failed" ++ duration.ms) + resume(()) + } } } // 3) Format the test results + println(" ") + println(" " ++ (show(passedUnit + passedForall + passedExists) ++ " pass").dimWhenZeroElse(passedUnit + passedForall + passedExists) { green }) + println(" " ++ (show(failedUnit + failedForall + failedExists) ++ " fail").dimWhenZeroElse(failedUnit + failedForall + failedExists) { red }) + println(" " ++ (passedUnit + failedUnit + passedForall + failedForall + passedExists + failedExists).show ++ " test(s) total" ++ totalDuration.ms) + println("") - println(" " ++ (passed.show ++ " pass").dimWhenZeroElse(passed) { green }) - println(" " ++ (failed.show ++ " fail").dimWhenZeroElse(failed) { red }) - println(" " ++ (passed + failed).show ++ " tests total" ++ totalDuration.ms) // 4) Return true if all tests succeeded, otherwise false - return failed == 0 + return failedUnit == 0 && failedForall == 0 && failedExists == 0 } /// See `suite` above. @@ -157,4 +632,4 @@ def mainSuite(name: String) { body: => Unit / { Test, Formatted } }: Unit = { val result = suite(name, true) { body } val exitCode = if (result) 0 else 1 exit(exitCode) -} +} \ No newline at end of file From 28e8d458d4ceee74278da608d259ae3bf90f71f7 Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:34:39 +0100 Subject: [PATCH 02/41] Fix: Splices with non-String type break if there is no splice (#901) Fixes #892 --- .../main/scala/effekt/RecursiveDescent.scala | 7 +++---- examples/pos/string_interpolation.check | 3 ++- examples/pos/string_interpolation.effekt | 11 ++++++++++ .../pos/string_interpolation_literal.check | 5 +++++ .../pos/string_interpolation_literal.effekt | 21 +++++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 examples/pos/string_interpolation_literal.check create mode 100644 examples/pos/string_interpolation_literal.effekt diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 9db4032b3..7fc0bb7e4 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -1030,10 +1030,9 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) def templateString(): Term = nonterminal: backtrack(idRef()) ~ template() match { - // We do not need to apply any transformation if there are no splices - case _ ~ Template(str :: Nil, Nil) => StringLit(str) - case _ ~ Template(strs, Nil) => fail("Cannot occur") - // s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y) } + // We do not need to apply any transformation if there are no splices _and_ no custom handler id is given + case None ~ Template(str :: Nil, Nil) => StringLit(str) + // s"a${x}b${y}" ~> s { do literal("a"); do splice(x); do literal("b"); do splice(y); return () } case id ~ Template(strs, args) => val target = id.getOrElse(IdRef(Nil, "s")) val doLits = strs.map { s => diff --git a/examples/pos/string_interpolation.check b/examples/pos/string_interpolation.check index 04bcd4f9e..bff0393aa 100644 --- a/examples/pos/string_interpolation.check +++ b/examples/pos/string_interpolation.check @@ -1,2 +1,3 @@ GET https://api.effekt-lang.org/users/effekt/resource/42 -Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x \ No newline at end of file +Fix point combinator: \ f -> (\ x -> f x x) \ x -> f x x +12 diff --git a/examples/pos/string_interpolation.effekt b/examples/pos/string_interpolation.effekt index 509b3fc9f..b5fe1e529 100644 --- a/examples/pos/string_interpolation.effekt +++ b/examples/pos/string_interpolation.effekt @@ -28,6 +28,15 @@ def pretty { prog: () => Unit / {literal, splice[Expr]} }: String = { } } +def len { prog: () => Unit / {literal} }: Int = { + try { + prog() + 0 + } with literal { s => + s.length + } +} + def main() = { val domain = "https://api.effekt-lang.org" val user = "effekt" @@ -36,4 +45,6 @@ def main() = { val fixpoint = Abs("f", App(Abs("x", App(Var("f"), App(Var("x"), Var("x")))), Abs("x", App(Var("f"), App(Var("x"), Var("x")))))) println(pretty"Fix point combinator: ${fixpoint}") + + println(show(len"hello, world")) } diff --git a/examples/pos/string_interpolation_literal.check b/examples/pos/string_interpolation_literal.check new file mode 100644 index 000000000..8d842fe6e --- /dev/null +++ b/examples/pos/string_interpolation_literal.check @@ -0,0 +1,5 @@ +42 +[WARNING] Frobnicators have been jabberwocked! +-1 +[ERROR] Stuff went wrong! +[ERROR] Aborting! diff --git a/examples/pos/string_interpolation_literal.effekt b/examples/pos/string_interpolation_literal.effekt new file mode 100644 index 000000000..978637456 --- /dev/null +++ b/examples/pos/string_interpolation_literal.effekt @@ -0,0 +1,21 @@ +effect log(msg: String): Unit + +def error { body: => Unit / literal }: Unit / log = + try body() with literal { x => do log("[ERROR] " ++ x); resume(()) } + +def warn { body: => Unit / literal }: Unit / log = + try body() with literal { x => do log("[WARNING] " ++ x); resume(()) } + +def doc { body: => Unit / {literal} }: Unit = + try body() with literal { _ => resume(()) } + +def main() = try { + doc"This is the doc string for my main function!" + + println(42) + warn"Frobnicators have been jabberwocked!" + + println(-1) + error"Stuff went wrong!" + error"Aborting!" +} with log { msg => println(msg); resume(()) } From e1b92bb1fc36660caa773884948f4ec6f321603c Mon Sep 17 00:00:00 2001 From: Marvin Date: Fri, 28 Mar 2025 11:39:46 +0100 Subject: [PATCH 03/41] Add lambda calculus NbE example/benchmark (#898) This is a cleaned up version of my [effectful Pi-Day submission](https://gist.github.com/marvinborner/44236c67bbcfdaa184f37bc9b784a73f). Here I've decided to not use the lookup effect boxing for looking up bindings from the environment and used a hashmap instead. --- examples/benchmarks/other/nbe.check | 10 ++ examples/benchmarks/other/nbe.effekt | 151 +++++++++++++++++++++++++++ libraries/common/stringbuffer.effekt | 2 +- 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 examples/benchmarks/other/nbe.check create mode 100644 examples/benchmarks/other/nbe.effekt diff --git a/examples/benchmarks/other/nbe.check b/examples/benchmarks/other/nbe.check new file mode 100644 index 000000000..6c2239814 --- /dev/null +++ b/examples/benchmarks/other/nbe.check @@ -0,0 +1,10 @@ +S ~> S: +λ1.λ2.λ3.((1 3) (2 3)) ~> λ1.λ2.λ3.((1 3) (2 3)) +(ι (ι (ι (ι ι)))) ~> S: +(λ1.((1 λ2.λ3.λ4.((2 4) (3 4))) λ5.λ6.5) (λ7.((7 λ8.λ9.λ10.((8 10) (9 10))) λ11.λ12.11) (λ13.((13 λ14.λ15.λ16.((14 16) (15 16))) λ17.λ18.17) (λ19.((19 λ20.λ21.λ22.((20 22) (21 22))) λ23.λ24.23) λ25.((25 λ26.λ27.λ28.((26 28) (27 28))) λ29.λ30.29))))) ~> λ1.λ2.λ3.((1 3) (2 3)) +(2^3)%3 == 2 // using Church numerals: +(((λ1.λ2.λ3.(((3 λ4.λ5.(4 λ6.((5 λ7.λ8.(7 ((6 7) 8))) 6))) (λ9.λ10.(10 9) λ11.λ12.12)) λ13.(((2 1) (((3 λ14.λ15.λ16.(14 λ17.((15 17) 16))) λ18.18) λ19.λ20.(20 19))) (((3 λ21.λ22.21) λ23.23) λ24.24))) λ25.λ26.(25 (25 26))) λ27.λ28.(27 (27 (27 28)))) λ29.λ30.(29 (29 (29 30)))) ~> λ1.λ2.(1 (1 2)) +5! == 120 // using Church numerals: +(λ1.λ2.(((1 λ3.λ4.(4 (3 λ5.λ6.((4 5) (5 6))))) λ7.2) λ8.8) λ9.λ10.(9 (9 (9 (9 (9 10)))))) ~> λ1.λ2.(1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 2)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) +(prime? 7) == true // using Church numerals and Wilson's Theorem: +(λ1.(λ2.((2 λ3.λ4.λ5.5) λ6.λ7.6) ((λ8.λ9.λ10.λ11.(((8 (λ12.λ13.(13 12) λ14.λ15.14)) ((8 λ16.(((9 λ17.λ18.λ19.((19 (17 (10 18))) 18)) λ20.16) 11)) λ21.11)) λ22.λ23.23) (λ24.λ25.λ26.(25 ((24 25) 26)) (λ27.λ28.(((27 λ29.λ30.(30 (29 λ31.λ32.((30 31) (31 32))))) λ33.28) λ34.34) (λ35.λ36.λ37.((λ38.λ39.(39 38) λ40.40) ((35 λ41.(λ42.λ43.(43 42) (41 36))) λ44.37)) 1)))) 1)) λ45.λ46.(45 (45 (45 (45 (45 (45 (45 46)))))))) ~> λ1.λ2.1 diff --git a/examples/benchmarks/other/nbe.effekt b/examples/benchmarks/other/nbe.effekt new file mode 100644 index 000000000..64304ea52 --- /dev/null +++ b/examples/benchmarks/other/nbe.effekt @@ -0,0 +1,151 @@ +/// Demo of an effectful Normalization by Evaluation (NbE) implementation for the pure, untyped lambda calculus. +/// Uses the Call-by-Value reduction order, same as host. + +import io +import map +import stream +import process +import stringbuffer + +type Name = Int + +effect fresh(): Name +effect lookup(n: Name): Int + +/// lambda term +type Term { + Abs(n: Name, t: Term) + App(a: Term, b: Term) + Var(n: Name) +} + +/// lambda term in normal domain +type Value { + VNeu(neu: Neutral) + VClo(n: Name, t: Term, env: Map[Name, Value]) +} + +/// lambda term in neutral domain (not yet reducible) +type Neutral { + NVar(n: Name) + NApp(a: Neutral, b: Value) +} + +/// evaluate a single term without going into abstractions +def eval(env: Map[Name, Value], t: Term): Value = t match { + case Abs(n, t) => VClo(n, t, env) + case App(a, b) => apply(eval(env, a), eval(env, b)) + case Var(n) => env.getOrElse(n) { VNeu(NVar(n)) } +} + +/// apply terms in their normal domain +/// this does the actual substitution via environment lookup +def apply(a: Value, b: Value): Value = a match { + case VNeu(neu) => VNeu(NApp(neu, b)) + case VClo(n, t, env) => eval(env.put(n, b), t) +} + +/// reflect variable name to the neutral domain +def reflect(n: Name): Value = VNeu(NVar(n)) + +/// convert terms to their normal form (in term domain) +def reify(v: Value): Term / fresh = v match { + case VNeu(NVar(n)) => Var(n) + case VNeu(NApp(a, b)) => App(reify(VNeu(a)), reify(b)) + case _ => { + val n = do fresh() + Abs(n, reify(apply(v, reflect(n)))) + } +} + +/// strong normalization of the term +def normalize(t: Term): Term = { + var i = 0 + try reify(eval(empty[Name, Value](compareInt), t)) + with fresh { i = i + 1; resume(i) } +} + +/// parse named term from BLC stream (de Bruijn) +def parse(): Term / { read[Bool], stop } = { + def go(): Term / { fresh, lookup } = + (do read[Bool](), do read[Bool]) match { + case (false, false) => { + val n = do fresh() + Abs(n, try go() with lookup { i => + resume(if (i == 0) n else do lookup(i - 1)) + }) + } + case (false, true ) => App(go(), go()) + case (true , false) => Var(do lookup(0)) + case (true , true ) => { + var i = 1 + while (do read[Bool]()) i = i + 1 + Var(do lookup(i)) + } + } + + var i = 0 + try go() + with fresh { i = i + 1; resume(i) } + with lookup { n => + println("error: free variable " ++ n.show) + exit(1) + } +} + +/// helper function for pretty string interpolation of terms +def pretty { s: () => Unit / { literal, splice[Term] } }: String = { + with stringBuffer + + try { s(); do flush() } + with literal { l => resume(do write(l)) } + with splice[Term] { t => + t match { + case Abs(n, t) => do write("λ" ++ n.show ++ pretty".${t}") + case App(a, b) => do write(pretty"(${a} ${b})") + case Var(v) => do write(v.show) + } + resume(()) + } +} + +/// convert char stream to bit stream, skipping non-bit chars +def bits { p: () => Unit / { read[Bool], stop } }: Unit / read[Char] = + try exhaustively { p() } + with read[Bool] { + with exhaustively + val c = do read[Char]() + if (c == '0') resume { false } + if (c == '1') resume { true } + } + +/// evaluate the input BLC string and prettify it +def testNormalization(input: String) = { + with feed(input) + with bits + val t = parse() + println(pretty"${t} ~> ${normalize(t)}") +} + +def main() = { + var t = "00000001011110100111010" + println("S ~> S:") + testNormalization(t) + + t = "010001011000000001011110100111010000011001000101100000000101111010011101000001100100010110000000010111101001110100000110010001011000000001011110100111010000011000010110000000010111101001110100000110" + println("(ι (ι (ι (ι ι)))) ~> S:") + testNormalization(t) + + t = "010101000000010101100000011100001011100000011100101111011010100100000110110000010000101011110111100101011100000000111100001011110101100010000001101100101011100000110001000100000011100111010000001110011100111010000001110011100111010" + println("(2^3)%3 == 2 // using Church numerals:") + testNormalization(t) + + // TODO: is there an off-by-one in the string buffer? + t = "010000010101110000001100111000000101111011001110100011000100000011100111001110011100111010" + println("5! == 120 // using Church numerals:") + testNormalization(t) + + t = "010001000101100000001000001100101000000000101011111001000001101100000110010111110000101011111000000001011001111001111111011011000110110001100000100100000001110010111101101001000001010111000000110011100000010111101100111010001100010010000000101000001101100010010111100001000001101100110111000110101000000111001110011100111001110011100111010" + println("(prime? 7) == true // using Church numerals and Wilson's Theorem:") + testNormalization(t) +} diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index 92b8fa280..60b4a3b89 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -14,7 +14,7 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = { var pos = 0 def ensureCapacity(sizeToAdd: Int): Unit = { - val cap = buffer.size - pos + 1 + val cap = buffer.size - pos if (sizeToAdd <= cap) () else { // Double the capacity while ensuring the required capacity From fe047ceb386f3a436f7ecbac1a20f598e426f59c Mon Sep 17 00:00:00 2001 From: Marvin Date: Fri, 28 Mar 2025 12:49:54 +0100 Subject: [PATCH 04/41] Run tests without optimization (#851) Closes #846 This runs every test without optimization *in addition* to running them with optimization. While this is good for testing, we should change this once it's mergable so our CI doesn't take ~~twice as long~~ much longer. There are many new errors now. I've spent some time investigating and they generally fall into these two categories: - no block info (unsoundness in the optimizer) - unsupported LLVM feature (toplevel object definitions, reached hole (no LLVM FFI for extern definition)), which were somehow optimized away - valgrind error --- .../src/test/scala/effekt/EffektTests.scala | 22 +++++++++++++--- .../test/scala/effekt/JavaScriptTests.scala | 14 +++++++++- .../jvm/src/test/scala/effekt/LLVMTests.scala | 26 +++++++++++++++++++ .../src/test/scala/effekt/StdlibTests.scala | 24 +++++++++++++++++ 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/EffektTests.scala b/effekt/jvm/src/test/scala/effekt/EffektTests.scala index ee3eabbe1..aaaa014d8 100644 --- a/effekt/jvm/src/test/scala/effekt/EffektTests.scala +++ b/effekt/jvm/src/test/scala/effekt/EffektTests.scala @@ -36,9 +36,12 @@ trait EffektTests extends munit.FunSuite { def negatives: List[File] = List() + // Test files that should be run with optimizations disabled + def withoutOptimizations: List[File] = List() + def runTestFor(input: File, expected: String): Unit = test(input.getPath + s" (${backendName})") { - assertNoDiff(run(input), expected) + assertNoDiff(run(input, true), expected) } // one shared driver for all tests in this test runner @@ -59,7 +62,7 @@ trait EffektTests extends munit.FunSuite { compiler.compileFile(input.getPath, configs) compiler.context.backup - def run(input: File): String = + def run(input: File, optimizations: Boolean): String = val compiler = driver var options = Seq( "--Koutput", "string", @@ -68,6 +71,7 @@ trait EffektTests extends munit.FunSuite { ) if (valgrind) options = options :+ "--valgrind" if (debug) options = options :+ "--debug" + if (!optimizations) options = options :+ "--no-optimize" val configs = compiler.createConfig(options) configs.verify() @@ -96,6 +100,16 @@ trait EffektTests extends munit.FunSuite { case Right(value) => negatives.foreach(runNegativeTestsIn) positives.foreach(runPositiveTestsIn) + withoutOptimizations.foreach(runWithoutOptimizations) + } + + def runWithoutOptimizations(dir: File): Unit = + foreachFileIn(dir) { + case (f, None) => sys error s"Missing checkfile for ${f.getPath}" + case (f, Some(expected)) => + test(s"${f.getPath} (${backendName})") { + assertNoDiff(run(f, false), expected) + } } def runPositiveTestsIn(dir: File): Unit = @@ -103,7 +117,7 @@ trait EffektTests extends munit.FunSuite { case (f, None) => sys error s"Missing checkfile for ${f.getPath}" case (f, Some(expected)) => test(s"${f.getPath} (${backendName})") { - assertNoDiff(run(f), expected) + assertNoDiff(run(f, true), expected) } } @@ -111,7 +125,7 @@ trait EffektTests extends munit.FunSuite { foreachFileIn(dir) { case (f, Some(expected)) => test(s"${f.getPath} (${backendName})") { - assertNoDiff(run(f), expected) + assertNoDiff(run(f, true), expected) } case (f, None) => diff --git a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala index c3626213d..262e89afc 100644 --- a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala +++ b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala @@ -24,6 +24,18 @@ class JavaScriptTests extends EffektTests { examplesDir / "neg" ) + override lazy val withoutOptimizations: List[File] = List( + // contifying under reset + //examplesDir / "pos" / "issue842.effekt", + //examplesDir / "pos" / "issue861.effekt", + + // syntax error (multiple declaration) + //examplesDir / "char" / "ascii_isalphanumeric.effekt", + //examplesDir / "char" / "ascii_iswhitespace.effekt", + //examplesDir / "pos" / "parser.effekt", + //examplesDir / "pos" / "probabilistic.effekt", + ) + override def ignored: List[File] = List( // unsafe cont examplesDir / "pos" / "propagators.effekt" @@ -58,7 +70,7 @@ object TestUtils { val shouldGenerate = regenerateAll || f.lastModified() > checkfile.lastModified() if (!isIgnored && shouldGenerate) { println(s"Writing checkfile for ${f}") - val out = run(f) + val out = run(f, true) // Save checkfile in source folder (e.g. examples/) // We remove ansi colors to make check files human-readable. diff --git a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala index e9e5fd18e..e0b1faca5 100644 --- a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala +++ b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala @@ -53,6 +53,32 @@ class LLVMTests extends EffektTests { examplesDir / "pos" / "issue733.effekt", ) + override lazy val withoutOptimizations: List[File] = List( + // contifying under reset + //examplesDir / "pos" / "issue842.effekt", + //examplesDir / "pos" / "issue861.effekt", + + // top-level object definition + //examplesDir / "pos" / "object" / "if_control_effect.effekt", + //examplesDir / "pos" / "lambdas" / "toplevel_objects.effekt", + //examplesDir / "pos" / "type_omission_op.effekt", + //examplesDir / "pos" / "bidirectional" / "higherorderobject.effekt", + //examplesDir / "pos" / "bidirectional" / "res_obj_boxed.effekt", + //examplesDir / "pos" / "bidirectional" / "effectfulobject.effekt", + + // no block info + //examplesDir / "pos" / "capture" / "regions.effekt", + //examplesDir / "pos" / "capture" / "selfregion.effekt", + //examplesDir / "benchmarks" / "other" / "generator.effekt", + + // hole + //examplesDir / "pos" / "bidirectional" / "typeparametric.effekt", + + // segfault + //examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt", + //examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt", + ) + override lazy val ignored: List[File] = missingFeatures ++ noValgrind(examplesDir) } diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index f7e92745b..6922267a7 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -12,13 +12,33 @@ abstract class StdlibTests extends EffektTests { ) override def ignored: List[File] = List() + + override def withoutOptimizations: List[File] = List() } class StdlibJavaScriptTests extends StdlibTests { def backendName: String = "js" + override def withoutOptimizations: List[File] = List( + examplesDir / "stdlib" / "acme.effekt", + + //examplesDir / "stdlib" / "json.effekt", + //examplesDir / "stdlib" / "exception" / "combinators.effekt", + + // reference error (k is not defined) + //examplesDir / "stdlib" / "stream" / "fibonacci.effekt", + //examplesDir / "stdlib" / "list" / "flatmap.effekt", + //examplesDir / "stdlib" / "list" / "sortBy.effekt", + //examplesDir / "stdlib" / "stream" / "zip.effekt", + //examplesDir / "stdlib" / "stream" / "characters.effekt", + + // oom + //examplesDir / "stdlib" / "list" / "deleteat.effekt", + ) + override def ignored: List[File] = List() } + abstract class StdlibChezTests extends StdlibTests { override def ignored: List[File] = List( // Not implemented yet @@ -39,6 +59,10 @@ class StdlibLLVMTests extends StdlibTests { override def valgrind = sys.env.get("EFFEKT_VALGRIND").nonEmpty override def debug = sys.env.get("EFFEKT_DEBUG").nonEmpty + override def withoutOptimizations: List[File] = List( + examplesDir / "stdlib" / "acme.effekt", + ) + override def ignored: List[File] = List( // String comparison using `<`, `<=`, `>`, `>=` is not implemented yet on LLVM examplesDir / "stdlib" / "string" / "compare.effekt", From 6f8b155aeb53063002728f817c50c628342f97a4 Mon Sep 17 00:00:00 2001 From: Phi Date: Fri, 28 Mar 2025 12:57:10 +0100 Subject: [PATCH 05/41] Fix inferring effectful while header (#902) Should fix #601 --- effekt/shared/src/main/scala/effekt/Typer.scala | 2 +- examples/neg/typer/infer-effectful-while.check | 3 +++ examples/neg/typer/infer-effectful-while.effekt | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 examples/neg/typer/infer-effectful-while.check create mode 100644 examples/neg/typer/infer-effectful-while.effekt diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index ae6d34ced..edc45379e 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -119,7 +119,7 @@ object Typer extends Phase[NameResolved, Typechecked] { checkStmt(s, expectedType) }.getOrElse(Result(TUnit, ConcreteEffects.empty)) - Result(Context.join(bodyTpe, defaultTpe), defaultEffs ++ bodyEffs) + Result(Context.join(bodyTpe, defaultTpe), defaultEffs ++ bodyEffs ++ guardEffs) case source.Var(id) => id.symbol match { case x: RefBinder => Context.lookup(x) match { diff --git a/examples/neg/typer/infer-effectful-while.check b/examples/neg/typer/infer-effectful-while.check new file mode 100644 index 000000000..ce30f309a --- /dev/null +++ b/examples/neg/typer/infer-effectful-while.check @@ -0,0 +1,3 @@ +[error] examples/neg/typer/infer-effectful-while.effekt:14:1: Main cannot have effects, but includes effects: { Foo } +def main() = { +^ diff --git a/examples/neg/typer/infer-effectful-while.effekt b/examples/neg/typer/infer-effectful-while.effekt new file mode 100644 index 000000000..44efff167 --- /dev/null +++ b/examples/neg/typer/infer-effectful-while.effekt @@ -0,0 +1,16 @@ +interface Foo { def foo(): Unit } + +def bar(): Bool / Foo = { + do foo() + false +} + +def fooo() = { + while(bar() is true) { + () + } +} + +def main() = { + fooo() +} From 9195bbece3740631ce1780e755f2746be3e0986a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattis=20B=C3=B6ckle?= Date: Fri, 28 Mar 2025 12:57:31 +0100 Subject: [PATCH 06/41] Allow trailing comma in list literals (#900) Fixes #873 --- .../scala/effekt/RecursiveDescentTests.scala | 6 +++++ .../main/scala/effekt/RecursiveDescent.scala | 25 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala index 5bfb2f798..44731886a 100644 --- a/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala +++ b/effekt/jvm/src/test/scala/effekt/RecursiveDescentTests.scala @@ -125,6 +125,12 @@ class RecursiveDescentTests extends munit.FunSuite { parseExpr("fun() { foo(()) }") parseExpr("10.seconds") + + parseExpr("[1,2,3]") + parseExpr("[3,2,1,]") + parseExpr("[]") + parseExpr("[,]") + intercept[Throwable] { parseExpr("[,1]") } } test("Boxing") { diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 7fc0bb7e4..02d996280 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -993,7 +993,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) } def listLiteral(): Term = nonterminal: - many(expr, `[`, `,`, `]`).foldRight(NilTree) { ConsTree } + manyTrailing(expr, `[`, `,`, `]`).foldRight(NilTree) { ConsTree } private def NilTree: Term = Call(IdTarget(IdRef(List(), "Nil")), Nil, Nil, Nil) @@ -1406,6 +1406,29 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) components.toList } + inline def manyTrailing[T](p: () => T, before: TokenKind, sep: TokenKind, after: TokenKind): List[T] = + consume(before) + if (peek(after)) { + consume(after) + Nil + } else if (peek(sep)) { + consume(sep) + consume(after) + Nil + } else { + val components: ListBuffer[T] = ListBuffer.empty + components += p() + while (peek(sep)) { + consume(sep) + + if (!peek(after)) { + components += p() + } + } + consume(after) + components.toList + } + // Positions From 7219512abae072130fd2a75e9f930307ff19fe4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Fri, 28 Mar 2025 14:26:01 +0100 Subject: [PATCH 07/41] Standalone language server (#885) Free the language server from Kiama and implement a test suite. Found some bugs while reimplementing, and I've confirmed all of them also exist in the old server. It should be on-par with the old implementation in terms of features and bugs. Depends on https://github.com/effekt-lang/kiama/pull/9. **Differences to the old Server:** * Added unit test suite * New server runs single threaded and sequentially * Compile on `didChange`, by default * No Monto anymore, added a custom `$/effekt/publishIR` notification instead. **Implementation status:** * [x] `initialize` * [x] `shutdown` - not covered by unit tests - they cannot handle the process exit * [x] `exit` - not covered by unit tests - they cannot handle the process exit * [x] setTrace * [x] Diagnostics `afterCompilation` * [x] `didChange` * [x] `didOpen` * [x] `didSave` * [x] `didClose` * [ ] `hover` * [x] for symbols * [ ] :bug: for holes: https://github.com/effekt-lang/effekt/issues/549 * [ ] `documentSymbol` * [x] impl & test * [ ] :bug: Fix spurious symbols at `(0, 0)`: https://github.com/effekt-lang/effekt/issues/895 * [ ] `references` * [x] impl & test * [ ] :bug: Fix not all references being returned (see test case): https://github.com/effekt-lang/effekt/issues/896 * [x] `inlayHint` * [x] impl & test * [x] :bug: inlayHints and hoverInfo sometimes return null (as in old implementation): * [x] inlayHints: https://github.com/effekt-lang/effekt/issues/876 (fixed in #894) * [x] hoverInfo: https://github.com/effekt-lang/effekt/issues/366 (**can't reproduce**) Note that the timing issue should be gone as we now run on `newSingleThreadExecutor` (which claims "Tasks are guaranteed to execute sequentially"), the caching issues have been fixed in #894. * [ ] `getCodeActions` * [x] port impl from old server * [ ] :bug: impl doesn't even work in old server: https://github.com/effekt-lang/effekt/issues/897 * [x] `definition` * [ ] `notebookDocument/*` * not implemented, student works on another branch * [ ] publish IR (`$/effekt/publishIR`), replacing Monto * [x] impl server and test (this PR) * [x] impl client (https://github.com/effekt-lang/effekt-vscode/pull/63) * [ ] :bug: many options (e.g. `target`) don't work because the server only runs the compiler frontend - same issue with old server Important questions to reviewers: * [x] `effekt/jvm/src/main/scala/effekt/KiamaUtils.scala` is MPL-licensed code from Kiama - what do we do with this? * I decided to move it to Kiama to be safe. --------- Co-authored-by: Marvin Borner --- effekt/jvm/src/main/scala/effekt/Main.scala | 14 +- effekt/jvm/src/main/scala/effekt/Server.scala | 565 +++++++++++--- .../jvm/src/test/scala/effekt/LSPTests.scala | 692 ++++++++++++++++++ kiama | 2 +- 4 files changed, 1165 insertions(+), 108 deletions(-) create mode 100644 effekt/jvm/src/test/scala/effekt/LSPTests.scala diff --git a/effekt/jvm/src/main/scala/effekt/Main.scala b/effekt/jvm/src/main/scala/effekt/Main.scala index 5d8578e8d..d600889d9 100644 --- a/effekt/jvm/src/main/scala/effekt/Main.scala +++ b/effekt/jvm/src/main/scala/effekt/Main.scala @@ -17,14 +17,19 @@ object Main { parseArgs(args) } catch { case e: ScallopException => - System.err.println(e.getMessage()) + System.err.println(e.getMessage) return } if (config.server()) { - Server.launch(config) + val serverConfig = ServerConfig( + debug = config.debug(), + debugPort = config.debugPort() + ) + val server = new Server(config) + server.launch(serverConfig) } else if (config.repl()) { - new Repl(Server).run(config) + new Repl(new Driver {}).run(config) } else { compileFiles(config) } @@ -43,8 +48,9 @@ object Main { * Compile files specified in the configuration. */ private def compileFiles(config: EffektConfig): Unit = { + val driver = new Driver {} for (filename <- config.filenames()) { - Server.compileFile(filename, config) + driver.compileFile(filename, config) } } } diff --git a/effekt/jvm/src/main/scala/effekt/Server.scala b/effekt/jvm/src/main/scala/effekt/Server.scala index 3678ba8bc..17705f7be 100644 --- a/effekt/jvm/src/main/scala/effekt/Server.scala +++ b/effekt/jvm/src/main/scala/effekt/Server.scala @@ -1,60 +1,149 @@ package effekt +import com.google.gson.JsonElement +import kiama.util.Convert.* import effekt.context.Context -import effekt.core.PrettyPrinter -import effekt.source.{ FunDef, Hole, ModuleDecl, Tree } -import effekt.util.{ PlainMessaging, getOrElseAborting } +import effekt.source.Def.FunDef +import effekt.source.Term.Hole +import effekt.source.Tree +import effekt.symbols.Binder.{ValBinder, VarBinder} +import effekt.symbols.BlockTypeConstructor.{ExternInterface, Interface} +import effekt.symbols.TypeConstructor.{DataType, ExternType} +import effekt.symbols.{Anon, Binder, Callable, Effects, Module, Param, Symbol, TypeAlias, TypePrinter, UserFunction, ValueType, isSynthetic} +import effekt.util.{PlainMessaging, PrettyPrinter} import effekt.util.messages.EffektError -import kiama.util.{ Filenames, Notebook, NotebookCell, Position, Services, Source, Range } -import kiama.output.PrettyPrinterTypes.Document -import org.eclipse.lsp4j.{ Diagnostic, DocumentSymbol, ExecuteCommandParams, SymbolKind } +import kiama.util.Collections.{mapToJavaMap, seqToJavaList} +import kiama.util.{Collections, Convert, Position, Source} +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.jsonrpc.{Launcher, messages} +import org.eclipse.lsp4j.launch.LSPLauncher +import org.eclipse.lsp4j.services.* +import org.eclipse.lsp4j.{CodeAction, CodeActionKind, CodeActionParams, Command, DefinitionParams, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InitializeParams, InitializeResult, InlayHint, InlayHintKind, InlayHintParams, Location, LocationLink, MarkupContent, MessageParams, MessageType, PublishDiagnosticsParams, ReferenceParams, SaveOptions, ServerCapabilities, SetTraceParams, SymbolInformation, SymbolKind, TextDocumentSaveRegistrationOptions, TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkspaceEdit, Range as LSPRange} + +import java.io.{InputStream, OutputStream, PrintWriter} +import java.net.ServerSocket +import java.nio.file.Paths +import java.util +import java.util.concurrent.{CompletableFuture, ExecutorService, Executors} /** - * effekt.Intelligence <--- gathers information -- LSPServer --- provides LSP interface ---> kiama.Server - * | - * v - * effekt.Compiler + * Effekt Language Server + * + * @param compileOnChange Whether to compile on `didChange` events + * Currently disabled because references are erased when there are any compiler errors. + * Therefore, we currently only update on `didSave` until we have working caching for references. */ -trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with Driver with Intelligence { +class Server(config: EffektConfig, compileOnChange: Boolean=false) extends LanguageServer with Driver with Intelligence with TextDocumentService with WorkspaceService { + private var client: EffektLanguageClient = _ + private val textDocumentService = this + private val workspaceService = this - import effekt.symbols._ + // Track whether shutdown has been requested + private var shutdownRequested: Boolean = false + // Configuration sent by the language client + var settings: JsonElement = null - import org.eclipse.lsp4j.{ Location, Range => LSPRange } + val getDriver: Driver = this + val getConfig: EffektConfig = config - val name = "effekt" - - // Diagnostics object lspMessaging extends PlainMessaging - override def messageToDiagnostic(message: EffektError): Diagnostic = - diagnostic(message.range, lspMessaging.formatContent(message), message.severity) + // LSP Lifecycle + // + // + + override def initialize(params: InitializeParams): CompletableFuture[InitializeResult] = { + val capabilities = new ServerCapabilities() + capabilities.setTextDocumentSync(TextDocumentSyncKind.Full) + capabilities.setHoverProvider(true) + capabilities.setDefinitionProvider(true) + capabilities.setReferencesProvider(true) + capabilities.setDocumentSymbolProvider(true) + capabilities.setCodeActionProvider(true) + capabilities.setInlayHintProvider(true) + + // We need to explicitly ask the client to include the text on save events. + // Otherwise, when not listening to `didChange`, we have no way to get the text of the file, + // when the client decides not to include the text in the `didSave` event. + val saveOptions = new SaveOptions() + saveOptions.setIncludeText(true) + + val syncOptions = new TextDocumentSyncOptions(); + syncOptions.setOpenClose(true); + syncOptions.setChange(TextDocumentSyncKind.Full); + syncOptions.setSave(saveOptions); + capabilities.setTextDocumentSync(syncOptions); + + // Load the initial settings from client-sent `initializationOptions` (if any) + // This is not part of the LSP standard, but seems to be the most reliable way to have the correct initial settings + // on first compile. + // There is a `workspace/didChangeConfiguration` notification and a `workspace/configuration` request, but we cannot + // guarantee that the client will send these before the file is first compiled, leading to either duplicate work + // or a bad user experience. + if (params.getInitializationOptions != null) + workspaceService.didChangeConfiguration(new DidChangeConfigurationParams(params.getInitializationOptions)) + + val result = new InitializeResult(capabilities) + CompletableFuture.completedFuture(result) + } - override def getDefinition(position: Position): Option[Tree] = - getDefinitionAt(position)(using context) + override def shutdown(): CompletableFuture[Object] = { + shutdownRequested = true + CompletableFuture.completedFuture(null) + } - /** - * Overriding hook to also publish core and target for LSP server - */ - override def afterCompilation(source: Source, config: EffektConfig)(using C: Context): Unit = try { - super.afterCompilation(source, config) + override def exit(): Unit = { + System.exit(if (shutdownRequested) 0 else 1) + } - // don't do anything, if we aren't running as a language server - if (!C.config.server()) return ; + override def setTrace(params: SetTraceParams): Unit = { + // Do nothing + } - val showIR = settingStr("showIR") - val showTree = settingBool("showTree") + // The LSP services are also implemented by the Server class as they are strongly coupled anyway. + override def getTextDocumentService(): TextDocumentService = this + override def getWorkspaceService(): WorkspaceService = this - def publishTree(name: String, tree: Any): Unit = - publishProduct(source, name, "scala", util.PrettyPrinter.format(tree)) + // LSP Diagnostics + // + // - def publishIR(name: String, doc: Document): Unit = - publishProduct(source, name, "ir", doc) + def clearDiagnostics(name: String): Unit = { + publishDiagnostics(name, Vector()) + } + + def publishDiagnostics(name: String, diagnostics: Vector[Diagnostic]): Unit = { + val params = new PublishDiagnosticsParams(Convert.toURI(name), Collections.seqToJavaList(diagnostics)) + client.publishDiagnostics(params) + } - if (showIR == "none") { return; } + // Custom Effekt extensions + // + // + + /** + * Publish Effekt IR for a given source file + * + * @param source The Kiama source file + * @param config The Effekt configuration + * @param C The compiler context + */ + def publishIR(source: Source, config: EffektConfig)(implicit C: Context): Unit = { + // Publish Effekt IR + val showIR = workspaceService.settingString("showIR").getOrElse("none") + val showTree = workspaceService.settingBool("showTree") + + if (showIR == "none") { + return; + } if (showIR == "source") { - val tree = C.compiler.getAST(source).getOrElseAborting { return; } - publishTree("source", tree) + val tree = C.compiler.getAST(source) + if (tree.isEmpty) return + client.publishIR(EffektPublishIRParams( + basename(source.name) + ".scala", + PrettyPrinter.format(tree.get).layout + )) return; } @@ -69,18 +158,123 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with } if (showTree) { - publishTree(showIR, C.compiler.treeIR(source, stage).getOrElse(unsupported)) + client.publishIR(EffektPublishIRParams( + basename(source.name) + ".scala", + PrettyPrinter.format(C.compiler.treeIR(source, stage).getOrElse(unsupported)).layout + )) } else if (showIR == "target") { - publishProduct(source, "target", C.runner.extension, C.compiler.prettyIR(source, Stage.Target).getOrElse(unsupported)) + client.publishIR(EffektPublishIRParams( + basename(source.name) + "." + C.runner.extension, + C.compiler.prettyIR(source, Stage.Target).getOrElse(unsupported).layout + )) } else { - publishIR(showIR, C.compiler.prettyIR(source, stage).getOrElse(unsupported)) + client.publishIR(EffektPublishIRParams( + basename(source.name) + ".ir", + C.compiler.prettyIR(source, stage).getOrElse(unsupported).layout + )) + } + } + + // Driver methods + // + // + + override def afterCompilation(source: Source, config: EffektConfig)(implicit C: Context): Unit = { + // Publish LSP diagnostics + val messages = C.messaging.buffer + val groups = messages.groupBy(msg => msg.sourceName.getOrElse("")) + for ((name, msgs) <- groups) { + publishDiagnostics(name, msgs.distinct.map(Convert.messageToDiagnostic(lspMessaging))) + } + try { + publishIR(source, config) + } catch { + case e => client.logMessage(new MessageParams(MessageType.Error, e.toString + ":" + e.getMessage)) + } + } + + // Other methods + // + // + + def basename(filename: String): String = { + val name = Paths.get(filename).getFileName.toString + val dotIndex = name.lastIndexOf('.') + if (dotIndex > 0) name.substring(0, dotIndex) else name + } + + def connect(client: EffektLanguageClient): Unit = { + this.client = client + } + + /** + * Launch a language server using provided input/output streams. + * This allows tests to connect via in-memory pipes. + */ + def launch(client: EffektLanguageClient, in: InputStream, out: OutputStream): Launcher[EffektLanguageClient] = { + val executor = Executors.newSingleThreadExecutor() + val launcher = + new LSPLauncher.Builder() + .setLocalService(this) + .setRemoteInterface(classOf[EffektLanguageClient]) + .setInput(in) + .setOutput(out) + .setExecutorService(executor) + .create() + this.connect(client) + launcher.startListening() + launcher + } + + // LSP Document Lifecycle + // + // + + def didChange(params: DidChangeTextDocumentParams): Unit = { + if (!compileOnChange) return + val document = params.getTextDocument + clearDiagnostics(document.getUri) + getDriver.compileString(document.getUri, params.getContentChanges.get(0).getText, getConfig) + } + + def didClose(params: DidCloseTextDocumentParams): Unit = { + clearDiagnostics(params.getTextDocument.getUri) + } + + def didOpen(params: DidOpenTextDocumentParams): Unit = { + val document = params.getTextDocument + clearDiagnostics(document.getUri) + getDriver.compileString(document.getUri, document.getText, getConfig) + } + + def didSave(params: DidSaveTextDocumentParams): Unit = { + val document = params.getTextDocument + val text = Option(params.getText) match { + case Some(t) => t + case None => + return } - } catch { - case e => logMessage(e.toString + ":" + e.getMessage) + clearDiagnostics(document.getUri) + getDriver.compileString(document.getUri, text, getConfig) } - override def getHover(position: Position): Option[String] = - getSymbolHover(position) orElse getHoleHover(position) + // LSP Hover + // + // + + override def hover(params: HoverParams): CompletableFuture[Hover] = { + val position = sources.get(params.getTextDocument.getUri).map { source => + Convert.fromLSPPosition(params.getPosition, source) + } + position match + case Some(position) => { + val hover = getSymbolHover(position) orElse getHoleHover(position) + val markup = new MarkupContent("markdown", hover.getOrElse("")) + val result = new Hover(markup, new LSPRange(params.getPosition, params.getPosition)) + CompletableFuture.completedFuture(result) + } + case None => CompletableFuture.completedFuture(new Hover()) + } def getSymbolHover(position: Position): Option[String] = for { (tree, sym) <- getSymbolAt(position)(using context) @@ -93,56 +287,39 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with info <- getHoleInfo(tree)(using context) } yield info - def positionToLocation(p: Position): Location = { - val s = convertPosition(Some(p)) - new Location(p.source.name, new LSPRange(s, s)) - } + // LSP Document Symbols + // + // - def getSourceTreeFor(sym: Symbol): Option[Tree] = sym match { - case a: Anon => Some(a.decl) - case f: UserFunction => Some(f.decl) - case b: Binder => Some(b.decl) - case _ => context.definitionTreeOption(sym) - } + override def documentSymbol(params: DocumentSymbolParams): CompletableFuture[util.List[messages.Either[SymbolInformation, DocumentSymbol]]] = { + val source = sources.get(params.getTextDocument.getUri) + if (source.isEmpty) return CompletableFuture.completedFuture(Collections.seqToJavaList(Vector())) - override def getSymbols(source: Source): Option[Vector[DocumentSymbol]] = - - context.compiler.runFrontend(source)(using context) + context.compiler.runFrontend(source.get)(using context) val documentSymbols = for { - sym <- context.sourceSymbolsFor(source).toVector + sym <- context.sourceSymbolsFor(source.get).toVector if !sym.isSynthetic id <- context.definitionTreeOption(sym) decl <- getSourceTreeFor(sym) kind <- getSymbolKind(sym) detail <- getInfoOf(sym)(using context) - } yield new DocumentSymbol(sym.name.name, kind, rangeOfNode(decl), rangeOfNode(id), detail.header) - Some(documentSymbols) - - override def getReferences(position: Position, includeDecl: Boolean): Option[Vector[Tree]] = - for { - (tree, sym) <- getSymbolAt(position)(using context) - refs = context.distinctReferencesTo(sym) - allRefs = if (includeDecl) tree :: refs else refs - } yield allRefs.toVector - - override def getInlayHints(range: kiama.util.Range): Option[Vector[InlayHint]] = - val captures = getInferredCaptures(range)(using context).map { - case (p, c) => - val prettyCaptures = TypePrinter.show(c) - InlayHint(InlayHintKind.Type, p, prettyCaptures, markdownTooltip = s"captures: `${prettyCaptures}`", paddingRight = true, effektKind = "capture") - }.toVector - - if captures.isEmpty then None else Some(captures) - - // settings might be null - override def setSettings(settings: Object): Unit = { - import com.google.gson.JsonObject - if (settings == null) super.setSettings(new JsonObject()) - else super.setSettings(settings) + declRange = convertRange(positions.getStart(decl), positions.getFinish(decl)) + idRange = convertRange(positions.getStart(id), positions.getFinish(id)) + } yield new DocumentSymbol(sym.name.name, kind, declRange, idRange, detail.header) + + val result = Collections.seqToJavaList( + documentSymbols.map(sym => messages.Either.forRight[SymbolInformation, DocumentSymbol](sym)) + ) + CompletableFuture.completedFuture(result) } - //references + def getSourceTreeFor(sym: effekt.symbols.Symbol): Option[Tree] = sym match { + case a: Anon => Some(a.decl) + case f: UserFunction => Some(f.decl) + case b: Binder => Some(b.decl) + case _ => context.definitionTreeOption(sym) + } def getSymbolKind(sym: Symbol): Option[SymbolKind] = sym match { @@ -160,25 +337,121 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with None } - override def getCodeActions(position: Position): Option[Vector[TreeAction]] = - Some(for { - trees <- getTreesAt(position)(using context).toVector - actions <- trees.flatMap { t => action(t)(using context) } - } yield actions) + // LSP Go To Definition + // + // + + override def definition(params: DefinitionParams): CompletableFuture[messages.Either[util.List[_ <: Location], util.List[_ <: LocationLink]]] = { + val location = for { + position <- sources.get(params.getTextDocument.getUri).map { source => + fromLSPPosition(params.getPosition, source) + }; + definition <- getDefinitionAt(position)(using context); + location = locationOfNode(positions, definition) + } yield location + + val result = location.map(l => messages.Either.forLeft[util.List[_ <: Location], util.List[_ <: LocationLink]](Collections.seqToJavaList(List(l)))) + .getOrElse(messages.Either.forLeft(Collections.seqToJavaList(List()))) + + CompletableFuture.completedFuture(result) + } + + // LSP References + // + // + + override def references(params: ReferenceParams): CompletableFuture[util.List[_ <: Location]] = { + val position = sources.get(params.getTextDocument.getUri).map { source => + fromLSPPosition(params.getPosition, source) + } + if (position.isEmpty) + return CompletableFuture.completedFuture(Collections.seqToJavaList(Vector())) + + val locations = for { + (tree, sym) <- getSymbolAt(position.get)(using context) + refs = context.distinctReferencesTo(sym) + // getContext may be null! + includeDeclaration = Option(params.getContext).exists(_.isIncludeDeclaration) + allRefs = if (includeDeclaration) tree :: refs else refs + locations = allRefs.map(ref => locationOfNode(positions, ref)) + } yield locations + + CompletableFuture.completedFuture(Collections.seqToJavaList(locations.getOrElse(Seq[Location]()))) + } + + // LSP Inlay Hints + // + // + + override def inlayHint(params: InlayHintParams): CompletableFuture[util.List[InlayHint]] = { + val hints = for { + source <- sources.get(params.getTextDocument.getUri) + hints = { + val range = fromLSPRange(params.getRange, source) + getInferredCaptures(range)(using context).map { + case (p, c) => + val prettyCaptures = TypePrinter.show(c) + val inlayHint = new InlayHint(convertPosition(p), messages.Either.forLeft(prettyCaptures)) + inlayHint.setKind(InlayHintKind.Type) + val markup = new MarkupContent() + markup.setValue(s"captures: `${prettyCaptures}`") + markup.setKind("markdown") + inlayHint.setTooltip(markup) + inlayHint.setPaddingRight(true) + inlayHint.setData("capture") + inlayHint + }.toVector + } + } yield hints + + CompletableFuture.completedFuture(Collections.seqToJavaList(hints.getOrElse(Vector()))) + } - def action(tree: Tree)(using C: Context): Option[TreeAction] = tree match { + // LSP Code Actions + // + // + + // FIXME: This is the code actions code from the previous language server implementation. + // It doesn't even work in the previous implementation. + override def codeAction(params: CodeActionParams): CompletableFuture[util.List[messages.Either[Command, CodeAction]]] = { + val codeActions = for { + position <- sources.get(params.getTextDocument.getUri).map { source => + fromLSPPosition(params.getRange.getStart, source) + }; + codeActions = for { + trees <- getTreesAt(position)(using context).toVector + actions <- trees.flatMap { t => action(t)(using context) } + } yield actions + } yield codeActions.toList + + val result = codeActions.getOrElse(List[CodeAction]()).map(messages.Either.forRight[Command, CodeAction]) + CompletableFuture.completedFuture(Collections.seqToJavaList(result)) + } + + def action(tree: Tree)(using C: Context): Option[CodeAction] = tree match { case f: FunDef => inferEffectsAction(f) - case h: Hole => closeHoleAction(h) - case _ => None + case h: Hole => closeHoleAction(h) + case _ => None } - def CodeAction(description: String, oldNode: Any, newText: String): Option[TreeAction] = + def EffektCodeAction(description: String, oldNode: Any, newText: String): Option[CodeAction] = { for { posFrom <- positions.getStart(oldNode) posTo <- positions.getFinish(oldNode) - } yield TreeAction(description, posFrom.source.name, posFrom, posTo, newText) + } yield { + val textEdit = new TextEdit(convertRange(Some(posFrom), Some(posTo)), newText) + val changes = Map(posFrom.source.name -> seqToJavaList(List(textEdit))) + val workspaceEdit = new WorkspaceEdit(mapToJavaMap(changes)) + val action = new CodeAction(description) + action.setKind(CodeActionKind.Refactor) + action.setEdit(workspaceEdit) + action + } + } /** + * FIXME: The following comment was left on the previous Kiama-based implementation and can now be addressed: + * * TODO it would be great, if Kiama would allow setting the position of the code action separately * from the node to replace. Here, we replace the annotated return type, but would need the * action on the function (since the return type might not exist in the original program). @@ -186,7 +459,7 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with * Also, it is necessary to be able to manually set the code action kind (and register them on startup). * This way, we can use custom kinds like `refactor.closehole` that can be mapped to keys. */ - def inferEffectsAction(fun: FunDef)(using C: Context): Option[TreeAction] = for { + def inferEffectsAction(fun: FunDef)(using C: Context): Option[CodeAction] = for { // the inferred type (tpe, eff) <- C.inferredTypeAndEffectOption(fun) // the annotated type @@ -194,24 +467,26 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with result <- fun.symbol.annotatedResult effects <- fun.symbol.annotatedEffects } yield (result, effects) - if ann.map { needsUpdate(_, (tpe, eff)) }.getOrElse(true) - res <- CodeAction("Update return type with inferred effects", fun.ret, s": $tpe / $eff") + if ann.map { + needsUpdate(_, (tpe, eff)) + }.getOrElse(true) + res <- EffektCodeAction("Update return type with inferred effects", fun.ret, s": $tpe / $eff") } yield res - def closeHoleAction(hole: Hole)(using C: Context): Option[TreeAction] = for { + def closeHoleAction(hole: Hole)(using C: Context): Option[CodeAction] = for { holeTpe <- C.inferredTypeOption(hole) contentTpe <- C.inferredTypeOption(hole.stmts) if holeTpe == contentTpe res <- hole match { case Hole(source.Return(exp)) => for { text <- positions.textOf(exp) - res <- CodeAction("Close hole", hole, text) + res <- EffektCodeAction("Close hole", hole, text) } yield res // <{ s1 ; s2; ... }> case Hole(stmts) => for { text <- positions.textOf(stmts) - res <- CodeAction("Close hole", hole, s"locally { ${text} }") + res <- EffektCodeAction("Close hole", hole, s"locally { ${text} }") } yield res } } yield res @@ -222,13 +497,97 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with tpe1 != tpe2 || effs1 != effs2 } - override def createServices(config: EffektConfig) = new LSPServices(this, config) + // LSP methods + // + // + + def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = { + this.settings = params.getSettings.asInstanceOf[JsonElement].getAsJsonObject + } + + def didChangeWatchedFiles(params: DidChangeWatchedFilesParams): Unit = {} + + // Settings + // + // + + def settingBool(name: String): Boolean = { + if (settings == null) return false + val obj = settings.getAsJsonObject + if (obj == null) return false + val value = obj.get(name) + if (value == null) return false + value.getAsBoolean + } + + def settingString(name: String): Option[String] = { + if (settings == null) return None + val obj = settings.getAsJsonObject + if (obj == null) return None + val value = obj.get(name) + if (value == null) return None + Some(value.getAsString) + } + + /** + * Launch a language server with a given `ServerConfig` + */ + def launch(config: ServerConfig): Unit = { + // Create a single-threaded executor to serialize all requests. + val executor: ExecutorService = Executors.newSingleThreadExecutor() + + if (config.debug) { + val serverSocket = new ServerSocket(config.debugPort) + System.err.println(s"Starting language server in debug mode on port ${config.debugPort}") + val socket = serverSocket.accept() + + val launcher = + new LSPLauncher.Builder() + .setLocalService(this) + .setRemoteInterface(classOf[EffektLanguageClient]) + .setInput(socket.getInputStream) + .setOutput(socket.getOutputStream) + .setExecutorService(executor) + .traceMessages(new PrintWriter(System.err, true)) + .create() + val client = launcher.getRemoteProxy + this.connect(client) + launcher.startListening() + } else { + val launcher = + new LSPLauncher.Builder() + .setLocalService(this) + .setRemoteInterface(classOf[EffektLanguageClient]) + .setInput(System.in) + .setOutput(System.out) + .setExecutorService(executor) + .create() + + val client = launcher.getRemoteProxy + this.connect(client) + launcher.startListening() + } + } +} + +case class ServerConfig(debug: Boolean = false, debugPort: Int = 5000) - // Class to easily test custom LSP services not (yet) meant to go into kiama.Services - class LSPServices(server: LSPServer, config: EffektConfig) extends Services[Tree, EffektConfig, EffektError](server, config) {} +trait EffektLanguageClient extends LanguageClient { + /** + * Custom LSP notification to publish Effekt IR + * + * @param params The parameters for the notification + */ + @JsonNotification("$/effekt/publishIR") + def publishIR(params: EffektPublishIRParams): Unit } /** - * Singleton for the language server + * Custom LSP notification to publish Effekt IR + * + * @param filename The filename of the resulting IR file + * @param content The IR content */ -object Server extends LSPServer +case class EffektPublishIRParams(filename: String, + content: String +) diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala new file mode 100644 index 000000000..d463d15a2 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala @@ -0,0 +1,692 @@ +package effekt + +import com.google.gson.{JsonElement, JsonParser} +import munit.FunSuite +import org.eclipse.lsp4j.{DefinitionParams, Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InitializeParams, InitializeResult, InlayHint, InlayHintKind, InlayHintParams, MarkupContent, MessageActionItem, MessageParams, Position, PublishDiagnosticsParams, Range, ReferenceContext, ReferenceParams, SaveOptions, ServerCapabilities, SetTraceParams, ShowMessageRequestParams, SymbolInformation, SymbolKind, TextDocumentContentChangeEvent, TextDocumentItem, TextDocumentSyncKind, TextDocumentSyncOptions, VersionedTextDocumentIdentifier} +import org.eclipse.lsp4j.jsonrpc.messages + +import java.io.{PipedInputStream, PipedOutputStream} +import java.util +import java.util.concurrent.CompletableFuture +import scala.collection.mutable +import scala.collection.mutable.Queue +import scala.jdk.CollectionConverters.* + +class LSPTests extends FunSuite { + // Import the extension method for String + import TextDocumentSyntax.* + + // Test helpers + // + // + + def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = { + val driver = new Driver {} + val config = EffektConfig(Seq("--server")) + config.verify() + + val clientIn = new PipedInputStream() + val clientOut = new PipedOutputStream() + val serverIn = new PipedInputStream(clientOut) + val serverOut = new PipedOutputStream(clientIn) + + // The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing + // because we would like to switch to `didChange` events once we have working caching for references. + val server = new Server(config, compileOnChange = true) + + val mockClient = new MockLanguageClient() + server.connect(mockClient) + + val launcher = server.launch(mockClient, serverIn, serverOut) + + testBlock(mockClient, server) + } + + // Fixtures + // + // + + val helloWorld = raw""" + |def main() = { println("Hello, world!") } + |""".textDocument + + val helloEffekt = raw""" + |def main() = { println("Hello, Effekt!") } + |""" + + // LSP: lifecycle events + // + // + + test("Initialization works") { + withClientAndServer { (client, server) => + val initializeResult = server.initialize(new InitializeParams()).get() + val expectedCapabilities = new ServerCapabilities() + expectedCapabilities.setHoverProvider(true) + expectedCapabilities.setDefinitionProvider(true) + expectedCapabilities.setReferencesProvider(true) + expectedCapabilities.setDocumentSymbolProvider(true) + expectedCapabilities.setCodeActionProvider(true) + expectedCapabilities.setInlayHintProvider(true) + + val saveOptions = new SaveOptions() + saveOptions.setIncludeText(true) + + val syncOptions = new TextDocumentSyncOptions(); + syncOptions.setOpenClose(true); + syncOptions.setChange(TextDocumentSyncKind.Full); + syncOptions.setSave(saveOptions); + expectedCapabilities.setTextDocumentSync(syncOptions); + + assertEquals(initializeResult, new InitializeResult(expectedCapabilities)) + } + } + + test("didOpen yields empty diagnostics") { + withClientAndServer { (client, server) => + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + + val diagnostics = client.receivedDiagnostics() + assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(helloWorld.getUri, new util.ArrayList()))) + } + } + + test("setTrace is implemented") { + withClientAndServer { (client, server) => + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = SetTraceParams("off") + server.setTrace(params) + } + } + + // LSP: Changes to text documents + // + // + + test("didOpen yields error diagnostics") { + withClientAndServer { (client, server) => + val (textDoc, range) = raw""" + |val x: Int = "String" + | ↑ ↑ + |""".textDocumentAndRange + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val diagnostic = new Diagnostic() + diagnostic.setRange(range) + diagnostic.setSeverity(DiagnosticSeverity.Error) + diagnostic.setSource("effekt") + diagnostic.setMessage("Expected Int but got String.") + + val diagnosticsWithError = new util.ArrayList[Diagnostic]() + diagnosticsWithError.add(diagnostic) + + val expected = List( + new PublishDiagnosticsParams("file://test.effekt", new util.ArrayList[Diagnostic]()), + new PublishDiagnosticsParams("file://test.effekt", diagnosticsWithError) + ) + + val diagnostics = client.receivedDiagnostics() + assertEquals(diagnostics, expected) + } + } + + test("didChange yields empty diagnostics") { + withClientAndServer { (client, server) => + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + // Pop the diagnostics from the queue before changing the document + val _ = client.receivedDiagnostics() + + val (textDoc, changeEvent) = helloWorld.changeTo(helloEffekt) + + val didChangeParams = new DidChangeTextDocumentParams() + didChangeParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) + didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) + server.getTextDocumentService().didChange(didChangeParams) + + val diagnostics = client.receivedDiagnostics() + assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) + } + } + + test("didSave yields empty diagnostics") { + withClientAndServer { (client, server) => + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + // Pop the diagnostics from the queue before changing the document + val _ = client.receivedDiagnostics() + + val (textDoc, changeEvent) = helloWorld.changeTo(helloEffekt) + + val didSaveParams = new DidSaveTextDocumentParams() + didSaveParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) + didSaveParams.setText(textDoc.getText) + server.getTextDocumentService().didSave(didSaveParams) + + val diagnostics = client.receivedDiagnostics() + assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) + } + } + + + test("didClose yields empty diagnostics") { + withClientAndServer { (client, server) => + // We use an erroneous example to show that closing the document clears the diagnostics. + val textDoc = raw"""val x: Int = "String"""".textDocument + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + // Pop the diagnostics from the queue before closing the document + val _ = client.receivedDiagnostics() + + val didCloseParams = new DidCloseTextDocumentParams() + didCloseParams.setTextDocument(textDoc.versionedTextDocumentIdentifier) + server.getTextDocumentService().didClose(didCloseParams) + + val diagnostics = client.receivedDiagnostics() + assertEquals(diagnostics, Seq(new PublishDiagnosticsParams(textDoc.getUri, new util.ArrayList()))) + } + } + + test("didSave doesn't throw a NullPointerException when text is null") { + withClientAndServer { (client, server) => + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + // Clear any initial diagnostics. + val _ = client.receivedDiagnostics() + + val didSaveParams = new DidSaveTextDocumentParams() + didSaveParams.setTextDocument(helloWorld.versionedTextDocumentIdentifier) + // The text is set to null + didSaveParams.setText(null) + + server.getTextDocumentService().didSave(didSaveParams) + } + } + + // LSP: Hovering + // + // + + test("Hovering over symbol shows type information") { + withClientAndServer { (client, server) => + val (textDoc, cursor) = raw""" + |val x: Int = 42 + | ↑ + |""".textDocumentAndPosition + val hoverContents = + raw"""|#### Value binder + |```effekt + |test::x: Int + |``` + |""".stripMargin + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) + val hover = server.getTextDocumentService().hover(hoverParams).get() + + val expectedHover = new Hover() + expectedHover.setRange(new Range(cursor, cursor)) + expectedHover.setContents(new MarkupContent("markdown", hoverContents)) + assertEquals(hover, expectedHover) + } + } + + // FIXME: Hovering over holes does not work at the moment. + // https://github.com/effekt-lang/effekt/issues/549 + test("Hovering over hole shows nothing") { + withClientAndServer { (client, server) => + val (textDoc, cursor) = raw""" + |def foo(x: Int) = <> + | ↑ + |""".textDocumentAndPosition + val hoverContents = "" + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) + val hover = server.getTextDocumentService().hover(hoverParams).get() + + val expectedHover = new Hover() + expectedHover.setRange(new Range(cursor, cursor)) + expectedHover.setContents(new MarkupContent("markdown", hoverContents)) + assertEquals(hover, expectedHover) + } + } + + test("Hovering over mutable binder without extended description") { + withClientAndServer { (client, server) => + val (textDoc, cursor) = raw""" + |def main() = { + | var foo = 1 + | ↑ + | <> + |} + |""".textDocumentAndPosition + val hoverContents = + raw"""#### Mutable variable binder + |```effekt + |foo: Int + |``` + |""".stripMargin + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) + val hover = server.getTextDocumentService().hover(hoverParams).get() + + val expectedHover = new Hover() + expectedHover.setRange(new Range(cursor, cursor)) + expectedHover.setContents(new MarkupContent("markdown", hoverContents)) + assertEquals(hover, expectedHover) + } + } + + test("Hovering over mutable binder with extended description") { + withClientAndServer { (client, server) => + val (textDoc, cursor) = raw""" + |def main() = { + | var foo = 1 + | ↑ + | <> + |} + |""".textDocumentAndPosition + val hoverContents = + raw"""#### Mutable variable binder + |```effekt + |foo: Int + |``` + |Like in other languages, mutable variable binders like `foo` + |can be modified (e.g., `foo = VALUE`) by code that has `foo` + |in its lexical scope. + | + |However, as opposed to other languages, variable binders in Effekt + |are stack allocated and show the right backtracking behavior in + |combination with effect handlers. + |""".stripMargin + " \n" + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val configParams = new DidChangeConfigurationParams() + val settings: JsonElement = JsonParser.parseString("""{"showExplanations": true}""") + configParams.setSettings(settings) + server.getWorkspaceService().didChangeConfiguration(configParams) + + val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor) + val hover = server.getTextDocumentService().hover(hoverParams).get() + + val expectedHover = new Hover() + expectedHover.setRange(new Range(cursor, cursor)) + expectedHover.setContents(new MarkupContent("markdown", hoverContents)) + assertEquals(hover, expectedHover) + } + } + + // LSP: Document symbols + // + // + + test("documentSymbols returns expected symbols") { + withClientAndServer { (client, server) => + val (textDoc, positions) = + raw""" + |def mySymbol() = <> + |↑ ↑ ↑ ↑ + |""".textDocumentAndPositions + + val expectedSymbols: List[messages.Either[SymbolInformation, DocumentSymbol]] = List( + messages.Either.forRight(new DocumentSymbol( + "mySymbol", + SymbolKind.Method, + new Range(positions(0), positions(3)), + new Range(positions(1), positions(2)), + "Function", + )) + ) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new DocumentSymbolParams() + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + + val documentSymbols = server.getTextDocumentService().documentSymbol(params).get() + // FIXME: The server currently returns spurious symbols at position (0, 0) that we need to filter out. + val filtered = server.getTextDocumentService().documentSymbol(params).get().asScala.filter { + symbol => symbol.getRight.getRange.getStart != new Position(0, 0) && symbol.getRight.getRange.getEnd != new Position(0, 0) + }.asJava + + assertEquals(filtered, expectedSymbols.asJava) + } + } + + // LSP Go to definition + // + // + + test("definition returns expected range") { + withClientAndServer { (client, server) => + val (textDoc, positions) = + raw""" + |def foo() = <> + |↑ ↑ + |def bar() = foo() + | ↑ + """.textDocumentAndPositions + + val expectedRange = new Range(positions(0), positions(1)) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new DefinitionParams() + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + params.setPosition(positions(2)) + + val definition = server.getTextDocumentService().definition(params).get().getLeft.get(0) + assertEquals(definition.getRange, expectedRange) + } + } + + // LSP References + // + // + + // FIXME: the server doesn't actually return the reference to `foo` in `bar` in this example + // It only returns the declaration site. + test("references with setIncludeDeclaration returns declaration site") { + withClientAndServer { (client, server) => + val (textDoc, positions) = + raw""" + |def foo() = <> + | ↑ ↑ + |def bar() = foo() + """.textDocumentAndPositions + + val expectedReferences: List[Range] = List( + new Range(positions(0), positions(1)), + ) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new ReferenceParams() + params.setPosition(positions(0)) + val context = new ReferenceContext() + context.setIncludeDeclaration(true) + params.setContext(context) + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + + val references = server.getTextDocumentService().references(params).get() + assertEquals(references.asScala.map(_.getRange).toList, expectedReferences) + } + } + + // LSP: Inlay hints + // + // + + test("inlayHints should show the io effect") { + withClientAndServer { (client, server) => + val (textDoc, positions) = raw""" + |↑ + |def main() = { + |↑ + | println("Hello, world!") + |} + |↑ + |""".textDocumentAndPositions + + val inlayHint = new InlayHint() + inlayHint.setKind(InlayHintKind.Type) + inlayHint.setPosition(positions(1)) + inlayHint.setLabel("{io}") + val markup = new MarkupContent() + markup.setKind("markdown") + markup.setValue("captures: `{io}`") + inlayHint.setTooltip(markup) + inlayHint.setPaddingRight(true) + inlayHint.setData("capture") + + val expectedInlayHints = List(inlayHint) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new InlayHintParams() + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + params.setRange(new Range(positions(0), positions(2))) + + val inlayHints = server.getTextDocumentService().inlayHint(params).get() + assertEquals(inlayHints, expectedInlayHints.asJava) + } + } + + // Effekt: Publish IR + // + // + + test("When showIR=source, server should provide source AST") { + withClientAndServer { (client, server) => + val source = + raw""" + |def main() = { println("Hello, world!") } + |""" + val textDoc = new TextDocumentItem("file://path/to/test.effekt", "effekt", 0, source.stripMargin) + val initializeParams = new InitializeParams() + val initializationOptions = """{"showIR": "source"}""" + initializeParams.setInitializationOptions(JsonParser.parseString(initializationOptions)) + server.initialize(initializeParams).get() + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(helloWorld) + server.getTextDocumentService().didOpen(didOpenParams) + + val expectedIRContents = + raw"""ModuleDecl( + | test, + | Nil, + | List( + | FunDef( + | IdDef(main), + | Nil, + | Nil, + | Nil, + | None(), + | BlockStmt( + | Return( + | Call( + | IdTarget(IdRef(Nil, println)), + | Nil, + | List(Literal(Hello, world!, ValueTypeApp(String_whatever, Nil))), + | Nil + | ) + | ) + | ) + | ) + | ) + |)""".stripMargin + + val receivedIRContent = client.receivedIR() + assertEquals(receivedIRContent.length, 1) + val fixedReceivedIR = receivedIRContent.head.content.replaceAll("String_\\d+", "String_whatever") + assertEquals(fixedReceivedIR, expectedIRContents) + } + } + + // Text document DSL + // + // + + test("Correct cursor position") { + val (textDoc, cursor) = raw""" + |def main() = { println("Hello, world!") } + | ↑ + |""".textDocumentAndPosition + + assertEquals(cursor, new org.eclipse.lsp4j.Position(1, 4)) + } + + test("Missing cursor throws exception") { + intercept[IllegalArgumentException] { + raw""" + |def main() = { println("Hello, world!") } + |""".textDocumentAndPosition + } + } + + test("Correct multiline range") { + val (textDoc, range) = raw""" + | There is some content here. + | ↑ + | And here. + | ↑ + |""".textDocumentAndRange + + val textWithoutRanges = raw""" + | There is some content here. + | And here.""".stripMargin + + assertEquals(range.getStart, new org.eclipse.lsp4j.Position(1, 5)) + assertEquals(range.getEnd, new org.eclipse.lsp4j.Position(2, 6)) + assertEquals(textDoc.getText, textWithoutRanges) + } +} + +class MockLanguageClient extends EffektLanguageClient { + private val diagnosticQueue: mutable.Queue[PublishDiagnosticsParams] = mutable.Queue.empty + private val publishIRQueue: mutable.Queue[EffektPublishIRParams] = mutable.Queue.empty + + /** + * Pops all diagnostics received since the last call to this method. + */ + def receivedDiagnostics(): Seq[PublishDiagnosticsParams] = { + val diagnostics = diagnosticQueue.toSeq + diagnosticQueue.clear() + diagnostics + } + + /** + * Pops all publishIR events received since the last call to this method. + */ + def receivedIR(): Seq[EffektPublishIRParams] = { + val irs = publishIRQueue.toSeq + publishIRQueue.clear() + irs + } + + override def telemetryEvent(`object`: Any): Unit = { + // Not implemented for testing. + } + + override def publishDiagnostics(diagnostics: PublishDiagnosticsParams): Unit = { + diagnosticQueue.enqueue(diagnostics) + } + + override def showMessage(messageParams: MessageParams): Unit = { + // Not implemented for testing. + } + + override def showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture[MessageActionItem] = { + // Not implemented for testing. + CompletableFuture.completedFuture(null) + } + + override def logMessage(message: MessageParams): Unit = { + // Not implemented for testing. + } + + override def publishIR(params: EffektPublishIRParams): Unit = { + publishIRQueue.enqueue(params) + } +} + +// DSL for creating text documents using extension methods for String +object TextDocumentSyntax { + implicit class StringOps(val content: String) extends AnyVal { + def textDocument(version: Int): TextDocumentItem = + new TextDocumentItem("file://test.effekt", "effekt", version, content.stripMargin) + + def textDocument: TextDocumentItem = + new TextDocumentItem("file://test.effekt", "effekt", 0, content.stripMargin) + + def textDocumentAndPosition: (TextDocumentItem, Position) = { + val (textDocument, positions) = content.textDocumentAndPositions + + if (positions.length != 1) + throw new IllegalArgumentException("Exactly one marker line (with '" + "↑" + "') is required.") + + (textDocument, positions.head) + } + + def textDocumentAndRange: (TextDocumentItem, Range) = { + val (textDocument, positions) = content.textDocumentAndPositions + if (positions.length != 2) + throw new IllegalArgumentException("Exactly two marker lines (with '" + "↑" + "') are required.") + val start = positions(0) + val end = positions(1) + // The end of the range is exclusive, so we need to increment the character position. + val range = new Range(start, new Position(end.getLine, end.getCharacter + 1)) + (textDocument, range) + } + + def textDocumentAndPositions: (TextDocumentItem, Seq[Position]) = { + val lines = content.stripMargin.split("\n").toBuffer + val positions = scala.collection.mutable.ArrayBuffer[Position]() + var lineIdx = 0 + while (lineIdx < lines.length) { + val line = lines(lineIdx) + if (line.contains("↑")) { + if (lineIdx == 0) + throw new IllegalArgumentException("Marker on first line cannot refer to a previous line.") + // There may be multiple markers on the same line, so we need to record all of them. + for (i <- line.indices if line(i) == '↑') { + positions += new Position(lineIdx - 1, i) + } + lines.remove(lineIdx) + // adjust index because of removal + lineIdx -= 1 + } + lineIdx += 1 + } + val newContent = lines.mkString("\n") + (newContent.textDocument, positions.toList) + } + } + + implicit class TextDocumentOps(val textDocument: TextDocumentItem) extends AnyVal { + def changeTo(newContent: String): (TextDocumentItem, TextDocumentContentChangeEvent) = { + val newDoc = new TextDocumentItem(textDocument.getUri, textDocument.getLanguageId, textDocument.getVersion + 1, newContent.stripMargin) + val changeEvent = new TextDocumentContentChangeEvent(newDoc.getText) + (newDoc, changeEvent) + } + + def versionedTextDocumentIdentifier: VersionedTextDocumentIdentifier = + new VersionedTextDocumentIdentifier(textDocument.getUri, textDocument.getVersion) + } +} diff --git a/kiama b/kiama index 3abc1e974..f4379b140 160000 --- a/kiama +++ b/kiama @@ -1 +1 @@ -Subproject commit 3abc1e97416b867449aaf7ed381a725f4832b1fa +Subproject commit f4379b1402374efadd188a050c37da1fcdb325ec From d2bdb9f0420974fb195a952f06caaa5c108211ca Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:56:23 +0100 Subject: [PATCH 08/41] Fix: make type pretty printer less confusing for boxed types (#906) Fixes #659, but not #667. I propose deferring fixing #667 to a later point in time, as it requires more design. What this PR does, for now, is to parenthesize boxed types when occurring in parameter or return type positions. For example, `(Int => Int at {}) => Int` instead of `Int => Int at {} => Int`, which, in my opinion, is utterly unreadable. --- .../main/scala/effekt/symbols/TypePrinter.scala | 6 +++++- examples/neg/lambdas/inference.check | 16 ++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala b/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala index 627df9997..96a14bfa3 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/TypePrinter.scala @@ -66,13 +66,17 @@ object TypePrinter extends ParenPrettyPrinter { val tps = if (tparams.isEmpty) emptyDoc else typeParams(tparams) val ps: Doc = (vparams, bparams) match { case (Nil, Nil) => "()" + case (List(tpe: BoxedType), Nil) => parens(toDoc(tpe)) case (List(tpe), Nil) => if (tparams.isEmpty) toDoc(tpe) else parens(toDoc(tpe)) case (_, _) => val vps = if (vparams.isEmpty) emptyDoc else parens(hsep(vparams.map(toDoc), comma)) val bps = if (bparams.isEmpty) emptyDoc else hcat(bparams.map(toDoc).map(braces)) vps <> bps } - val ret = toDoc(result) + val ret = result match { + case _: BoxedType => parens(toDoc(result)) + case _ => toDoc(result) + } val eff = if (effects.isEmpty) emptyDoc else space <> "/" <+> toDoc(effects) tps <> ps <+> "=>" <+> ret <> eff diff --git a/examples/neg/lambdas/inference.check b/examples/neg/lambdas/inference.check index e0186fc8a..8c2650d04 100644 --- a/examples/neg/lambdas/inference.check +++ b/examples/neg/lambdas/inference.check @@ -1,22 +1,22 @@ [error] Expected type - Int => Bool at {} => String + (Int => Bool at {}) => String but got type - Int => Unit at {} => String + (Int => Unit at {}) => String Type mismatch between Bool and Unit. comparing the argument types of - Int => Unit at {} => String (given) - Int => Bool at {} => String (expected) + (Int => Unit at {}) => String (given) + (Int => Bool at {}) => String (expected) when comparing the return type of the function. [error] examples/neg/lambdas/inference.effekt:13:8: Expected type - Int => Bool at {} => String at {} + (Int => Bool at {}) => String at {} but got type - Int => Unit at {} => String at ?C + (Int => Unit at {}) => String at ?C Type mismatch between Bool and Unit. comparing the argument types of - Int => Unit at {} => String (given) - Int => Bool at {} => String (expected) + (Int => Unit at {}) => String (given) + (Int => Bool at {}) => String (expected) when comparing the return type of the function. hof2(fun(f: (Int) => Unit at {}) { "" }) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 3773ff220c05c1f0f15b4b24c791805f812e6e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 28 Mar 2025 15:12:31 +0100 Subject: [PATCH 09/41] Add test for cyclic imports (#903) Fixes #760 by giving a more user-friendly error message like the following: Screenshot 2025-03-28 at 12 02 58 ## How it achieves this In a dynamic variable, it keeps a shadow-stack of what `Namer` is currently processing, checking on insertion. This check is moved into the processing of imports so the positioning information is correct. --- .../shared/src/main/scala/effekt/Namer.scala | 23 ++++++++++++++++++- examples/neg/cyclic_a.check | 10 ++++++++ examples/neg/cyclic_a.effekt | 5 ++++ examples/neg/cyclic_b.effekt | 5 ++++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 examples/neg/cyclic_a.check create mode 100644 examples/neg/cyclic_a.effekt create mode 100644 examples/neg/cyclic_b.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 4aabfe03b..38e9c2835 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -13,6 +13,8 @@ import effekt.util.messages.ErrorMessageReifier import effekt.symbols.scopes.* import effekt.source.FeatureFlag.supportedByFeatureFlags +import scala.util.DynamicVariable + /** * The output of this phase: a mapping from source identifier to symbol * @@ -39,6 +41,24 @@ object Namer extends Phase[Parsed, NameResolved] { Some(NameResolved(source, tree, mod)) } + /** Shadow stack of modules currently named, for detecction of cyclic imports */ + private val currentlyNaming: DynamicVariable[List[ModuleDecl]] = DynamicVariable(List()) + /** + * Run body in a context where we are currently naming `mod`. + * Produces a cyclic import error when this is already the case + */ + private def recursiveProtect[R](mod: ModuleDecl)(body: => R)(using Context): R = { + if (currentlyNaming.value.contains(mod)) { + val cycle = mod :: currentlyNaming.value.takeWhile(_ != mod).reverse + Context.abort( + pretty"""Cyclic import: ${mod.path} depends on itself, via:\n\t${cycle.map(_.path).mkString(" -> ")} -> ${mod.path}""") + } else { + currentlyNaming.withValue(mod :: currentlyNaming.value) { + body + } + } + } + def resolve(mod: Module)(using Context): ModuleDecl = { val Module(decl, src) = mod val scope = scopes.toplevel(Context.module.namespace, builtins.rootBindings) @@ -72,7 +92,8 @@ object Namer extends Phase[Parsed, NameResolved] { // process all includes, updating the terms and types in scope val includes = decl.includes collect { case im @ source.Include(path) => - val mod = Context.at(im) { importDependency(path) } + // [[recursiveProtect]] is called here so the source position is the recursive import + val mod = Context.at(im) { recursiveProtect(decl){ importDependency(path) } } Context.annotate(Annotations.IncludedSymbols, im, mod) mod } diff --git a/examples/neg/cyclic_a.check b/examples/neg/cyclic_a.check new file mode 100644 index 000000000..c3839982d --- /dev/null +++ b/examples/neg/cyclic_a.check @@ -0,0 +1,10 @@ +[error] ./examples/neg/cyclic_a.effekt:3:1: Cyclic import: cyclic_a depends on itself, via: + cyclic_a -> cyclic_b -> cyclic_a +import examples/neg/cyclic_b +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +[error] ./examples/neg/cyclic_a.effekt:3:1: Cannot compile dependency: ./examples/neg/cyclic_a +import examples/neg/cyclic_b +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +[error] ./examples/neg/cyclic_a.effekt:3:1: Cannot compile dependency: ./examples/neg/cyclic_b +import examples/neg/cyclic_b +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/neg/cyclic_a.effekt b/examples/neg/cyclic_a.effekt new file mode 100644 index 000000000..89158d000 --- /dev/null +++ b/examples/neg/cyclic_a.effekt @@ -0,0 +1,5 @@ +module cyclic_a + +import examples/neg/cyclic_b + +def main() = () diff --git a/examples/neg/cyclic_b.effekt b/examples/neg/cyclic_b.effekt new file mode 100644 index 000000000..0d6287d6e --- /dev/null +++ b/examples/neg/cyclic_b.effekt @@ -0,0 +1,5 @@ +module cyclic_b + +import examples/neg/cyclic_a + +def main() = () From ee78f8496e22cb182194897698f3882977111eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattis=20B=C3=B6ckle?= Date: Fri, 28 Mar 2025 15:58:20 +0100 Subject: [PATCH 10/41] Bump to llvm version 18 in CI (#887) We are running into problems where the versions used for development and for the CI are differing so much that we sometimes write code in development that is invalid in the CI. (See #877) I propose upping the Version to 18, which seems to be the Ubuntu standard Edit: the latest release is 20.1.0, so we are also not in unstable territory --- .github/actions/setup-effekt/action.yml | 2 +- effekt/jvm/src/main/scala/effekt/EffektConfig.scala | 4 ++-- effekt/jvm/src/main/scala/effekt/Runner.scala | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-effekt/action.yml b/.github/actions/setup-effekt/action.yml index 5a0dfee26..2730c730b 100644 --- a/.github/actions/setup-effekt/action.yml +++ b/.github/actions/setup-effekt/action.yml @@ -13,7 +13,7 @@ inputs: llvm-version: description: 'LLVM version to install' required: false - default: '15' + default: '18' install-dependencies: description: 'Whether to install system dependencies (Linux only)' required: false diff --git a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala index 9fba7dd6d..4f68a3547 100644 --- a/effekt/jvm/src/main/scala/effekt/EffektConfig.scala +++ b/effekt/jvm/src/main/scala/effekt/EffektConfig.scala @@ -68,8 +68,8 @@ class EffektConfig(args: Seq[String]) extends REPLConfig(args.takeWhile(_ != "-- val llvmVersion: ScallopOption[String] = opt[String]( "llvm-version", - descr = "the llvm version that should be used to compile the generated programs (only necessary if backend is llvm, defaults to 15)", - default = Some(sys.env.getOrElse("EFFEKT_LLVM_VERSION", "15")), + descr = "the llvm version that should be used to compile the generated programs (only necessary if backend is llvm, defaults to 18)", + default = Some(sys.env.getOrElse("EFFEKT_LLVM_VERSION", "18")), noshort = true, group = advanced ) diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala index 33afd4d0a..994214b48 100644 --- a/effekt/jvm/src/main/scala/effekt/Runner.scala +++ b/effekt/jvm/src/main/scala/effekt/Runner.scala @@ -267,8 +267,8 @@ object LLVMRunner extends Runner[String] { override def includes(path: File): List[File] = List(path / ".." / "llvm") lazy val gccCmd = discoverExecutable(List("cc", "clang", "gcc"), List("--version")) - lazy val llcCmd = discoverExecutable(List("llc", "llc-15", "llc-16"), List("--version")) - lazy val optCmd = discoverExecutable(List("opt", "opt-15", "opt-16"), List("--version")) + lazy val llcCmd = discoverExecutable(List("llc", "llc-18"), List("--version")) + lazy val optCmd = discoverExecutable(List("opt", "opt-18"), List("--version")) def checkSetup(): Either[String, Unit] = gccCmd.getOrElseAborting { return Left("Cannot find gcc. This is required to use the LLVM backend.") } From 67f47cb6f208446928a2f4d2cde2e6fb899bf76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Immanuel=20Brachth=C3=A4user?= Date: Fri, 28 Mar 2025 16:09:08 +0100 Subject: [PATCH 11/41] Improve performance of state (#907) --- .../effekt/generator/js/TransformerCps.scala | 9 +- libraries/js/effekt_runtime.js | 147 ++++++++++-------- 2 files changed, 83 insertions(+), 73 deletions(-) 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 259d68a91..69bfb8dd5 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala @@ -381,10 +381,7 @@ object TransformerCps extends Transformer { // DEALLOC(ref); body case cps.Stmt.Dealloc(ref, body) => - Binding { k => - js.ExprStmt(js.Call(DEALLOC, nameRef(ref))) :: - toJS(body).run(k) - } + toJS(body) // const id = ref.value; body case cps.Stmt.Get(ref, id, body) => @@ -393,9 +390,9 @@ object TransformerCps extends Transformer { toJS(body).run(k) } - // ref.value = _value; body + // ref.set(value); body case cps.Stmt.Put(ref, value, body) => Binding { k => - js.Assign(js.Member(nameRef(ref), JSName("value")), toJS(value)) :: + js.ExprStmt(js.MethodCall(nameRef(ref), JSName("set"), toJS(value))) :: toJS(body).run(k) } diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index b1b5ff2e1..57ccfc28d 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,69 +1,89 @@ +// Complexity of state: +// +// get: O(1) +// set: O(1) +// capture: O(1) +// restore: O(|write operations since capture|) +const Mem = null + +function Arena() { + const s = { + root: { value: Mem }, + generation: 0, + fresh: (v) => { + const r = { + value: v, + generation: s.generation, + store: s, + set: (v) => { + const s = r.store + const r_gen = r.generation + const s_gen = s.generation + + if (r_gen == s_gen) { + r.value = v; + } else { + const root = { value: Mem } + // update store + s.root.value = { ref: r, value: r.value, generation: r_gen, root: root } + s.root = root + r.value = v + r.generation = s_gen + } + } + }; + return r + }, + // not implemented + newRegion: () => s + }; + return s +} -// Common Runtime -// -------------- -function Cell(init, region) { - const cell = { - value: init, - backup: function() { - const _backup = cell.value; - // restore function (has a STRONG reference to `this`) - return () => { cell.value = _backup; return cell } - } +const global = { + fresh: (v) => { + const r = { + value: v, + set: (v) => { r.value = v } + }; + return r } - return cell; } -const global = { - fresh: Cell +function snapshot(s) { + const snap = { store: s, root: s.root, generation: s.generation } + s.generation = s.generation + 1 + return snap } -function Arena(_region) { - const region = _region; - return { - fresh: function(init) { - const cell = Cell(init); - // region keeps track what to backup, but we do not need to backup unreachable cells - region.push(cell) // new WeakRef(cell)) - return cell; - }, - region: _region, - newRegion: function() { - // a region aggregates weak references - const nested = Arena([]) - // this doesn't work yet, since Arena.backup doesn't return a thunk - region.push(nested) //new WeakRef(nested)) - return nested; - }, - backup: function() { - const _backup = [] - let nextIndex = 0; - for (const ref of region) { - const cell = ref //.deref() - // only backup live cells - if (cell) { - _backup[nextIndex] = cell.backup() - nextIndex++ - } - } - function restore() { - const region = [] - let nextIndex = 0; - for (const restoreCell of _backup) { - region[nextIndex] = restoreCell() // new WeakRef(restoreCell()) - nextIndex++ - } - return Arena(region) - } - return restore; - } - } +function reroot(n) { + if (n.value === Mem) return; + + const diff = n.value + const r = diff.ref + const v = diff.value + const g = diff.generation + const n2 = diff.root + reroot(n2) + n.value = Mem + n2.value = { ref: r, value: r.value, generation: r.generation, root: n} + r.value = v + r.generation = g } +function restore(store, snap) { + // linear in the number of modifications... + reroot(snap.root) + store.root = snap.root + store.generation = snap.generation + 1 +} +// Common Runtime +// -------------- let _prompt = 1; const TOPLEVEL_K = (x, ks) => { throw { computationIsDone: true, result: x } } -const TOPLEVEL_KS = { prompt: 0, arena: Arena([]), rest: null } +const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null } function THUNK(f) { f.thunk = true @@ -80,9 +100,6 @@ function CAPTURE(body) { const RETURN = (x, ks) => ks.rest.stack(x, ks.rest) -// const r = ks.arena.newRegion(); body -// const x = r.alloc(init); body - // HANDLE(ks, ks, (p, ks, k) => { STMT }) function RESET(prog, ks, k) { const prompt = _prompt++; @@ -90,13 +107,6 @@ function RESET(prog, ks, k) { return prog(prompt, { prompt, arena: Arena([]), rest }, RETURN) } -function DEALLOC(ks) { - const arena = ks.arena - if (!!arena) { - arena.length = arena.length - 1 - } -} - function SHIFT(p, body, ks, k) { // TODO avoid constructing this object @@ -104,13 +114,15 @@ function SHIFT(p, body, ks, k) { let cont = null while (!!meta && meta.prompt !== p) { - cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont } + let store = meta.arena + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } meta = meta.rest } if (!meta) { throw `Prompt not found ${p}` } // package the prompt itself - cont = { stack: meta.stack, prompt: meta.prompt, backup: meta.arena.backup(), rest: cont } + let store = meta.arena + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } meta = meta.rest const k1 = meta.stack @@ -123,7 +135,8 @@ function RESUME(cont, c, ks, k) { let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } let toRewind = cont while (!!toRewind) { - meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.backup(), rest: meta } + restore(toRewind.arena, toRewind.backup) + meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta } toRewind = toRewind.rest } From 9ba3cfaf03a4ee080e557a3fc05811abcb18718c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20S=C3=BCberkr=C3=BCb?= Date: Fri, 28 Mar 2025 16:45:52 +0100 Subject: [PATCH 12/41] Fix lsp caching issues (#894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depends on #885 (mostly so we can actually express the unit test). I split `AnnotationsDB` into three separate databases which each tracks its own map. This allows us to properly distinguish types that should be annotated by their object identity (symbols, trees) and those that should be annotated by their value identity (sources). The new types are: * `TreeAnnotations`: key: `source.Tree`, object identity * `SourceAnnotations`: key: `kiama.util.Source`, value identity * `SymbolAnnotations`: key: `symbols.Symbol`, object identity Fixes #876. --------- Co-authored-by: Jonathan Brachthäuser --- .../jvm/src/test/scala/effekt/LSPTests.scala | 239 ++++++++++++- .../scala/effekt/context/Annotations.scala | 331 +++++++++++------- .../main/scala/effekt/context/Context.scala | 4 +- kiama | 2 +- 4 files changed, 435 insertions(+), 141 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala index d463d15a2..9e995c452 100644 --- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala +++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala @@ -20,7 +20,12 @@ class LSPTests extends FunSuite { // // - def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = { + + /** + * @param compileOnChange The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing + * because we would like to switch to `didChange` events once we have working caching for references. + */ + def withClientAndServer(compileOnChange: Boolean)(testBlock: (MockLanguageClient, Server) => Unit): Unit = { val driver = new Driver {} val config = EffektConfig(Seq("--server")) config.verify() @@ -30,9 +35,7 @@ class LSPTests extends FunSuite { val serverIn = new PipedInputStream(clientOut) val serverOut = new PipedOutputStream(clientIn) - // The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing - // because we would like to switch to `didChange` events once we have working caching for references. - val server = new Server(config, compileOnChange = true) + val server = new Server(config, compileOnChange) val mockClient = new MockLanguageClient() server.connect(mockClient) @@ -42,7 +45,11 @@ class LSPTests extends FunSuite { testBlock(mockClient, server) } - // Fixtures + def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = { + withClientAndServer(true)(testBlock) + } + + // Fixtures // // @@ -343,6 +350,78 @@ class LSPTests extends FunSuite { } } + test("Hovering works after editing") { + withClientAndServer { (client, server) => + // Initial code + // + // + + val (textDoc, firstPos) = raw""" + |val x: Int = 42 + | ↑ + |""".textDocumentAndPosition + val hoverContents = + raw"""|#### Value binder + |```effekt + |test::x: Int + |``` + |""".stripMargin + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, firstPos) + val hover = server.getTextDocumentService().hover(hoverParams).get() + + val expectedHover = (pos: Position) => { + val expectedHover = new Hover() + expectedHover.setRange(new Range(pos, pos)) + expectedHover.setContents(new MarkupContent("markdown", hoverContents)) + expectedHover + } + assertEquals(hover, expectedHover(firstPos)) + + // First edit: now we add a blank line in front + // + // + + val (newTextDoc, changeEvent) = textDoc.changeTo( + raw""" + | + |val x: Int = 42 + |""".stripMargin + ) + val secondPos = new Position(firstPos.getLine + 1, firstPos.getCharacter) + + val didChangeParams = new DidChangeTextDocumentParams() + didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) + didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) + server.getTextDocumentService().didChange(didChangeParams) + + val hoverParamsAfterChange = new HoverParams(newTextDoc.versionedTextDocumentIdentifier, secondPos) + val hoverAfterChange = server.getTextDocumentService().hover(hoverParamsAfterChange).get() + + assertEquals(hoverAfterChange, expectedHover(secondPos)) + + // Second edit: we revert the change + // + // + + val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText) + + val didChangeParamsReverted = new DidChangeTextDocumentParams() + didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) + didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent)) + server.getTextDocumentService().didChange(didChangeParamsReverted) + + val hoverParamsAfterRevert = new HoverParams(revertedTextDoc.versionedTextDocumentIdentifier, firstPos) + val hoverAfterRevert = server.getTextDocumentService().hover(hoverParamsAfterRevert).get() + + assertEquals(hoverAfterRevert, expectedHover(firstPos)) + } + } + // LSP: Document symbols // // @@ -487,6 +566,156 @@ class LSPTests extends FunSuite { } } + test("inlayHints work after editing") { + withClientAndServer { (client, server) => + val (textDoc, positions) = + raw""" + |↑ + |def main() = { + |↑ + | println("Hello, world!") + |} + |↑ + |""".textDocumentAndPositions + + val inlayHint = new InlayHint() + inlayHint.setKind(InlayHintKind.Type) + inlayHint.setPosition(positions(1)) + inlayHint.setLabel("{io}") + val markup = new MarkupContent() + markup.setKind("markdown") + markup.setValue("captures: `{io}`") + inlayHint.setTooltip(markup) + inlayHint.setPaddingRight(true) + inlayHint.setData("capture") + + val expectedInlayHints = List(inlayHint) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new InlayHintParams() + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + params.setRange(new Range(positions(0), positions(2))) + + val inlayHints = server.getTextDocumentService().inlayHint(params).get() + assertEquals(inlayHints, expectedInlayHints.asJava) + + // First edit: now we add a blank line in front + // + // + + val (newTextDoc, changeEvent) = textDoc.changeTo( + raw""" + | + |def main() = { + | println("Hello, world!") + |} + |""".stripMargin + ) + val newPos = new Position(positions(1).getLine + 1, positions(1).getCharacter) + + val didChangeParams = new DidChangeTextDocumentParams() + didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) + didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) + server.getTextDocumentService().didChange(didChangeParams) + + val paramsAfterChange = new InlayHintParams() + paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) + paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter))) + + inlayHint.setPosition(newPos) + val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get() + assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava) + + // Second edit: we revert the change + // + // + + val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText) + inlayHint.setPosition(positions(1)) + + val didChangeParamsReverted = new DidChangeTextDocumentParams() + didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) + didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent)) + server.getTextDocumentService().didChange(didChangeParamsReverted) + + val paramsAfterRevert = new InlayHintParams() + paramsAfterRevert.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier) + paramsAfterRevert.setRange(new Range(positions(0), positions(2))) + + val inlayHintsAfterRevert = server.getTextDocumentService().inlayHint(paramsAfterRevert).get() + assertEquals(inlayHintsAfterRevert, expectedInlayHints.asJava) + } + + } + + test("inlayHints work after invalid edits") { + withClientAndServer(false) { (client, server) => + val (textDoc, positions) = + raw""" + |↑ + |def main() = { + |↑ + | println("Hello, world!") + |} + |↑ + |""".textDocumentAndPositions + + val inlayHint = new InlayHint() + inlayHint.setKind(InlayHintKind.Type) + inlayHint.setPosition(positions(1)) + inlayHint.setLabel("{io}") + val markup = new MarkupContent() + markup.setKind("markdown") + markup.setValue("captures: `{io}`") + inlayHint.setTooltip(markup) + inlayHint.setPaddingRight(true) + inlayHint.setData("capture") + + val expectedInlayHints = List(inlayHint) + + val didOpenParams = new DidOpenTextDocumentParams() + didOpenParams.setTextDocument(textDoc) + server.getTextDocumentService().didOpen(didOpenParams) + + val params = new InlayHintParams() + params.setTextDocument(textDoc.versionedTextDocumentIdentifier) + params.setRange(new Range(positions(0), positions(2))) + + val inlayHints = server.getTextDocumentService().inlayHint(params).get() + assertEquals(inlayHints, expectedInlayHints.asJava) + + // Edit: now we add some invalid syntax to the end + // + // + + val (newTextDoc, changeEvent) = textDoc.changeTo( + raw""" + |def main() = { + | println("Hello, world!") + |} + |invalid syntax + |""".stripMargin + ) + + val didChangeParams = new DidChangeTextDocumentParams() + didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) + didChangeParams.setContentChanges(util.Arrays.asList(changeEvent)) + server.getTextDocumentService().didChange(didChangeParams) + + val paramsAfterChange = new InlayHintParams() + paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier) + // The client may send a range that is outside of the text the server currently has + // We use somewhat arbitrary values here. + paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter + 5))) + + val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get() + assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava) + } + } + // Effekt: Publish IR // // diff --git a/effekt/shared/src/main/scala/effekt/context/Annotations.scala b/effekt/shared/src/main/scala/effekt/context/Annotations.scala index bfa619d39..0e34c48c8 100644 --- a/effekt/shared/src/main/scala/effekt/context/Annotations.scala +++ b/effekt/shared/src/main/scala/effekt/context/Annotations.scala @@ -1,12 +1,28 @@ package effekt package context -import effekt.symbols.ResumeParam +import effekt.symbols.{BlockSymbol, BlockType, ResumeParam, Symbol, ValueSymbol} import effekt.util.messages.ErrorReporter -import kiama.util.Memoiser -case class Annotation[K, V](name: String, description: String, bindToObjectIdentity: Boolean = true) { +import java.util + +sealed trait Annotation[K, V] + +case class SymbolAnnotation[K <: symbols.Symbol, V](name: String, description: String) extends Annotation[K, V] { + type Value = V + + override def toString = name +} + +case class SourceAnnotation[K <: kiama.util.Source, V](name: String, description: String) extends Annotation[K, V] { type Value = V + + override def toString = name +} + +case class TreeAnnotation[K <: source.Tree, V](name: String, description: String) extends Annotation[K, V] { + type Value = V + override def toString = name } @@ -34,35 +50,42 @@ class Annotations private( def update[K, V](ann: Annotation[K, V], key: K, value: V): Unit = { val anns = annotationsAt(ann) - val updatedAnns = anns.updated(Annotations.makeKey(ann, key), value) + val updatedAnns = anns.updated(Key(key), value) annotations = annotations.updated(ann, updatedAnns.asInstanceOf) } def get[K, V](ann: Annotation[K, V], key: K): Option[V] = - annotationsAt(ann).get(Annotations.makeKey(ann, key)) + annotationsAt(ann).get(Key(key)) def getOrElse[K, V](ann: Annotation[K, V], key: K, default: => V): V = - annotationsAt(ann).getOrElse(Annotations.makeKey(ann, key), default) + annotationsAt(ann).getOrElse(Key(key), default) def getOrElseUpdate[K, V](ann: Annotation[K, V], key: K, default: => V): V = - annotationsAt(ann).getOrElse(Annotations.makeKey(ann, key), { + annotationsAt(ann).getOrElse(Key(key), { val value = default update(ann, key, value) value }) def removed[K, V](ann: Annotation[K, V], key: K): Unit = - annotations = annotations.updated(ann, annotationsAt(ann).removed(Annotations.makeKey(ann, key)).asInstanceOf) + annotations = annotations.updated(ann, annotationsAt(ann).removed(Key(key)).asInstanceOf) def apply[K, V](ann: Annotation[K, V]): List[(K, V)] = annotationsAt(ann).map { case (k, v) => (k.key, v) }.toList - def apply[K, V](ann: Annotation[K, V], key: K)(implicit C: ErrorReporter): V = - get(ann, key).getOrElse { C.abort(s"Cannot find ${ann.name} '${key}'") } + def apply[K, V](ann: Annotation[K, V], key: K)(using C: ErrorReporter): V = + get(ann, key).getOrElse { C.abort(s"Cannot find ${ann.toString} '${key}'") } - def updateAndCommit[K, V](ann: Annotation[K, V])(f: (K, V) => V)(implicit global: AnnotationsDB): Unit = + def updateAndCommit[K, V](ann: Annotation[K, V])(f: (K, V) => V)(using treesDB: TreeAnnotations, symbolsDB: SymbolAnnotations): Unit = val anns = annotationsAt(ann) - anns.foreach { case (kk, v) => global.annotate(ann, kk.key, f(kk.key, v)) } + anns.foreach { case (kk, v) => + kk.key match { + case sym: symbols.Symbol => + symbolsDB.annotate(ann.asInstanceOf[SymbolAnnotation[_, V]], sym, f(sym, v)) + case key: source.Tree => + treesDB.annotate(ann.asInstanceOf[TreeAnnotation[_, V]], key, f(key, v)) + } + } override def toString = s"Annotations(${annotations})" } @@ -70,38 +93,21 @@ object Annotations { def empty: Annotations = new Annotations(Map.empty) - sealed trait Key[T] { def key: T } - - private class HashKey[T](val key: T) extends Key[T] { + class Key[T](val key: T) { override val hashCode = System.identityHashCode(key) - override def equals(o: Any) = o match { - case k: HashKey[_] => hashCode == k.hashCode - case _ => false - } - } - private class IdKey[T](val key: T) extends Key[T] { - override val hashCode = key.hashCode() override def equals(o: Any) = o match { - case k: Key[_] => key == k.key - case _ => false + case k: Key[_] => hashCode == k.hashCode + case _ => false } } - object Key { - def unapply[T](k: Key[T]): Option[T] = Some(k.key) - } - - private def makeKey[K, V](ann: Annotation[K, V], k: K): Key[K] = - if (ann.bindToObjectIdentity) new HashKey(k) - else new IdKey(k) - /** * The as inferred by typer at a given position in the tree * * Can also be used by LSP server to display type information for type-checked trees */ - val InferredEffect = Annotation[source.Tree, symbols.Effects]( + val InferredEffect = TreeAnnotation[source.Tree, symbols.Effects]( "InferredEffect", "the inferred effect of" ) @@ -112,7 +118,7 @@ object Annotations { * Important for finding the types of temporary variables introduced by transformation * Can also be used by LSP server to display type information for type-checked trees */ - val InferredValueType = Annotation[source.Tree, symbols.ValueType]( + val InferredValueType = TreeAnnotation[source.Tree, symbols.ValueType]( "InferredValueType", "the inferred type of" ) @@ -120,7 +126,7 @@ object Annotations { /** * The type as inferred by typer at a given position in the tree */ - val InferredBlockType = Annotation[source.Tree, symbols.BlockType]( + val InferredBlockType = TreeAnnotation[source.Tree, symbols.BlockType]( "InferredBlockType", "the inferred block type of" ) @@ -128,7 +134,7 @@ object Annotations { /** * Type arguments of a _function call_ as inferred by typer */ - val TypeArguments = Annotation[source.CallLike, List[symbols.ValueType]]( + val TypeArguments = TreeAnnotation[source.CallLike, List[symbols.ValueType]]( "TypeArguments", "the inferred or annotated type arguments of" ) @@ -136,7 +142,7 @@ object Annotations { /** * Existential type parameters inferred by the typer when type-checking pattern matches. */ - val TypeParameters = Annotation[source.TagPattern | source.OpClause, List[symbols.TypeVar]]( + val TypeParameters = TreeAnnotation[source.TagPattern | source.OpClause, List[symbols.TypeVar]]( "TypeParameters", "the existentials of the constructor pattern or operation clause" ) @@ -144,7 +150,7 @@ object Annotations { /** * Value type of symbols like value binders or value parameters */ - val ValueType = Annotation[symbols.ValueSymbol, symbols.ValueType]( + val ValueType = SymbolAnnotation[symbols.ValueSymbol, symbols.ValueType]( "ValueType", "the type of value symbol" ) @@ -152,7 +158,7 @@ object Annotations { /** * Block type of symbols like function definitions, block parameters, or continuations */ - val BlockType = Annotation[symbols.BlockSymbol, symbols.BlockType]( + val BlockType = SymbolAnnotation[symbols.BlockSymbol, symbols.BlockType]( "BlockType", "the type of block symbol" ) @@ -160,7 +166,7 @@ object Annotations { /** * Capability set used by a function definition, block parameter, ... */ - val Captures = Annotation[symbols.BlockSymbol, symbols.Captures]( + val Captures = SymbolAnnotation[symbols.BlockSymbol, symbols.Captures]( "Captures", "the set of used capabilities of a block symbol" ) @@ -168,7 +174,7 @@ object Annotations { /** * Used by LSP to list all captures */ - val CaptureForFile = Annotation[kiama.util.Source, List[(source.Tree, symbols.CaptureSet)]]( + val CaptureForFile = SourceAnnotation[kiama.util.Source, List[(source.Tree, symbols.CaptureSet)]]( "CaptureSet", "all inferred captures for file" ) @@ -178,7 +184,7 @@ object Annotations { * * @deprecated */ - val SourceModule = Annotation[symbols.Symbol, symbols.Module]( + val SourceModule = SymbolAnnotation[symbols.Symbol, symbols.Module]( "SourceModule", "the source module of symbol" ) @@ -186,7 +192,7 @@ object Annotations { /** * Used by LSP for jump-to-definition of imports */ - val IncludedSymbols = Annotation[source.Include, symbols.Module]( + val IncludedSymbols = TreeAnnotation[source.Include, symbols.Module]( "IncludedSymbols", "the symbol for an import / include" ) @@ -194,7 +200,7 @@ object Annotations { /** * All symbols defined in a source file */ - val DefinedSymbols = Annotation[kiama.util.Source, Set[symbols.Symbol]]( + val DefinedSymbols = SourceAnnotation[kiama.util.Source, Set[symbols.Symbol]]( "DefinedSymbols", "all symbols for source file" ) @@ -206,7 +212,7 @@ object Annotations { * * TODO maybe store the whole definition tree instead of the name, which requries refactoring of assignSymbol */ - val DefinitionTree = Annotation[symbols.Symbol, source.IdDef]( + val DefinitionTree = SymbolAnnotation[symbols.Symbol, source.IdDef]( "DefinitionTree", "the tree identifying the definition site of symbol" ) @@ -216,7 +222,7 @@ object Annotations { * * Filled by namer and used for reverse lookup in LSP server */ - val References = Annotation[symbols.Symbol, List[source.Reference]]( + val References = SymbolAnnotation[symbols.Symbol, List[source.Reference]]( "References", "the references referring to symbol" ) @@ -227,7 +233,7 @@ object Annotations { * Id can be the definition-site (IdDef) or use-site (IdRef) of the * specific symbol */ - val Symbol = Annotation[source.Id, symbols.Symbol]( + val Symbol = TreeAnnotation[source.Id, symbols.Symbol]( "Symbol", "the symbol for identifier" ) @@ -237,12 +243,12 @@ object Annotations { * * Resolved and annotated by namer and used by typer. */ - val Type = Annotation[source.Type, symbols.Type]( + val Type = TreeAnnotation[source.Type, symbols.Type]( "Type", "the resolved type for" ) - val Capture = Annotation[source.CaptureSet, symbols.CaptureSet]( + val Capture = TreeAnnotation[source.CaptureSet, symbols.CaptureSet]( "Capture", "the resolved capture set for" ) @@ -250,7 +256,7 @@ object Annotations { /** * Similar to TypeAndEffect: the capture set of a program */ - val InferredCapture = Annotation[source.Tree, symbols.CaptureSet]( + val InferredCapture = TreeAnnotation[source.Tree, symbols.CaptureSet]( "InferredCapture", "the inferred capture for source tree" ) @@ -261,7 +267,7 @@ object Annotations { * * Inferred by typer, used by elaboration. */ - val BoundCapabilities = Annotation[source.Tree, List[symbols.BlockParam]]( + val BoundCapabilities = TreeAnnotation[source.Tree, List[symbols.BlockParam]]( "BoundCapabilities", "capabilities bound by this tree" ) @@ -271,12 +277,12 @@ object Annotations { * * Inferred by typer, used by elaboration. */ - val CapabilityArguments = Annotation[source.CallLike, List[symbols.BlockParam]]( + val CapabilityArguments = TreeAnnotation[source.CallLike, List[symbols.BlockParam]]( "CapabilityArguments", "capabilities inferred as additional arguments for this call" ) - val CapabilityReceiver = Annotation[source.Do, symbols.BlockParam]( + val CapabilityReceiver = TreeAnnotation[source.Do, symbols.BlockParam]( "CapabilityReceiver", "the receiver as inferred for this effect operation call" ) @@ -286,7 +292,7 @@ object Annotations { * * Used by typer for region checking mutable variables. */ - val SelfRegion = Annotation[source.Tree, symbols.TrackedParam]( + val SelfRegion = TreeAnnotation[source.Tree, symbols.TrackedParam]( "SelfRegion", "the region corresponding to a lexical scope" ) @@ -298,39 +304,40 @@ object Annotations { * Introduced by the pretyper. * Used by typer in order to display a more precise error message. */ - val UnboxParentDef = Annotation[source.Unbox, source.Def]( + val UnboxParentDef = TreeAnnotation[source.Unbox, source.Def]( "UnboxParentDef", "the parent definition of an Unbox if it was synthesized" ) } - /** - * A global annotations database + * Global annotations on syntax trees * * This database is mixed into the compiler `Context` and is * globally visible across all phases. If you want to hide changes in - * subsequent phases, consider using an instance of `Annotions`, instead. + * subsequent phases, consider using an instance of `Annotations`, instead. * - * Calling `Annotations.commit` transfers all annotations into this global DB. + * Calling `Annotations.commit` transfers all annotations into the global databases. * * The DB is also "global" in the sense, that modifications cannot be backtracked. * It should thus only be used to store a "ground" truth that will not be changed again. */ -trait AnnotationsDB { self: Context => +trait TreeAnnotations { self: Context => + private type AnnotationsMap = Map[TreeAnnotation[_, _], Any] - private type Annotations = Map[Annotation[_, _], Any] - type DB = Memoiser[Any, Map[Annotation[_, _], Any]] - var db: DB = Memoiser.makeIdMemoiser() + private type Annotations = Map[TreeAnnotation[_, _], Any] + type DB = util.IdentityHashMap[source.Tree, Map[TreeAnnotation[_, _], Any]] + var db: DB = new util.IdentityHashMap() - private def annotationsAt(key: Any): Map[Annotation[_, _], Any] = db.getOrDefault(key, Map.empty) + private def annotationsAt[K](key: K): AnnotationsMap = + db.getOrDefault(key, Map.empty) /** * Copies annotations, keeping existing annotations at `to` */ - def copyAnnotations(from: Any, to: Any): Unit = { + def copyAnnotations(from: source.Tree, to: source.Tree): Unit = { val existing = annotationsAt(to) - val source = annotationsAt(from) + val source = annotationsAt(from) annotate(to, source ++ existing) } @@ -339,23 +346,25 @@ trait AnnotationsDB { self: Context => * * Used by Annotations.commit to commit all temporary annotations to the DB */ - def annotate[K, V](key: K, value: Map[Annotation[_, _], Any]): Unit = { - val anns = annotationsAt(key) + def annotate(key: source.Tree, value: AnnotationsMap): Unit = { + val anns = db.getOrDefault(key, Map.empty) db.put(key, anns ++ value) } - def annotate[K, V](ann: Annotation[K, V], key: K, value: V): Unit = { - val anns = annotationsAt(key) + def annotate[K <: source.Tree, V](ann: TreeAnnotation[K, V], key: source.Tree, value: V): Unit = { + val anns = db.getOrDefault(key, Map.empty) db.put(key, anns + (ann -> value)) } - def annotationOption[K, V](ann: Annotation[K, V], key: K): Option[V] = + def annotationOption[V](ann: TreeAnnotation[_, V], key: source.Tree): Option[V] = annotationsAt(key).get(ann).asInstanceOf[Option[V]] - def annotation[K, V](ann: Annotation[K, V], key: K): V = - annotationOption(ann, key).getOrElse { panic(s"Cannot find ${ann.description} for '${key}'") } + def annotation[V](ann: TreeAnnotation[_, V], key: source.Tree): V = + annotationOption(ann, key).getOrElse { + panic(s"Cannot find ${ann.description} for '${key}'") + } - def hasAnnotation[K, V](ann: Annotation[K, V], key: K): Boolean = + def hasAnnotation[V](ann: TreeAnnotation[_, V], key: source.Tree): Boolean = annotationsAt(key).isDefinedAt(ann) // Customized Accessors @@ -421,51 +430,6 @@ trait AnnotationsDB { self: Context => def resolvedCapture(tree: source.CaptureSet): symbols.CaptureSet = annotation(Annotations.Capture, tree) - def typeOf(s: Symbol): Type = s match { - case s: ValueSymbol => valueTypeOf(s) - case s: BlockSymbol => blockTypeOf(s) - case _ => panic(s"Cannot find a type for symbol '${s}'") - } - - def functionTypeOf(s: Symbol): FunctionType = - functionTypeOption(s) getOrElse { panic(s"Cannot find type for block '${s}'") } - - def functionTypeOption(s: Symbol): Option[FunctionType] = - s match { - case b: BlockSymbol => annotationOption(Annotations.BlockType, b) flatMap { - case b: FunctionType => Some(b) - case _ => None - } - // The callsite should be adjusted, this is NOT the job of annotations... - case v: ValueSymbol => ??? - // valueTypeOption(v).flatMap { v => - // v.dealias match { - // case symbols.BoxedType(tpe: FunctionType, _) => Some(tpe) - // case _ => None - // } - // } - } - - def blockTypeOf(s: Symbol): BlockType = - blockTypeOption(s) getOrElse { panic(s"Cannot find interface type for block '${s}'") } - - def blockTypeOption(s: Symbol): Option[BlockType] = - s match { - case b: BlockSymbol => annotationOption(Annotations.BlockType, b) flatMap { - case b: BlockType => Some(b) - } - case _ => panic(s"Trying to find a interface type for non block '${s}'") - } - - def valueTypeOf(s: Symbol): ValueType = - valueTypeOption(s) getOrElse { panic(s"Cannot find value binder for ${s}") } - - def valueTypeOption(s: Symbol): Option[ValueType] = s match { - case s: ValueSymbol => annotationOption(Annotations.ValueType, s) - case _ => panic(s"Trying to find a value type for non-value '${s}'") - } - - // Symbols // ------- @@ -521,44 +485,143 @@ trait AnnotationsDB { self: Context => */ def symbolOf(tree: source.Definition): Symbol = symbolOf(tree.id) +} + +/** + * Global annotations on entire Source objects + * + * It is very important that the comparison between keys is based on value rather than object identity: + * Even when a separate Source object is crated for the same file contents, it should track the same annotations. + * This situation frequently occurs in the language server where sources are transmitted from the language client (editor). + */ +trait SourceAnnotations { self: Context => + import scala.collection.mutable + + private val sourceAnnotationsDB: mutable.Map[kiama.util.Source, Map[SourceAnnotation[_, _], Any]] = + mutable.Map.empty + + private def annotationsAt(source: kiama.util.Source): Map[SourceAnnotation[_, _], Any] = + sourceAnnotationsDB.getOrElse(source, Map.empty) + + def annotate[A](ann: SourceAnnotation[_, A], source: kiama.util.Source, value: A): Unit = { + val anns = annotationsAt(source) + sourceAnnotationsDB.update(source, anns + (ann -> value)) + } + + def annotationOption[A](ann: SourceAnnotation[_, A], source: kiama.util.Source): Option[A] = + annotationsAt(source).get(ann).asInstanceOf[Option[A]] /** - * Searching the definition for a symbol + * List all symbols that have a source module + * + * Used by the LSP server to generate outline */ - def definitionTreeOption(s: Symbol): Option[source.IdDef] = - annotationOption(Annotations.DefinitionTree, s) + def sourceSymbolsFor(src: kiama.util.Source): Set[symbols.Symbol] = + annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty) /** * Adds [[s]] to the set of defined symbols for the current module, by writing * it into the [[Annotations.DefinedSymbols]] annotation. */ - def addDefinedSymbolToSource(s: Symbol): Unit = + def addDefinedSymbolToSource(s: symbols.Symbol): Unit = if (module != null) { - val syms = annotationOption(Annotations.DefinedSymbols, module.source).getOrElse(Set.empty) - annotate(Annotations.DefinedSymbols, module.source, syms + s) + val src = module.source + val syms = annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty) + annotate(Annotations.DefinedSymbols, src, syms + s) + } +} + +/** + * Global annotations on symbols + */ +trait SymbolAnnotations { self: Context => + + private val symbolAnnotationsDB: util.IdentityHashMap[symbols.Symbol, Map[SymbolAnnotation[_, _], Any]] = + new util.IdentityHashMap() + + // Retrieve the annotations for a given symbol. + private def annotationsAt(sym: symbols.Symbol): Map[SymbolAnnotation[_, _], Any] = + symbolAnnotationsDB.getOrDefault(sym, Map.empty) + + // Annotate a symbol with an annotation and its value. + def annotate[A](ann: SymbolAnnotation[_, A], sym: symbols.Symbol, value: A): Unit = { + val key = sym + val anns = annotationsAt(sym) + symbolAnnotationsDB.put(key, anns + (ann -> value)) + } + + // Retrieve an optional annotation for a symbol. + def annotationOption[A](ann: SymbolAnnotation[_, A], sym: symbols.Symbol): Option[A] = + annotationsAt(sym).get(ann).asInstanceOf[Option[A]] + + def typeOf(s: Symbol): symbols.Type = s match { + case s: ValueSymbol => valueTypeOf(s) + case s: BlockSymbol => blockTypeOf(s) + case _ => panic(s"Cannot find a type for symbol '${s}'") + } + + // Retrieve the value type of a value symbol. + def valueTypeOption(s: symbols.Symbol): Option[symbols.ValueType] = s match { + case vs: symbols.ValueSymbol => annotationOption(Annotations.ValueType, vs) + case _ => panic(s"Trying to find a value type for non-value '${s}'") + } + + def valueTypeOf(s: symbols.Symbol): symbols.ValueType = + valueTypeOption(s).getOrElse(panic(s"Cannot find value type for ${s}")) + + def blockTypeOption(s: Symbol): Option[BlockType] = + s match { + case b: BlockSymbol => annotationOption(Annotations.BlockType, b) flatMap { + case b: BlockType => Some(b) + } + case _ => panic(s"Trying to find a interface type for non block '${s}'") } + def blockTypeOf(s: symbols.Symbol): symbols.BlockType = + blockTypeOption(s).getOrElse(panic(s"Cannot find block type for ${s}")) + + // Retrieve the function type of a block symbol. + def functionTypeOption(s: symbols.Symbol): Option[symbols.FunctionType] = s match { + case bs: symbols.BlockSymbol => + annotationOption(Annotations.BlockType, bs) match { + case Some(ft: symbols.FunctionType) => Some(ft) + case _ => None + } + // The callsite should be adjusted, this is NOT the job of annotations... + case v: ValueSymbol => ??? + // valueTypeOption(v).flatMap { v => + // v.dealias match { + // case symbols.BoxedType(tpe: FunctionType, _) => Some(tpe) + // case _ => None + // } + // } + case _ => None + } + + def functionTypeOf(s: symbols.Symbol): symbols.FunctionType = + functionTypeOption(s).getOrElse(panic(s"Cannot find function type for ${s}")) + /** - * List all symbols that have a source module - * - * Used by the LSP server to generate outline + * Searching the definition for a symbol */ - def sourceSymbolsFor(src: kiama.util.Source): Set[Symbol] = - annotationOption(Annotations.DefinedSymbols, src).getOrElse(Set.empty) + def definitionTreeOption(s: symbols.Symbol): Option[source.IdDef] = + annotationOption(Annotations.DefinitionTree, s) /** * List all references for a symbol * * Used by the LSP server for reverse lookup */ - def distinctReferencesTo(sym: Symbol): List[source.Reference] = + def distinctReferencesTo(sym: symbols.Symbol): List[source.Reference] = annotationOption(Annotations.References, sym) .getOrElse(Nil) + .asInstanceOf[List[source.Reference]] .distinctBy(r => System.identityHashCode(r)) - def captureOf(sym: BlockSymbol): symbols.Captures = - annotation(Annotations.Captures, sym) + def captureOf(sym: symbols.BlockSymbol): symbols.Captures = + annotationOption(Annotations.Captures, sym) + .getOrElse(panic(s"Cannot find captures for ${sym}")) - def captureOfOption(sym: BlockSymbol): Option[symbols.Captures] = + def captureOfOption(sym: symbols.BlockSymbol): Option[symbols.Captures] = annotationOption(Annotations.Captures, sym) } diff --git a/effekt/shared/src/main/scala/effekt/context/Context.scala b/effekt/shared/src/main/scala/effekt/context/Context.scala index aca90b526..594f287f8 100644 --- a/effekt/shared/src/main/scala/effekt/context/Context.scala +++ b/effekt/shared/src/main/scala/effekt/context/Context.scala @@ -18,7 +18,9 @@ import kiama.util.Positions */ trait ContextOps extends ErrorReporter - with AnnotationsDB { self: Context => + with TreeAnnotations + with SourceAnnotations + with SymbolAnnotations { self: Context => /** * Used throughout the compiler to create a new "scope" diff --git a/kiama b/kiama index f4379b140..668874d4e 160000 --- a/kiama +++ b/kiama @@ -1 +1 @@ -Subproject commit f4379b1402374efadd188a050c37da1fcdb325ec +Subproject commit 668874d4e54c303bb45d0df2ee1abb38eb81a3e1 From 86ebd77bbc1cdb63f63d54a40d25172dcf72f8ce Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:47:03 +0100 Subject: [PATCH 13/41] Fix: ufcs integer lexing (#912) fixes #911 --- effekt/shared/src/main/scala/effekt/Lexer.scala | 5 ++++- examples/pos/int_ufcs.check | 2 ++ examples/pos/int_ufcs.effekt | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 examples/pos/int_ufcs.check create mode 100644 examples/pos/int_ufcs.effekt diff --git a/effekt/shared/src/main/scala/effekt/Lexer.scala b/effekt/shared/src/main/scala/effekt/Lexer.scala index 53f52c991..da98747c6 100644 --- a/effekt/shared/src/main/scala/effekt/Lexer.scala +++ b/effekt/shared/src/main/scala/effekt/Lexer.scala @@ -340,7 +340,10 @@ class Lexer(source: Source) { case None => err("Not a 64bit floating point literal.") case Some(n) => TokenKind.Float(n) } - case _ => TokenKind.Integer(slice().toInt) + case _ => slice().toLongOption match { + case None => err("Not a 64bit integer literal.") + case Some(n) => TokenKind.Integer(n) + } } } case _ => diff --git a/examples/pos/int_ufcs.check b/examples/pos/int_ufcs.check new file mode 100644 index 000000000..7f957ee0a --- /dev/null +++ b/examples/pos/int_ufcs.check @@ -0,0 +1,2 @@ +1234567890123 +1234567890123 diff --git a/examples/pos/int_ufcs.effekt b/examples/pos/int_ufcs.effekt new file mode 100644 index 000000000..74ac683d6 --- /dev/null +++ b/examples/pos/int_ufcs.effekt @@ -0,0 +1,5 @@ +def main() = { + 1234567890123.println + println(1234567890123) +} + From 1a7175120ad5cc4b48b6c4c5916ba98f085d0ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 28 Mar 2025 17:36:25 +0100 Subject: [PATCH 14/41] Add binary search benchmark (#904) Add binary search inspired by Jules Jacobs and Brent Yorgey. The interesting part is the missed optimisations --- if you comment out `150.findSqrtUpTo(100)`, you get _much_ better IR and JS code :) This could be made into a parametric benchmark: given N, search between 0 and N\*N for sqrt(N\*N). --- examples/benchmarks/other/binarysearch.check | 6 +++ examples/benchmarks/other/binarysearch.effekt | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 examples/benchmarks/other/binarysearch.check create mode 100644 examples/benchmarks/other/binarysearch.effekt diff --git a/examples/benchmarks/other/binarysearch.check b/examples/benchmarks/other/binarysearch.check new file mode 100644 index 000000000..cccad55fc --- /dev/null +++ b/examples/benchmarks/other/binarysearch.check @@ -0,0 +1,6 @@ +sqrt of 150 is between: +12 (^2 = 144) +13 (^2 = 169) +sqrt of 9876543210123 is between: +3142696 (^2 = 9876538148416) +3142697 (^2 = 9876544433809) diff --git a/examples/benchmarks/other/binarysearch.effekt b/examples/benchmarks/other/binarysearch.effekt new file mode 100644 index 000000000..53922e60f --- /dev/null +++ b/examples/benchmarks/other/binarysearch.effekt @@ -0,0 +1,49 @@ +module binarysearch + +// Effectful binary search +// ... inspired by Jules Jacobs https://julesjacobs.com/notes/binarysearch/binarysearch.pdf +// ... ... and Brent Yorgey https://byorgey.wordpress.com/2023/01/01/competitive-programming-in-haskell-better-binary-search/ + +effect breakWith[A](value: A): Nothing +effect mid[A](l: A, r: A): A / breakWith[(A, A)] + +def break2[A, B](x: A, y: B) = + do breakWith((x, y)) +def boundary[A] { prog: => A / breakWith[A] }: A = + try prog() with breakWith[A] { a => a } + +def search[A](l: A, r: A) { predicate: A => Bool }: (A, A) / mid[A] = boundary[(A, A)] { + def go(l: A, r: A): (A, A) = { + val m = do mid(l, r) + if (predicate(m)) { + go(l, m) + } else { + go(m, r) + } + } + go(l, r) +} + +def binary[R] { prog: => R / mid[Int] }: R = + try prog() with mid[Int] { (l, r) => + resume { + if ((r - l) > 1) { + (l + r) / 2 + } else { + break2(l, r) + } + } + } + +def main() = binary { + def findSqrtUpTo(pow2: Int, max: Int) = { + val (l, r) = search(0, max) { x => x * x >= pow2 } + println("sqrt of " ++ pow2.show ++ " is between:") + println(l.show ++ " (^2 = " ++ (l * l).show ++ ")") + println(r.show ++ " (^2 = " ++ (r * r).show ++ ")") + } + + // Comment out the first call below to get much better JS/Core codegen: + findSqrtUpTo(150, 100) + findSqrtUpTo(9876543210123, 9000000) +} From 9b9266c67fc3804d20510e57a331ca3ef660bf60 Mon Sep 17 00:00:00 2001 From: phischu Date: Fri, 28 Mar 2025 18:08:45 +0100 Subject: [PATCH 15/41] Properly represent existentials in core (#880) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jonathan Brachthäuser --- .../effekt/core/PatternMatchingTests.scala | 14 ++--- .../src/main/scala/effekt/core/Parser.scala | 2 +- .../effekt/core/PatternMatchingCompiler.scala | 60 ++++++++++--------- .../effekt/core/PolymorphismBoxing.scala | 4 +- .../scala/effekt/core/PrettyPrinter.scala | 2 +- .../main/scala/effekt/core/Recursive.scala | 2 +- .../main/scala/effekt/core/Transformer.scala | 27 ++++++--- .../src/main/scala/effekt/core/Tree.scala | 8 +-- .../src/main/scala/effekt/core/Type.scala | 2 +- .../core/optimizer/BindSubexpressions.scala | 4 +- .../effekt/core/optimizer/Normalizer.scala | 8 +-- .../effekt/core/optimizer/Reachable.scala | 2 +- .../core/optimizer/StaticArguments.scala | 2 +- .../src/main/scala/effekt/core/vm/VM.scala | 2 +- .../main/scala/effekt/cps/Transformer.scala | 2 +- .../effekt/generator/chez/Transformer.scala | 2 +- .../effekt/generator/js/Transformer.scala | 2 +- .../scala/effekt/machine/Transformer.scala | 4 +- 18 files changed, 83 insertions(+), 66 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala b/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala index d7a5ce5fd..521c00940 100644 --- a/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/PatternMatchingTests.scala @@ -38,10 +38,10 @@ class PatternMatchingTests extends CoreTests { List( Condition.Patterns(Map(x -> Pattern.Any(y.id))), Condition.Patterns(Map(y -> Pattern.Any(z.id)))), - f, List(z))) + f, Nil, List(z))) // case => f(z) - val expected = Clause(Nil, f, List(x)) + val expected = Clause(Nil, f, Nil, List(x)) assertEquals(normalized, expected) } @@ -73,11 +73,11 @@ class PatternMatchingTests extends CoreTests { Condition.Patterns(Map(sc -> Pattern.Any(x.id))), Condition.Val(p.id, TBoolean, trivalPredicate), Condition.Predicate(p)), - b1, List(x)), + b1, Nil, List(x)), Clause( List( Condition.Patterns(Map(sc -> Pattern.Ignore()))), - b2, List()))) + b2, Nil, List()))) val expected = Val(p.id, TBoolean, trivalPredicate, @@ -124,14 +124,14 @@ class PatternMatchingTests extends CoreTests { val result = compile(List( Clause( List( - Condition.Patterns(Map(opt -> Pattern.Tag(SomeC, List(Pattern.Any(v.id) -> TInt)))), + Condition.Patterns(Map(opt -> Pattern.Tag(SomeC, List(), List(Pattern.Any(v.id) -> TInt)))), Condition.Val(p.id, TBoolean, trivalPredicate), Condition.Predicate(p)), - b1, List(v)), + b1, Nil, List(v)), Clause( List( Condition.Patterns(Map(opt -> Pattern.Ignore()))), - b2, List()))) + b2, Nil, List()))) // opt match { // case Some(tmp) => val p = return v > 0; if (p) { b1(tmp) } else { b2() } diff --git a/effekt/shared/src/main/scala/effekt/core/Parser.scala b/effekt/shared/src/main/scala/effekt/core/Parser.scala index b83fcd22f..9907dd340 100644 --- a/effekt/shared/src/main/scala/effekt/core/Parser.scala +++ b/effekt/shared/src/main/scala/effekt/core/Parser.scala @@ -168,7 +168,7 @@ class CoreParsers(positions: Positions, names: Names) extends EffektLexers(posit ( literal | id ~ (`:` ~> valueType) ^^ Pure.ValueVar.apply | `box` ~> captures ~ block ^^ { case capt ~ block => Pure.Box(block, capt) } - | `make` ~> dataType ~ id ~ valueArgs ^^ Pure.Make.apply + | `make` ~> dataType ~ id ~ maybeTypeArgs ~ valueArgs ^^ Pure.Make.apply | maybeParens(blockVar) ~ maybeTypeArgs ~ valueArgs ^^ Pure.PureApp.apply | failure("Expected a pure expression.") ) diff --git a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala b/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala index 83bd76f64..04ae6f906 100644 --- a/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala +++ b/effekt/shared/src/main/scala/effekt/core/PatternMatchingCompiler.scala @@ -1,9 +1,7 @@ package effekt package core -import effekt.context.Context import effekt.core.substitutions.Substitution -import effekt.symbols.TmpValue import scala.collection.mutable @@ -54,9 +52,9 @@ import scala.collection.mutable object PatternMatchingCompiler { /** - * The conditions need to be met in sequence before the block at [[label]] can be evaluated with given [[args]]. + * The conditions need to be met in sequence before the block at [[label]] can be evaluated with given [[targs]] and [[args]]. */ - case class Clause(conditions: List[Condition], label: BlockVar, args: List[ValueVar]) + case class Clause(conditions: List[Condition], label: BlockVar, targs: List[ValueType], args: List[ValueVar]) enum Condition { // all of the patterns need to match for this condition to be met @@ -71,7 +69,7 @@ object PatternMatchingCompiler { enum Pattern { // sub-patterns are annotated with the inferred type of the scrutinee at this point // i.e. Cons(Some(x : TInt): Option[Int], xs: List[Option[Int]]) - case Tag(id: Id, patterns: List[(Pattern, ValueType)]) + case Tag(id: Id, tparams: List[Id], patterns: List[(Pattern, ValueType)]) case Ignore() case Any(id: Id) case Or(patterns: List[Pattern]) @@ -93,21 +91,21 @@ object PatternMatchingCompiler { // (1) Check the first clause to be matched (we can immediately handle non-pattern cases) val patterns = headClause match { // - The top-most clause already matches successfully - case Clause(Nil, target, args) => - return core.App(target, Nil, args, Nil) + case Clause(Nil, target, targs, args) => + return core.App(target, targs, args, Nil) // - We need to perform a computation - case Clause(Condition.Val(x, tpe, binding) :: rest, target, args) => - return core.Val(x, tpe, binding, compile(Clause(rest, target, args) :: remainingClauses)) + case Clause(Condition.Val(x, tpe, binding) :: rest, target, targs, args) => + return core.Val(x, tpe, binding, compile(Clause(rest, target, targs, args) :: remainingClauses)) // - We need to perform a computation - case Clause(Condition.Let(x, tpe, binding) :: rest, target, args) => - return core.Let(x, tpe, binding, compile(Clause(rest, target, args) :: remainingClauses)) + case Clause(Condition.Let(x, tpe, binding) :: rest, target, targs, args) => + return core.Let(x, tpe, binding, compile(Clause(rest, target, targs, args) :: remainingClauses)) // - We need to check a predicate - case Clause(Condition.Predicate(pred) :: rest, target, args) => + case Clause(Condition.Predicate(pred) :: rest, target, targs, args) => return core.If(pred, - compile(Clause(rest, target, args) :: remainingClauses), + compile(Clause(rest, target, targs, args) :: remainingClauses), compile(remainingClauses) ) - case Clause(Condition.Patterns(patterns) :: rest, target, args) => + case Clause(Condition.Patterns(patterns) :: rest, target, targs, args) => patterns } @@ -127,7 +125,7 @@ object PatternMatchingCompiler { def splitOnLiteral(lit: Literal, equals: (Pure, Pure) => Pure): core.Stmt = { // the different literal values that we match on val variants: List[core.Literal] = normalized.collect { - case Clause(Split(Pattern.Literal(lit, _), _, _), _, _) => lit + case Clause(Split(Pattern.Literal(lit, _), _, _), _, _, _) => lit }.distinct // for each literal, we collect the clauses that match it correctly @@ -141,8 +139,8 @@ object PatternMatchingCompiler { defaults = defaults :+ cl normalized.foreach { - case Clause(Split(Pattern.Literal(lit, _), restPatterns, restConds), label, args) => - addClause(lit, Clause(Condition.Patterns(restPatterns) :: restConds, label, args)) + case Clause(Split(Pattern.Literal(lit, _), restPatterns, restConds), label, targs, args) => + addClause(lit, Clause(Condition.Patterns(restPatterns) :: restConds, label, targs, args)) case c => addDefault(c) variants.foreach { v => addClause(v, c) } @@ -164,7 +162,7 @@ object PatternMatchingCompiler { def splitOnTag() = { // collect all variants that are mentioned in the clauses val variants: List[Id] = normalized.collect { - case Clause(Split(p: Pattern.Tag, _, _), _, _) => p.id + case Clause(Split(p: Pattern.Tag, _, _), _, _, _) => p.id }.distinct // for each tag, we collect the clauses that match it correctly @@ -179,7 +177,9 @@ object PatternMatchingCompiler { // used to make up new scrutinees val varsFor = mutable.Map.empty[Id, List[ValueVar]] - def fieldVarsFor(constructor: Id, fieldInfo: List[((Pattern, ValueType), String)]): List[ValueVar] = + val tvarsFor = mutable.Map.empty[Id, List[Id]] + def fieldVarsFor(constructor: Id, tparams: List[Id], fieldInfo: List[((Pattern, ValueType), String)]): List[ValueVar] = + tvarsFor.getOrElseUpdate(constructor, tparams) varsFor.getOrElseUpdate( constructor, fieldInfo.map { @@ -191,17 +191,17 @@ object PatternMatchingCompiler { ) normalized.foreach { - case Clause(Split(Pattern.Tag(constructor, patternsAndTypes), restPatterns, restConds), label, args) => + case Clause(Split(Pattern.Tag(constructor, tparams, patternsAndTypes), restPatterns, restConds), label, targs, args) => // NOTE: Ideally, we would use a `DeclarationContext` here, but we cannot: we're currently in the Source->Core transformer, so we do not have all of the details yet. val fieldNames: List[String] = constructor match { case c: symbols.Constructor => c.fields.map(_.name.name) case _ => List.fill(patternsAndTypes.size) { "y" } // NOTE: Only reached in PatternMatchingTests } - val fieldVars = fieldVarsFor(constructor, patternsAndTypes.zip(fieldNames)) + val fieldVars = fieldVarsFor(constructor, tparams, patternsAndTypes.zip(fieldNames)) val nestedMatches = fieldVars.zip(patternsAndTypes.map { case (pat, tpe) => pat }).toMap addClause(constructor, // it is important to add nested matches first, since they might include substitutions for the rest. - Clause(Condition.Patterns(nestedMatches) :: Condition.Patterns(restPatterns) :: restConds, label, args)) + Clause(Condition.Patterns(nestedMatches) :: Condition.Patterns(restPatterns) :: restConds, label, targs, args)) case c => // Clauses that don't match on that var are duplicated. @@ -214,8 +214,9 @@ object PatternMatchingCompiler { // (4) assemble syntax tree for the pattern match val branches = variants.map { v => val body = compile(clausesFor.getOrElse(v, Nil)) + val tparams = tvarsFor(v) val params = varsFor(v).map { case ValueVar(id, tpe) => core.ValueParam(id, tpe): core.ValueParam } - val blockLit: BlockLit = BlockLit(Nil, Nil, params, Nil, body) + val blockLit: BlockLit = BlockLit(tparams, Nil, params, Nil, body) (v, blockLit) } @@ -232,16 +233,17 @@ object PatternMatchingCompiler { def branchingHeuristic(patterns: Map[ValueVar, Pattern], clauses: List[Clause]): ValueVar = patterns.keys.maxBy(v => clauses.count { - case Clause(ps, _, _) => ps.contains(v) + case Clause(ps, _, _, _) => ps.contains(v) }) /** * Substitutes AnyPattern and removes wildcards. */ def normalize(clause: Clause): Clause = clause match { - case Clause(conditions, label, args) => + case Clause(conditions, label, targs, args) => val (normalized, substitution) = normalize(Map.empty, conditions, Map.empty) - Clause(normalized, label, args.map(v => substitution.getOrElse(v.id, v))) + // TODO also substitute types? + Clause(normalized, label, targs, args.map(v => substitution.getOrElse(v.id, v))) } @@ -309,8 +311,8 @@ object PatternMatchingCompiler { // ----------------------------- def show(cl: Clause): String = cl match { - case Clause(conditions, label, args) => - s"case ${conditions.map(show).mkString("; ")} => ${util.show(label.id)}${args.map(x => util.show(x)).mkString("(", ", ", ")")}" + case Clause(conditions, label, targs, args) => + s"case ${conditions.map(show).mkString("; ")} => ${util.show(label.id)}${targs.map(x => util.show(x))}${args.map(x => util.show(x)).mkString("(", ", ", ")")}" } def show(c: Condition): String = c match { @@ -321,7 +323,7 @@ object PatternMatchingCompiler { } def show(p: Pattern): String = p match { - case Pattern.Tag(id, patterns) => util.show(id) + patterns.map { case (p, tpe) => show(p) }.mkString("(", ", ", ")") + case Pattern.Tag(id, tparams, patterns) => util.show(id) + tparams.map(util.show).mkString("[", ",", "]") + patterns.map { case (p, tpe) => show(p) }.mkString("(", ", ", ")") case Pattern.Ignore() => "_" case Pattern.Any(id) => util.show(id) case Pattern.Or(patterns) => patterns.map(show).mkString(" | ") diff --git a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala index 901e1a2ab..05f4fbef2 100644 --- a/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala +++ b/effekt/shared/src/main/scala/effekt/core/PolymorphismBoxing.scala @@ -349,7 +349,7 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] { val vCoerced = (vargs zip tpe.vparams).map { (a, tpe) => coerce(transform(a), tpe) } coerce(PureApp(callee, targs.map(transformArg), vCoerced), itpe.result) - case Pure.Make(data, tag, vargs) => + case Pure.Make(data, tag, targs, vargs) => val dataDecl = PContext.getData(data.name) val ctorDecl = dataDecl.constructors.find(_.id == tag).getOrElse { Context.panic(pp"No constructor found for tag ${tag} in data type: ${data}") @@ -357,7 +357,7 @@ object PolymorphismBoxing extends Phase[CoreTransformed, CoreTransformed] { val paramTypes = ctorDecl.fields.map(_.tpe) val coercedArgs = (vargs zip paramTypes).map { case (arg, paramTpe) => coerce(transform(arg), paramTpe) } - Pure.Make(transform(data), tag, coercedArgs) + Pure.Make(transform(data), tag, targs.map(transformArg), coercedArgs) case Pure.Box(b, annotatedCapture) => Pure.Box(transform(b), annotatedCapture) diff --git a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala index a417c5cfd..d69fc45a8 100644 --- a/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala @@ -100,7 +100,7 @@ object PrettyPrinter extends ParenPrettyPrinter { case ValueVar(id, _) => toDoc(id) case PureApp(b, targs, vargs) => toDoc(b) <> argsToDoc(targs, vargs, Nil) - case Make(data, tag, vargs) => "make" <+> toDoc(data) <+> toDoc(tag) <> argsToDoc(Nil, vargs, Nil) + case Make(data, tag, targs, vargs) => "make" <+> toDoc(data) <+> toDoc(tag) <> argsToDoc(targs, vargs, Nil) case DirectApp(b, targs, vargs, bargs) => toDoc(b) <> argsToDoc(targs, vargs, bargs) case Box(b, capt) => parens("box" <+> toDoc(b)) diff --git a/effekt/shared/src/main/scala/effekt/core/Recursive.scala b/effekt/shared/src/main/scala/effekt/core/Recursive.scala index 7542320e9..f21bf3e52 100644 --- a/effekt/shared/src/main/scala/effekt/core/Recursive.scala +++ b/effekt/shared/src/main/scala/effekt/core/Recursive.scala @@ -94,7 +94,7 @@ class Recursive( case Pure.ValueVar(id, annotatedType) => () case Pure.Literal(value, annotatedType) => () case Pure.PureApp(b, targs, vargs) => process(b); vargs.foreach(process) - case Pure.Make(data, tag, vargs) => vargs.foreach(process) + case Pure.Make(data, tag, targs, vargs) => vargs.foreach(process) case Pure.Box(b, annotatedCapture) => process(b) } diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala index d3c1ef545..8f6b9f5dc 100644 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala @@ -273,13 +273,13 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { Stmt.Return(PureApp(BlockVar(b), targs, vargs))) } - // [[ f ]] = { (x) => make f(x) } + // [[ f ]] = { [A](x) => make f[A](x) } def etaExpandConstructor(b: Constructor): BlockLit = { assert(bparamtps.isEmpty) assert(effects.isEmpty) assert(cparams.isEmpty) BlockLit(tparams, Nil, vparams, Nil, - Stmt.Return(Make(core.ValueType.Data(b.tpe, targs), b, vargs))) + Stmt.Return(Make(core.ValueType.Data(b.tpe, targs), b, targs, vargs))) } // [[ f ]] = { (x){g} => let r = f(x){g}; return r } @@ -668,6 +668,15 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { case MatchGuard.BooleanGuard(condition) => Nil case MatchGuard.PatternGuard(scrutinee, pattern) => boundInPattern(pattern) } + def boundTypesInPattern(p: source.MatchPattern): List[Id] = p match { + case source.AnyPattern(id) => List() + case p @ source.TagPattern(id, patterns) => Context.annotation(Annotations.TypeParameters, p) ++ patterns.flatMap(boundTypesInPattern) + case _: source.LiteralPattern | _: source.IgnorePattern => Nil + } + def boundTypesInGuard(g: source.MatchGuard): List[Id] = g match { + case MatchGuard.BooleanGuard(condition) => Nil + case MatchGuard.PatternGuard(scrutinee, pattern) => boundTypesInPattern(pattern) + } def equalsFor(tpe: symbols.ValueType): (Pure, Pure) => Pure = val prelude = Context.module.findDependency(QualifiedName(Nil, "effekt")).getOrElse { Context.panic(pp"${Context.module.name.name}: Cannot find 'effekt' in prelude, which is necessary to compile pattern matching.") @@ -685,14 +694,16 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { } getOrElse { Context.panic(pp"Cannot find == for type ${tpe} in prelude!") } // create joinpoint + val tparams = patterns.flatMap { case (sc, p) => boundTypesInPattern(p) } ++ guards.flatMap(boundTypesInGuard) val params = patterns.flatMap { case (sc, p) => boundInPattern(p) } ++ guards.flatMap(boundInGuard) - val joinpoint = Context.bind(TmpBlock(label), BlockLit(Nil, Nil, params, Nil, body)) + val joinpoint = Context.bind(TmpBlock(label), BlockLit(tparams, Nil, params, Nil, body)) def transformPattern(p: source.MatchPattern): Pattern = p match { case source.AnyPattern(id) => Pattern.Any(id.symbol) - case source.TagPattern(id, patterns) => - Pattern.Tag(id.symbol, patterns.map { p => (transformPattern(p), transform(Context.inferredTypeOf(p))) }) + case p @ source.TagPattern(id, patterns) => + val tparams = Context.annotation(Annotations.TypeParameters, p) + Pattern.Tag(id.symbol, tparams, patterns.map { p => (transformPattern(p), transform(Context.inferredTypeOf(p))) }) case source.IgnorePattern() => Pattern.Ignore() case source.LiteralPattern(source.Literal(value, tpe)) => @@ -719,7 +730,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { val transformedGuards = guards.flatMap(transformGuard) val conditions = if transformedPatterns.isEmpty then transformedGuards else Condition.Patterns(transformedPatterns) :: guards.flatMap(transformGuard) - Clause(conditions, joinpoint, params.map(p => core.ValueVar(p.id, p.tpe))) + Clause(conditions, joinpoint, tparams.map(x => core.ValueType.Var(x)), params.map(p => core.ValueVar(p.id, p.tpe))) } /** @@ -785,7 +796,9 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { DirectApp(BlockVar(f), targs, vargsT, bargsT) case r: Constructor => if (bargs.nonEmpty) Context.abort("Constructors cannot take block arguments.") - Make(core.ValueType.Data(r.tpe, targs), r, vargsT) + val universals = targs.take(r.tpe.tparams.length) + val existentials = targs.drop(r.tpe.tparams.length) + Make(core.ValueType.Data(r.tpe, universals), r, existentials, vargsT) case f: Operation => Context.panic("Should have been translated to a method call!") case f: Field => diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 171a7befc..43f5e94a7 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -191,7 +191,7 @@ enum Pure extends Expr { * * Note: the structure mirrors interface implementation */ - case Make(data: ValueType.Data, tag: Id, vargs: List[Pure]) + case Make(data: ValueType.Data, tag: Id, targs: List[ValueType], vargs: List[Pure]) case Box(b: Block, annotatedCapture: Captures) } @@ -578,7 +578,7 @@ object Variables { case Pure.ValueVar(id, annotatedType) => Variables.value(id, annotatedType) case Pure.Literal(value, annotatedType) => Variables.empty case Pure.PureApp(b, targs, vargs) => free(b) ++ all(vargs, free) - case Pure.Make(data, tag, vargs) => all(vargs, free) + case Pure.Make(data, tag, targs, vargs) => all(vargs, free) case Pure.Box(b, annotatedCapture) => free(b) } @@ -772,8 +772,8 @@ object substitutions { case Literal(value, annotatedType) => Literal(value, substitute(annotatedType)) - case Make(tpe, tag, vargs) => - Make(substitute(tpe).asInstanceOf, tag, vargs.map(substitute)) + case Make(tpe, tag, targs, vargs) => + Make(substitute(tpe).asInstanceOf, tag, targs.map(substitute), vargs.map(substitute)) case PureApp(f, targs, vargs) => substitute(f) match { case g : Block.BlockVar => PureApp(g, targs.map(substitute), vargs.map(substitute)) diff --git a/effekt/shared/src/main/scala/effekt/core/Type.scala b/effekt/shared/src/main/scala/effekt/core/Type.scala index e52bbaaa1..cdba76670 100644 --- a/effekt/shared/src/main/scala/effekt/core/Type.scala +++ b/effekt/shared/src/main/scala/effekt/core/Type.scala @@ -252,7 +252,7 @@ object Type { case Pure.ValueVar(id, tpe) => tpe case Pure.Literal(value, tpe) => tpe case Pure.PureApp(callee, targs, args) => instantiate(callee.functionType, targs, Nil).result - case Pure.Make(tpe, tag, args) => tpe + case Pure.Make(tpe, tag, targs, args) => tpe // TODO instantiate? case Pure.Box(block, capt) => ValueType.Boxed(block.tpe, capt) } diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala index 3b91c76e8..478710e95 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/BindSubexpressions.scala @@ -126,8 +126,8 @@ object BindSubexpressions { case Pure.ValueVar(id, tpe) => pure(ValueVar(transform(id), transform(tpe))) case Pure.Literal(value, tpe) => pure(Pure.Literal(value, transform(tpe))) - case Pure.Make(data, tag, vargs) => transformExprs(vargs) { vs => - bind(Pure.Make(data, tag, vs)) + case Pure.Make(data, tag, targs, vargs) => transformExprs(vargs) { vs => + bind(Pure.Make(data, tag, targs, vs)) } case DirectApp(f, targs, vargs, bargs) => for { vs <- transformExprs(vargs); 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 6344fb070..dceb07a8e 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala @@ -204,10 +204,10 @@ object Normalizer { normal => } case Stmt.Match(scrutinee, clauses, default) => active(scrutinee) match { - case Pure.Make(data, tag, vargs) if clauses.exists { case (id, _) => id == tag } => + case Pure.Make(data, tag, targs, vargs) if clauses.exists { case (id, _) => id == tag } => val clause: BlockLit = clauses.collectFirst { case (id, cl) if id == tag => cl }.get - normalize(reduce(clause, Nil, vargs.map(normalize), Nil)) - case Pure.Make(data, tag, vargs) if default.isDefined => + normalize(reduce(clause, targs, vargs.map(normalize), Nil)) + case Pure.Make(data, tag, targs, vargs) if default.isDefined => normalize(default.get) case _ => val normalized = normalize(scrutinee) @@ -355,7 +355,7 @@ object Normalizer { normal => // congruences case Pure.PureApp(f, targs, vargs) => Pure.PureApp(f, targs, vargs.map(normalize)) - case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(normalize)) + case Pure.Make(data, tag, targs, vargs) => Pure.Make(data, tag, targs, vargs.map(normalize)) case Pure.ValueVar(id, annotatedType) => p case Pure.Literal(value, annotatedType) => p } diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala index 15cbbceeb..abbd2327a 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Reachable.scala @@ -109,7 +109,7 @@ class Reachable( case Pure.ValueVar(id, annotatedType) => process(id) case Pure.Literal(value, annotatedType) => () case Pure.PureApp(b, targs, vargs) => process(b); vargs.foreach(process) - case Pure.Make(data, tag, vargs) => process(tag); vargs.foreach(process) + case Pure.Make(data, tag, targs, vargs) => process(tag); vargs.foreach(process) case Pure.Box(b, annotatedCapture) => process(b) } diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala index b8d9f69c1..09f33c9ca 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala @@ -185,7 +185,7 @@ object StaticArguments { def rewrite(p: Pure)(using StaticArgumentsContext): Pure = p match { case Pure.PureApp(f, targs, vargs) => Pure.PureApp(f, targs, vargs.map(rewrite)) - case Pure.Make(data, tag, vargs) => Pure.Make(data, tag, vargs.map(rewrite)) + case Pure.Make(data, tag, targs, vargs) => Pure.Make(data, tag, targs, vargs.map(rewrite)) case x @ Pure.ValueVar(id, annotatedType) => x // congruences 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 631e779c2..1adfa2758 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -506,7 +506,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { case other => other.toString }.mkString(", ")}" } } - case Pure.Make(data, tag, vargs) => + case Pure.Make(data, tag, targs, vargs) => val result: Value.Data = Value.Data(data, tag, vargs.map(a => eval(a, env))) instrumentation.allocate(result) result diff --git a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala index 636787d09..b43c41633 100644 --- a/effekt/shared/src/main/scala/effekt/cps/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/cps/Transformer.scala @@ -182,7 +182,7 @@ object Transformer { case Block.BlockVar(id) => PureApp(id, vargs.map(transform)) case _ => sys error "Should not happen" } - case core.Pure.Make(data, tag, vargs) => Make(data, tag, vargs.map(transform)) + case core.Pure.Make(data, tag, targs, vargs) => Make(data, tag, vargs.map(transform)) case core.Pure.Box(b, annotatedCapture) => Box(transform(b)) } 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 01ea52559..458723355 100644 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala @@ -222,7 +222,7 @@ trait Transformer { case DirectApp(b, targs, vargs, bargs) => chez.Call(toChez(b), vargs.map(toChez) ++ bargs.map(toChez)) case PureApp(b, targs, args) => chez.Call(toChez(b), args map toChez) - case Make(data, tag, args) => chez.Call(chez.Variable(nameRef(tag)), args map toChez) + case Make(data, tag, targs, args) => chez.Call(chez.Variable(nameRef(tag)), args map toChez) case Box(b, _) => toChez(b) } diff --git a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala index f0065b61b..0ddb299aa 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/Transformer.scala @@ -174,7 +174,7 @@ trait Transformer { register(publicDependencySymbols(x), x) case ValueVar(x, tpe) if publicDependencySymbols.isDefinedAt(x) => register(publicDependencySymbols(x), x) - case Make(tpe, id, args) if publicDependencySymbols.isDefinedAt(id) => + case Make(tpe, id, targs, args) if publicDependencySymbols.isDefinedAt(id) => register(publicDependencySymbols(id), id) args.foreach(go) } diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 116e4acfd..97682de91 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -417,7 +417,9 @@ object Transformer { } } - case core.Make(data, constructor, vargs) => + case core.Make(data, constructor, targs, vargs) => + if (targs.exists(requiresBoxing)) { ErrorReporter.abort(s"Types ${targs} are used as type parameters but would require boxing.") } + val variable = Variable(freshName("make"), transform(data)); val tag = DeclarationContext.getConstructorTag(constructor) From 87a080dfe30b0cc51ecec71cd11eec44cfd70a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Fri, 28 Mar 2025 19:39:21 +0100 Subject: [PATCH 16/41] Allow lambda case patterns for multiple parameters (#914) Resolves #761 by allowing multiple patterns in a lambda case seperated by `,` like: ``` def foo[A,B,C](){ fn: (A, B) => C }: C = ... //... foo(){ case true, y => ... case x, y => ... } ``` ## Implementation This becomes a `Match` with multiple scrutinees, which is resolved in the pattern matching compiler. Typer checks that the clauses have the correct number of patterns (which will be assumed later). --- .../shared/src/main/scala/effekt/Namer.scala | 2 + .../main/scala/effekt/RecursiveDescent.scala | 53 ++++++++++++++++--- .../shared/src/main/scala/effekt/Typer.scala | 39 +++++++++++--- .../main/scala/effekt/core/Transformer.scala | 22 +++++--- .../src/main/scala/effekt/source/Tree.scala | 11 +++- .../effekt/typer/BoxUnboxInference.scala | 4 +- .../scala/effekt/typer/ConcreteEffects.scala | 4 ++ .../effekt/typer/ExhaustivityChecker.scala | 37 +++++++++---- .../scala/effekt/typer/Wellformedness.scala | 13 +++-- examples/neg/lambda_case_exhaustivity.effekt | 7 +++ examples/neg/lambdas/inference.check | 4 +- examples/neg/multi_arity_match_arity.effekt | 10 ++++ .../neg/multi_arity_match_exhaustivity.effekt | 9 ++++ examples/pos/multi_arity_lambda_case.check | 2 + examples/pos/multi_arity_lambda_case.effekt | 16 ++++++ 15 files changed, 196 insertions(+), 37 deletions(-) create mode 100644 examples/neg/lambda_case_exhaustivity.effekt create mode 100644 examples/neg/multi_arity_match_arity.effekt create mode 100644 examples/neg/multi_arity_match_exhaustivity.effekt create mode 100644 examples/pos/multi_arity_lambda_case.check create mode 100644 examples/pos/multi_arity_lambda_case.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 38e9c2835..1f1437ef3 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -654,6 +654,8 @@ object Namer extends Phase[Parsed, NameResolved] { } } patterns.flatMap { resolve } + case source.MultiPattern(patterns) => + patterns.flatMap { resolve } } def resolve(p: source.MatchGuard)(using Context): Unit = p match { diff --git a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala index 02d996280..c02e8ff35 100644 --- a/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala +++ b/effekt/shared/src/main/scala/effekt/RecursiveDescent.scala @@ -406,7 +406,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) val default = when(`else`) { Some(stmt()) } { None } val body = semi() ~> stmts() val clause = MatchClause(p, guards, body).withRangeOf(p, sc) - val matching = Match(sc, List(clause), default).withRangeOf(startMarker, sc) + val matching = Match(List(sc), List(clause), default).withRangeOf(startMarker, sc) Return(matching) } @@ -757,8 +757,13 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) def matchClause(): MatchClause = nonterminal: + val patterns = `case` ~> some(matchPattern, `,`) + val pattern = patterns match { + case List(pat) => pat + case pats => MultiPattern(pats) + } MatchClause( - `case` ~> matchPattern(), + pattern, manyWhile(`and` ~> matchGuard(), `and`), // allow a statement enclosed in braces or without braces // both is allowed since match clauses are already delimited by `case` @@ -802,7 +807,7 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) while (peek(`match`)) { val clauses = `match` ~> braces { manyWhile(matchClause(), `case`) } val default = when(`else`) { Some(stmt()) } { None } - sc = Match(sc, clauses, default) + sc = Match(List(sc), clauses, default) } sc @@ -944,14 +949,18 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) peek.kind match { // { case ... => ... } case `case` => someWhile(matchClause(), `case`) match { case cs => + val arity = cs match { + case MatchClause(MultiPattern(ps), _, _) :: _ => ps.length + case _ => 1 + } // TODO positions should be improved here and fresh names should be generated for the scrutinee // also mark the temp name as synthesized to prevent it from being listed in VSCode - val name = "__tmpRes" + val names = List.tabulate(arity){ n => s"__arg${n}" } BlockLiteral( Nil, - List(ValueParam(IdDef(name), None)), + names.map{ name => ValueParam(IdDef(name), None) }, Nil, - Return(Match(Var(IdRef(Nil, name)), cs, None))) : BlockLiteral + Return(Match(names.map{ name => Var(IdRef(Nil, name)) }, cs, None))) : BlockLiteral } case _ => // { (x: Int) => ... } @@ -1453,8 +1462,36 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source) // case _ => () // } - positions.setStart(res, source.offsetToPosition(start)) - positions.setFinish(res, source.offsetToPosition(end)) + val startPos = source.offsetToPosition(start) + val endPos = source.offsetToPosition(end) + + // recursively add positions to subtrees that are not yet annotated + // this is better than nothing and means we have positions for desugared stuff + def annotatePositions(res: Any): Unit = res match { + case l: List[_] => + if (positions.getRange(l).isEmpty) { + positions.setStart(l, startPos) + positions.setFinish(l, endPos) + l.foreach(annotatePositions) + } + case t: Tree => + val recurse = positions.getRange(t).isEmpty + if(positions.getStart(t).isEmpty) positions.setStart(t, startPos) + if(positions.getFinish(t).isEmpty) positions.setFinish(t, endPos) + t match { + case p: Product if recurse => + p.productIterator.foreach { c => + annotatePositions(c) + } + case _ => () + } + case _ => () + } + annotatePositions(res) + + // still annotate, in case it is not Tree + positions.setStart(res, startPos) + positions.setFinish(res, endPos) res } diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index edc45379e..aaecfb21a 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -287,18 +287,43 @@ object Typer extends Phase[NameResolved, Typechecked] { Result(ret, (effs -- handled) ++ handlerEffs) - case tree @ source.Match(sc, clauses, default) => + case tree @ source.Match(scs, clauses, default) => - // (1) Check scrutinee + // (1) Check scrutinees // for example. tpe = List[Int] - val Result(tpe, effs) = checkExpr(sc, None) + val results = scs.map{ sc => checkExpr(sc, None) } - var resEff = effs + var resEff = ConcreteEffects.union(results.map{ case Result(tpe, effs) => effs }) + + // check that number of patterns matches number of scrutinees + val arity = scs.length + clauses.foreach { + case cls @ source.MatchClause(source.MultiPattern(patterns), guards, body) => + if (patterns.length != arity) { + Context.at(cls){ + Context.error(pp"Number of patterns (${patterns.length}) does not match number of parameters / scrutinees (${arity}).") + } + } + case cls @ source.MatchClause(pattern, guards, body) => + if (arity != 1) { + Context.at(cls) { + Context.error(pp"Number of patterns (1) does not match number of parameters / scrutinees (${arity}).") + } + } + } val tpes = clauses.map { case source.MatchClause(p, guards, body) => - // (3) infer types for pattern - Context.bind(checkPattern(tpe, p)) + // (3) infer types for pattern(s) + p match { + case source.MultiPattern(ps) => + (results zip ps).foreach { case (Result(tpe, effs), p) => + Context.bind(checkPattern(tpe, p)) + } + case p => + val Result(tpe, effs) = results.head + Context.bind(checkPattern(tpe, p)) + } // infer types for guards val Result((), guardEffs) = checkGuards(guards) // check body of the clause @@ -592,6 +617,8 @@ object Typer extends Phase[NameResolved, Typechecked] { } bindings + case source.MultiPattern(patterns) => + Context.panic("Multi-pattern should have been split at the match and not occur nested.") } match { case res => Context.annotateInferredType(pattern, sc); res } def checkGuard(guard: MatchGuard)(using Context, Captures): Result[Map[Symbol, ValueType]] = guard match { diff --git a/effekt/shared/src/main/scala/effekt/core/Transformer.scala b/effekt/shared/src/main/scala/effekt/core/Transformer.scala index 8f6b9f5dc..dfee5412e 100644 --- a/effekt/shared/src/main/scala/effekt/core/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Transformer.scala @@ -427,14 +427,14 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { Context.bind(loopCall) // Empty match (matching on Nothing) - case source.Match(sc, Nil, None) => + case source.Match(List(sc), Nil, None) => val scrutinee: ValueVar = Context.bind(transformAsPure(sc)) Context.bind(core.Match(scrutinee, Nil, None)) - case source.Match(sc, cs, default) => + case source.Match(scs, cs, default) => // (1) Bind scrutinee and all clauses so we do not have to deal with sharing on demand. - val scrutinee: ValueVar = Context.bind(transformAsPure(sc)) - val clauses = cs.zipWithIndex.map((c, i) => preprocess(s"k${i}", scrutinee, c)) + val scrutinees: List[ValueVar] = scs.map{ sc => Context.bind(transformAsPure(sc)) } + val clauses = cs.zipWithIndex.map((c, i) => preprocess(s"k${i}", scrutinees, c)) val defaultClause = default.map(stmt => preprocess("k_els", Nil, Nil, transform(stmt))).toList val compiledMatch = PatternMatchingCompiler.compile(clauses ++ defaultClause) Context.bind(compiledMatch) @@ -653,8 +653,14 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { }) } - def preprocess(label: String, sc: ValueVar, clause: source.MatchClause)(using Context): Clause = - preprocess(label, List((sc, clause.pattern)), clause.guards, transform(clause.body)) + def preprocess(label: String, scs: List[ValueVar], clause: source.MatchClause)(using Context): Clause = { + val patterns = (clause.pattern, scs) match { + case (source.MultiPattern(ps), scs) => scs.zip(ps) + case (pattern, List(sc)) => List((sc, clause.pattern)) + case (_, _) => Context.abort("Malformed multi-match") + } + preprocess(label, patterns, clause.guards, transform(clause.body)) + } def preprocess(label: String, patterns: List[(ValueVar, source.MatchPattern)], guards: List[source.MatchGuard], body: core.Stmt)(using Context): Clause = { import PatternMatchingCompiler.* @@ -663,6 +669,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { case p @ source.AnyPattern(id) => List(ValueParam(p.symbol)) case source.TagPattern(id, patterns) => patterns.flatMap(boundInPattern) case _: source.LiteralPattern | _: source.IgnorePattern => Nil + case source.MultiPattern(patterns) => patterns.flatMap(boundInPattern) } def boundInGuard(g: source.MatchGuard): List[core.ValueParam] = g match { case MatchGuard.BooleanGuard(condition) => Nil @@ -672,6 +679,7 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { case source.AnyPattern(id) => List() case p @ source.TagPattern(id, patterns) => Context.annotation(Annotations.TypeParameters, p) ++ patterns.flatMap(boundTypesInPattern) case _: source.LiteralPattern | _: source.IgnorePattern => Nil + case source.MultiPattern(patterns) => patterns.flatMap(boundTypesInPattern) } def boundTypesInGuard(g: source.MatchGuard): List[Id] = g match { case MatchGuard.BooleanGuard(condition) => Nil @@ -708,6 +716,8 @@ object Transformer extends Phase[Typechecked, CoreTransformed] { Pattern.Ignore() case source.LiteralPattern(source.Literal(value, tpe)) => Pattern.Literal(Literal(value, transform(tpe)), equalsFor(tpe)) + case source.MultiPattern(patterns) => + Context.panic("Multi-pattern should have been split on toplevel / nested MultiPattern") } def transformGuard(p: source.MatchGuard): List[Condition] = diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 194b551e2..30e519832 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -382,7 +382,7 @@ enum Term extends Tree { // Control Flow case If(guards: List[MatchGuard], thn: Stmt, els: Stmt) case While(guards: List[MatchGuard], block: Stmt, default: Option[Stmt]) - case Match(scrutinee: Term, clauses: List[MatchClause], default: Option[Stmt]) + case Match(scrutinees: List[Term], clauses: List[MatchClause], default: Option[Stmt]) /** * Handling effects @@ -504,6 +504,15 @@ enum MatchPattern extends Tree { * A pattern that matches a single literal value */ case LiteralPattern(l: Literal) + + /** + * A pattern for multiple values + * + * case a, b => ... + * + * Currently should *only* occur in lambda-cases during Parsing + */ + case MultiPattern(patterns: List[MatchPattern]) extends MatchPattern } export MatchPattern.* diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala index 7b0a47daf..518e505f8 100644 --- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala +++ b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala @@ -68,8 +68,8 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] { case While(guards, body, default) => While(guards.map(rewrite), rewrite(body), default.map(rewrite)) - case Match(sc, clauses, default) => - Match(rewriteAsExpr(sc), clauses.map(rewrite), default.map(rewrite)) + case Match(scs, clauses, default) => + Match(scs.map(rewriteAsExpr), clauses.map(rewrite), default.map(rewrite)) case s @ Select(recv, name) if s.definition.isInstanceOf[Field] => Select(rewriteAsExpr(recv), name) diff --git a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala b/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala index 0ce1165e8..f68e66f04 100644 --- a/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala +++ b/effekt/shared/src/main/scala/effekt/typer/ConcreteEffects.scala @@ -52,6 +52,10 @@ object ConcreteEffects { def apply(effs: Effects)(using Context): ConcreteEffects = apply(effs.toList) def empty: ConcreteEffects = fromList(Nil) + + def union(effs: IterableOnce[ConcreteEffects]): ConcreteEffects = { + ConcreteEffects.fromList(effs.iterator.flatMap{ e => e.effects }.toList) + } } val Pure = ConcreteEffects.empty diff --git a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala b/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala index 06f8422f3..f7faeaec1 100644 --- a/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala +++ b/effekt/shared/src/main/scala/effekt/typer/ExhaustivityChecker.scala @@ -92,20 +92,28 @@ object ExhaustivityChecker { // Scrutinees are identified by tracing from the original scrutinee. enum Trace { - case Root(scrutinee: source.Term) + case Root(scrutinees: source.Term) case Child(c: Constructor, field: Field, outer: Trace) } - def preprocess(root: source.Term, cl: source.MatchClause)(using Context): Clause = cl match { - case source.MatchClause(pattern, guards, body) => + def preprocess(roots: List[source.Term], cl: source.MatchClause)(using Context): Clause = (roots, cl) match { + case (List(root), source.MatchClause(pattern, guards, body)) => Clause.normalized(Condition.Patterns(Map(Trace.Root(root) -> preprocessPattern(pattern))) :: guards.map(preprocessGuard), cl) + case (roots, source.MatchClause(MultiPattern(patterns), guards, body)) => + val rootConds: Map[Trace, Pattern] = (roots zip patterns).map { case (root, pattern) => + Trace.Root(root) -> preprocessPattern(pattern) + }.toMap + Clause.normalized(Condition.Patterns(rootConds) :: guards.map(preprocessGuard), cl) + case (_, _) => Context.abort("Malformed multi-match") } def preprocessPattern(p: source.MatchPattern)(using Context): Pattern = p match { case AnyPattern(id) => Pattern.Any() case IgnorePattern() => Pattern.Any() case p @ TagPattern(id, patterns) => Pattern.Tag(p.definition, patterns.map(preprocessPattern)) case LiteralPattern(lit) => Pattern.Literal(lit.value, lit.tpe) + case MultiPattern(patterns) => + Context.panic("Multi-pattern should have been split in preprocess already / nested MultiPattern") } def preprocessGuard(g: source.MatchGuard)(using Context): Condition = g match { case MatchGuard.BooleanGuard(condition) => @@ -121,7 +129,7 @@ object ExhaustivityChecker { * - non exhaustive pattern match should generate a list of patterns, so the IDE can insert them * - redundant cases should generate a list of cases that can be deleted. */ - class Exhaustivity(allClauses: List[source.MatchClause]) { + class Exhaustivity(allClauses: List[source.MatchClause], originalScrutinees: List[source.Term]) { // Redundancy Information // ---------------------- @@ -152,7 +160,8 @@ object ExhaustivityChecker { def reportNonExhaustive()(using C: ErrorReporter): Unit = { @tailrec def traceToCase(at: Trace, acc: String): String = at match { - case Trace.Root(_) => acc + case Trace.Root(_) if originalScrutinees.length == 1 => acc + case Trace.Root(e) => originalScrutinees.map { f => if e == f then acc else "_" }.mkString(", ") case Trace.Child(childCtor, field, outer) => val newAcc = s"${childCtor.name}(${childCtor.fields.map { f => if f == field then acc else "_" }.mkString(", ")})" traceToCase(outer, newAcc) @@ -191,13 +200,23 @@ object ExhaustivityChecker { } } - def checkExhaustive(scrutinee: source.Term, cls: List[source.MatchClause])(using C: Context): Unit = { - val initialClauses: List[Clause] = cls.map(preprocess(scrutinee, _)) - given E: Exhaustivity = new Exhaustivity(cls) - checkScrutinee(Trace.Root(scrutinee), Context.inferredTypeOf(scrutinee), initialClauses) + def checkExhaustive(scrutinees: List[source.Term], cls: List[source.MatchClause])(using C: Context): Unit = { + val initialClauses: List[Clause] = cls.map(preprocess(scrutinees, _)) + given E: Exhaustivity = new Exhaustivity(cls, scrutinees) + checkScrutinees(scrutinees.map(Trace.Root(_)), scrutinees.map{ scrutinee => Context.inferredTypeOf(scrutinee) }, initialClauses) E.report() } + def checkScrutinees(scrutinees: List[Trace], tpes: List[ValueType], clauses: List[Clause])(using E: Exhaustivity): Unit = { + (scrutinees, tpes) match { + case (List(scrutinee), List(tpe)) => checkScrutinee(scrutinee, tpe, clauses) + case _ => + clauses match { + case Nil => E.missingDefault(tpes.head, scrutinees.head) + case head :: tail => matchClauses(head, tail) + } + } + } def checkScrutinee(scrutinee: Trace, tpe: ValueType, clauses: List[Clause])(using E: Exhaustivity): Unit = { diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala index ecfd7330b..e8f4d5079 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala @@ -161,12 +161,17 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] pp"The return type ${tpe} of the region body is not allowed to refer to region ${reg.capture}." }) - case tree @ source.Match(scrutinee, clauses, default) => Context.at(tree) { + case tree @ source.Match(scrutinees, clauses, default) => Context.at(tree) { // TODO copy annotations from default to synthesized defaultClause (in particular positions) - val defaultClause = default.toList.map(body => source.MatchClause(source.IgnorePattern(), Nil, body)) - ExhaustivityChecker.checkExhaustive(scrutinee, clauses ++ defaultClause) + val defaultPattern = scrutinees match { + case List(_) => source.IgnorePattern() + case scs => source.MultiPattern(List.fill(scs.length){source.IgnorePattern()}) + } + + val defaultClause = default.toList.map(body => source.MatchClause(defaultPattern, Nil, body)) + ExhaustivityChecker.checkExhaustive(scrutinees, clauses ++ defaultClause) - query(scrutinee) + scrutinees foreach { query } clauses foreach { query } default foreach query diff --git a/examples/neg/lambda_case_exhaustivity.effekt b/examples/neg/lambda_case_exhaustivity.effekt new file mode 100644 index 000000000..13dc5ddc6 --- /dev/null +++ b/examples/neg/lambda_case_exhaustivity.effekt @@ -0,0 +1,7 @@ +def foo[A, B](a: A){ body: A => B }: B = body(a) + +def main() = { + foo(true){ // ERROR Non-exhaustive + case false => () + } +} \ No newline at end of file diff --git a/examples/neg/lambdas/inference.check b/examples/neg/lambdas/inference.check index 8c2650d04..ea7eebe7e 100644 --- a/examples/neg/lambdas/inference.check +++ b/examples/neg/lambdas/inference.check @@ -1,4 +1,4 @@ -[error] Expected type +[error] examples/neg/lambdas/inference.effekt:13:8: Expected type (Int => Bool at {}) => String but got type (Int => Unit at {}) => String @@ -8,6 +8,8 @@ Type mismatch between Bool and Unit. (Int => Unit at {}) => String (given) (Int => Bool at {}) => String (expected) when comparing the return type of the function. + hof2(fun(f: (Int) => Unit at {}) { "" }) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [error] examples/neg/lambdas/inference.effekt:13:8: Expected type (Int => Bool at {}) => String at {} but got type diff --git a/examples/neg/multi_arity_match_arity.effekt b/examples/neg/multi_arity_match_arity.effekt new file mode 100644 index 000000000..1f741b393 --- /dev/null +++ b/examples/neg/multi_arity_match_arity.effekt @@ -0,0 +1,10 @@ +def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = { + fn(a, b) +} + +def main() = { + foo(1, 2){ + case _, _ => () + case _, _, _ => () // ERROR Number of patterns + } +} \ No newline at end of file diff --git a/examples/neg/multi_arity_match_exhaustivity.effekt b/examples/neg/multi_arity_match_exhaustivity.effekt new file mode 100644 index 000000000..e3aa96836 --- /dev/null +++ b/examples/neg/multi_arity_match_exhaustivity.effekt @@ -0,0 +1,9 @@ +def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = { + fn(a, b) +} + +def main() = { + foo(1, false){ // ERROR Non-exhaustive + case _, true => () + } +} \ No newline at end of file diff --git a/examples/pos/multi_arity_lambda_case.check b/examples/pos/multi_arity_lambda_case.check new file mode 100644 index 000000000..62835ebbd --- /dev/null +++ b/examples/pos/multi_arity_lambda_case.check @@ -0,0 +1,2 @@ +Case III +OK \ No newline at end of file diff --git a/examples/pos/multi_arity_lambda_case.effekt b/examples/pos/multi_arity_lambda_case.effekt new file mode 100644 index 000000000..c3d33d304 --- /dev/null +++ b/examples/pos/multi_arity_lambda_case.effekt @@ -0,0 +1,16 @@ +def foo[A, B, C](a: A, b: B){ fn: (A, B) => C }: C = { + fn(a, b) +} + +def main() = { + foo(12, true){ + case i, false => println("Case I") + case 0, true => println("Case II") + case _, true => println("Case III") + } + foo((1,2), [1,2,3]){ + case (x, y), Cons(1,Cons(2,Cons(3,Nil()))) => println("OK") + case (_, _), _ => println("ERR") + } + () +} \ No newline at end of file From 4b8434eb5271ed2b760cc686a70fab02a1943431 Mon Sep 17 00:00:00 2001 From: Marvin Date: Fri, 28 Mar 2025 20:11:47 +0100 Subject: [PATCH 17/41] Fix failing tests without optimization (#890) This is a *continuation* of #851. We aim to fix the previously ignored tests that currently fail when being run without optimization. - [x] permute.effekt: segfault in resume->uniqueStack->copyStack since resume is called with an erased resumption stack - apparently from growing the stack via checkLimit - fixed e.g. by initial size = `shl 1, 8` instead of 7 - [x] multiple declarations (in JS) - `ascii_isalphanumeric.effekt`, `ascii_iswhitespace.effekt`, `parser.effekt`, `probabilistic.effekt` - fixed by "Do not contify under reset" (found out via bisect) - is still a problem though - [x] missing block info - [x] generator.effekt: by noting parameters for regions - [x] regions.effekt - [x] selfregion.effekt - [x] typeparametric.effekt: by returning garbage value (`undef`) - [x] issue842.effekt: by #872 - [x] issue861.effekt: by #872 - [x] top-level object definititions - if_control_effect.effekt, toplevel_objects.effekt, type_omission_op.effekt, higherorderobject.effekt, res_obj_boxed.effekt, effectfulobject.effekt --------- Co-authored-by: Philipp Schuster --- .../test/scala/effekt/JavaScriptTests.scala | 10 +- .../jvm/src/test/scala/effekt/LLVMTests.scala | 35 +++-- .../src/test/scala/effekt/StdlibTests.scala | 23 ++-- .../test/scala/effekt/core/CoreTests.scala | 2 +- .../src/test/scala/effekt/core/VMTests.scala | 8 +- .../shared/src/main/scala/effekt/Typer.scala | 15 --- .../src/main/scala/effekt/core/vm/VM.scala | 5 - .../effekt/generator/chez/Transformer.scala | 3 - .../effekt/generator/js/TransformerCps.scala | 7 +- .../effekt/generator/llvm/Transformer.scala | 2 - .../scala/effekt/machine/Transformer.scala | 61 +++++---- .../main/scala/effekt/symbols/builtins.scala | 16 +-- .../benchmarks/are_we_fast_yet/permute.effekt | 7 +- .../benchmarks/are_we_fast_yet/storage.effekt | 7 +- .../pos/bidirectional/typeparametric.effekt | 1 + examples/pos/capture/selfregion.effekt | 4 +- examples/stdlib/queue.check | 12 ++ examples/stdlib/queue.effekt | 3 + examples/tour/regions.effekt.md | 13 +- libraries/common/buffer.effekt | 65 +++++---- libraries/common/queue.effekt | 124 +++++++++--------- libraries/js/effekt_runtime.js | 10 -- libraries/llvm/rts.ll | 32 +---- 23 files changed, 203 insertions(+), 262 deletions(-) create mode 100644 examples/stdlib/queue.check create mode 100644 examples/stdlib/queue.effekt diff --git a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala index 262e89afc..de90df3b4 100644 --- a/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala +++ b/effekt/jvm/src/test/scala/effekt/JavaScriptTests.scala @@ -26,14 +26,12 @@ class JavaScriptTests extends EffektTests { override lazy val withoutOptimizations: List[File] = List( // contifying under reset - //examplesDir / "pos" / "issue842.effekt", - //examplesDir / "pos" / "issue861.effekt", + examplesDir / "pos" / "issue842.effekt", + examplesDir / "pos" / "issue861.effekt", // syntax error (multiple declaration) - //examplesDir / "char" / "ascii_isalphanumeric.effekt", - //examplesDir / "char" / "ascii_iswhitespace.effekt", - //examplesDir / "pos" / "parser.effekt", - //examplesDir / "pos" / "probabilistic.effekt", + examplesDir / "pos" / "parser.effekt", + examplesDir / "pos" / "probabilistic.effekt", ) override def ignored: List[File] = List( diff --git a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala index e0b1faca5..6a1c55b2d 100644 --- a/effekt/jvm/src/test/scala/effekt/LLVMTests.scala +++ b/effekt/jvm/src/test/scala/effekt/LLVMTests.scala @@ -55,28 +55,23 @@ class LLVMTests extends EffektTests { override lazy val withoutOptimizations: List[File] = List( // contifying under reset - //examplesDir / "pos" / "issue842.effekt", - //examplesDir / "pos" / "issue861.effekt", + examplesDir / "pos" / "issue842.effekt", + examplesDir / "pos" / "issue861.effekt", + + examplesDir / "pos" / "capture" / "regions.effekt", + examplesDir / "pos" / "capture" / "selfregion.effekt", + examplesDir / "benchmarks" / "other" / "generator.effekt", + examplesDir / "pos" / "bidirectional" / "typeparametric.effekt", + examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt", + examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt", // top-level object definition - //examplesDir / "pos" / "object" / "if_control_effect.effekt", - //examplesDir / "pos" / "lambdas" / "toplevel_objects.effekt", - //examplesDir / "pos" / "type_omission_op.effekt", - //examplesDir / "pos" / "bidirectional" / "higherorderobject.effekt", - //examplesDir / "pos" / "bidirectional" / "res_obj_boxed.effekt", - //examplesDir / "pos" / "bidirectional" / "effectfulobject.effekt", - - // no block info - //examplesDir / "pos" / "capture" / "regions.effekt", - //examplesDir / "pos" / "capture" / "selfregion.effekt", - //examplesDir / "benchmarks" / "other" / "generator.effekt", - - // hole - //examplesDir / "pos" / "bidirectional" / "typeparametric.effekt", - - // segfault - //examplesDir / "benchmarks" / "are_we_fast_yet" / "permute.effekt", - //examplesDir / "benchmarks" / "are_we_fast_yet" / "storage.effekt", + examplesDir / "pos" / "object" / "if_control_effect.effekt", + examplesDir / "pos" / "lambdas" / "toplevel_objects.effekt", + examplesDir / "pos" / "type_omission_op.effekt", + examplesDir / "pos" / "bidirectional" / "higherorderobject.effekt", + examplesDir / "pos" / "bidirectional" / "res_obj_boxed.effekt", + examplesDir / "pos" / "bidirectional" / "effectfulobject.effekt", ) override lazy val ignored: List[File] = missingFeatures ++ noValgrind(examplesDir) diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index 6922267a7..b053c633f 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -21,19 +21,16 @@ class StdlibJavaScriptTests extends StdlibTests { override def withoutOptimizations: List[File] = List( examplesDir / "stdlib" / "acme.effekt", - - //examplesDir / "stdlib" / "json.effekt", - //examplesDir / "stdlib" / "exception" / "combinators.effekt", - - // reference error (k is not defined) - //examplesDir / "stdlib" / "stream" / "fibonacci.effekt", - //examplesDir / "stdlib" / "list" / "flatmap.effekt", - //examplesDir / "stdlib" / "list" / "sortBy.effekt", - //examplesDir / "stdlib" / "stream" / "zip.effekt", - //examplesDir / "stdlib" / "stream" / "characters.effekt", - - // oom - //examplesDir / "stdlib" / "list" / "deleteat.effekt", + examplesDir / "stdlib" / "json.effekt", + examplesDir / "stdlib" / "exception" / "combinators.effekt", + examplesDir / "stdlib" / "stream" / "fibonacci.effekt", + examplesDir / "stdlib" / "list" / "flatmap.effekt", + examplesDir / "stdlib" / "list" / "sortBy.effekt", + examplesDir / "stdlib" / "stream" / "zip.effekt", + examplesDir / "stdlib" / "stream" / "characters.effekt", + examplesDir / "stdlib" / "list" / "deleteat.effekt", + examplesDir / "stdlib" / "char" / "ascii_isalphanumeric.effekt", + examplesDir / "stdlib" / "char" / "ascii_iswhitespace.effekt", ) override def ignored: List[File] = List() diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index 192652eda..f2132c35a 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -10,7 +10,7 @@ import effekt.PhaseResult.CoreTransformed */ trait CoreTests extends munit.FunSuite { - protected def defaultNames = symbols.builtins.rootTypes ++ symbols.builtins.rootTerms ++ symbols.builtins.rootCaptures + protected def defaultNames: Map[String, _root_.effekt.symbols.Symbol] = symbols.builtins.rootTypes ++ symbols.builtins.rootCaptures def shouldBeEqual(obtained: ModuleDecl, expected: ModuleDecl, clue: => Any)(using Location) = assertEquals(obtained, expected, { diff --git a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala index 575ee288e..0b6a26a9b 100644 --- a/effekt/jvm/src/test/scala/effekt/core/VMTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/VMTests.scala @@ -357,8 +357,8 @@ class VMTests extends munit.FunSuite { poppedFrames = 8661, allocations = 0, closures = 0, - variableReads = 32437, - variableWrites = 13699, + variableReads = 23776, + variableWrites = 5039, resets = 0, shifts = 0, resumes = 0 @@ -373,8 +373,8 @@ class VMTests extends munit.FunSuite { poppedFrames = 5462, allocations = 5461, closures = 0, - variableReads = 13654, - variableWrites = 9557, + variableReads = 8192, + variableWrites = 4096, resets = 0, shifts = 0, resumes = 0 diff --git a/effekt/shared/src/main/scala/effekt/Typer.scala b/effekt/shared/src/main/scala/effekt/Typer.scala index aaecfb21a..70fb51521 100644 --- a/effekt/shared/src/main/scala/effekt/Typer.scala +++ b/effekt/shared/src/main/scala/effekt/Typer.scala @@ -54,21 +54,6 @@ object Typer extends Phase[NameResolved, Typechecked] { Context in { Context.withUnificationScope { flowingInto(builtins.toplevelCaptures) { - // bring builtins into scope - builtins.rootTerms.values.foreach { - case term: BlockParam => - Context.bind(term, term.tpe.getOrElse { - INTERNAL_ERROR("Builtins should always be annotated with their types.") - }) - Context.bind(term, CaptureSet(term.capture)) - case term: ExternResource => - Context.bind(term, term.tpe) - Context.bind(term, CaptureSet(term.capture)) - case term: Callable => - Context.bind(term, term.toType) - case term => Context.panic(s"Cannot bind builtin term: ${term}") - } - // We split the type-checking of definitions into "pre-check" and "check" // to allow mutually recursive defs tree.defs.foreach { d => precheckDef(d) } 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 1adfa2758..31682d487 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -331,11 +331,6 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { // TODO make the type of Region more precise... case Stmt.Region(_) => ??? - case Stmt.Alloc(id, init, region, body) if region == symbols.builtins.globalRegion => - val value = eval(init, env) - val address = freshAddress() - State.Step(body, env.bind(id, Computation.Reference(address)), stack, heap.updated(address, value)) - case Stmt.Alloc(id, init, region, body) => val value = eval(init, env) 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 458723355..90882531d 100644 --- a/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/chez/Transformer.scala @@ -95,9 +95,6 @@ trait Transformer { case Var(ref, init, capt, body) => state(nameDef(ref), toChez(init), toChez(body)) - case Alloc(id, init, region, body) if region == symbols.builtins.globalRegion => - chez.Let(List(Binding(nameDef(id), chez.Builtin("box", toChez(init)))), toChez(body)) - case Alloc(id, init, region, body) => chez.Let(List(Binding(nameDef(id), chez.Builtin("fresh", chez.Variable(nameRef(region)), toChez(init)))), toChez(body)) 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 69bfb8dd5..db67fabf7 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala @@ -70,12 +70,7 @@ object TransformerCps extends Transformer { val jsDecls = module.declarations.flatMap(toJS) val stmts = module.definitions.map(toJS) - val state = js.Const( - nameDef(symbols.builtins.globalRegion), - js.Variable(JSName("global")) - ) :: Nil - - js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ state ++ stmts) + js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ stmts) } def compileLSP(input: cps.ModuleDecl, coreModule: core.ModuleDecl)(using C: Context): List[js.Stmt] = diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala index b16c5f521..fcbb1937d 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala @@ -378,8 +378,6 @@ object Transformer { def transform(value: machine.Variable)(using FunctionContext): Operand = substitute(value) match { - // TODO rethink existence of global - case machine.Variable("global", machine.Type.Prompt()) => ConstantGlobal("global") case machine.Variable(name, tpe) => LocalReference(transform(tpe), name) } diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 97682de91..96c4ad1bf 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -37,8 +37,11 @@ object Transformer { Definition(Label(transform(id), vparams.map(transform) ++ bparams.map(transform)), transform(body)) case core.Toplevel.Val(id, tpe, binding) => Definition(BC.globals(id), transform(binding)) + case core.Toplevel.Def(id, block @ core.New(impl)) => + val variable = Variable(freshName("returned"), transform(block.tpe)) + Definition(BC.globals(id), New(variable, transform(impl), Return(List(variable)))) case d => - ErrorReporter.abort(s"Toplevel object definitions not yet supported: ${d}") + ErrorReporter.abort(s"Other toplevel definitions not yet supported: ${d}") } val localDefinitions = BC.definitions @@ -89,9 +92,7 @@ object Transformer { // Regions are blocks and can be free, but do not have info. case core.Variable.Block(id, core.Type.TRegion, capt) => - if id == symbols.builtins.globalRegion - then Set.empty - else Set(Variable(transform(id), Type.Prompt())) + Set(Variable(transform(id), Type.Prompt())) case core.Variable.Block(pid, tpe, capt) if pid != id => BPC.info.get(pid) match { // For each known free block we have to add its free variables to this one (flat closure) @@ -120,13 +121,16 @@ object Transformer { noteParameter(id, block.tpe) New(Variable(transform(id), transform(impl.interface)), transform(impl), transform(rest)) - case core.Def(id, block @ core.BlockVar(alias, tpe, _), rest) => - getDefinition(alias) match { + case core.Def(id, core.BlockVar(other, tpe, capt), rest) => + getBlockInfo(other) match { case BlockInfo.Definition(free, params) => noteDefinition(id, free, params) + emitDefinition(transformLabel(id), Jump(transformLabel(other))) + transform(rest) + case BlockInfo.Parameter(_) => + noteParameter(id, tpe) + Substitute(List(Variable(transform(id), transform(tpe)) -> Variable(transform(other), transform(tpe))), transform(rest)) } - emitDefinition(transformLabel(id), Jump(transformLabel(alias))) - transform(rest) case core.Def(id, block @ core.Unbox(pure), rest) => noteParameter(id, block.tpe) @@ -190,6 +194,10 @@ object Transformer { val opTag = DeclarationContext.getPropertyTag(method) transform(vargs, bargs).run { (values, blocks) => callee match { + case Block.BlockVar(id, tpe, capt) if BPC.globals contains id => + val variable = Variable(freshName("receiver"), transform(tpe)) + PushFrame(Clause(List(variable), Invoke(variable, opTag, values ++ blocks)), Jump(BPC.globals(id))) + case Block.BlockVar(id, tpe, capt) => Invoke(Variable(transform(id), transform(tpe)), opTag, values ++ blocks) @@ -246,6 +254,8 @@ object Transformer { Resume(Variable(transform(k.id), Type.Stack()), transform(body)) case core.Region(core.BlockLit(tparams, cparams, vparams, List(region), body)) => + noteParameters(List(region)) + val variable = Variable(freshName("returned"), transform(body.tpe)) val returnClause = Clause(List(variable), Return(List(variable))) val prompt = transform(region) @@ -254,24 +264,13 @@ object Transformer { case core.Alloc(id, init, region, body) => transform(init).run { value => - val tpe = value.tpe; - val name = transform(id) - val variable = Variable(name, tpe) - val reference = Variable(transform(id), Type.Reference(tpe)) + val reference = Variable(transform(id), Type.Reference(value.tpe)) val prompt = Variable(transform(region), Type.Prompt()) val temporary = Variable(freshName("temporaryStack"), Type.Stack()) - region match { - case symbols.builtins.globalRegion => - val globalPrompt = Variable("global", Type.Prompt()) - Shift(temporary, globalPrompt, - Var(reference, value, Type.Positive(), - Resume(temporary, transform(body)))) - case _ => - Shift(temporary, prompt, - Var(reference, value, Type.Positive(), - Resume(temporary, transform(body)))) - } + Shift(temporary, prompt, + Var(reference, value, Type.Positive(), + Resume(temporary, transform(body)))) } case core.Var(ref, init, capture, body) => @@ -314,6 +313,11 @@ object Transformer { } yield (values, blocks) def transformBlockArg(block: core.Block)(using BPC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Binding[Variable] = block match { + case core.BlockVar(id, tpe, _) if BPC.globals contains id => + val variable = Variable(transform(id), transform(tpe)) + Binding { k => + PushFrame(Clause(List(variable), k(variable)), Jump(BPC.globals(id))) + } case core.BlockVar(id, tpe, capt) => getBlockInfo(id) match { case BlockInfo.Definition(_, parameters) => // Passing a top-level function directly, so we need to eta-expand turning it into a closure @@ -488,8 +492,9 @@ object Transformer { case core.BlockType.Interface(symbol, targs) => Negative() } - def transformLabel(id: Id)(using BPC: BlocksParamsContext): Label = getDefinition(id) match { + def transformLabel(id: Id)(using BPC: BlocksParamsContext): Label = getBlockInfo(id) match { case BlockInfo.Definition(freeParams, boundParams) => Label(transform(id), boundParams ++ freeParams) + case BlockInfo.Parameter(_) => sys error s"Expected a function definition, but got a block parameter: ${id}" } def transform(id: Id): String = @@ -514,6 +519,9 @@ object Transformer { case Toplevel.Val(id, tpe, binding) => noteDefinition(id, Nil, Nil) noteGlobal(id) + case Toplevel.Def(id, core.New(impl)) => + noteDefinition(id, Nil, Nil) + noteGlobal(id) case other => () } @@ -556,11 +564,6 @@ object Transformer { def getBlockInfo(id: Id)(using BPC: BlocksParamsContext): BlockInfo = BPC.info.getOrElse(id, sys error s"No block info for ${util.show(id)}") - def getDefinition(id: Id)(using BPC: BlocksParamsContext): BlockInfo.Definition = getBlockInfo(id) match { - case d : BlockInfo.Definition => d - case BlockInfo.Parameter(tpe) => sys error s"Expected a function getDefinition, but got a block parameter: ${id}" - } - case class Binding[A](run: (A => Statement) => Statement) { def flatMap[B](rest: A => Binding[B]): Binding[B] = { Binding(k => run(a => rest(a).run(k))) diff --git a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala index d3b33b553..c6e2d3813 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/builtins.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/builtins.scala @@ -53,6 +53,9 @@ object builtins { val AsyncSymbol = Interface(Name.local("Async"), Nil, Nil) val AsyncCapability = ExternResource(name("async"), InterfaceType(AsyncSymbol, Nil)) + val GlobalSymbol = Interface(Name.local("Global"), Nil, Nil) + val GlobalCapability = ExternResource(name("global"), InterfaceType(GlobalSymbol, Nil)) + object TState { val S: TypeParam = TypeParam(Name.local("S")) val interface: Interface = Interface(Name.local("Ref"), List(S), Nil) @@ -86,23 +89,16 @@ object builtins { "Region" -> RegionSymbol ) - lazy val globalRegion = ExternResource(name("global"), TRegion) - - val rootTerms: Map[String, TermSymbol] = Map( - "global" -> globalRegion - ) - val rootCaptures: Map[String, Capture] = Map( "io" -> IOCapability.capture, "async" -> AsyncCapability.capture, - "global" -> globalRegion.capture + "global" -> GlobalCapability.capture ) // captures which are allowed on the toplevel - val toplevelCaptures: CaptureSet = CaptureSet() // CaptureSet(IOCapability.capture, globalRegion.capture) + val toplevelCaptures: CaptureSet = CaptureSet() // CaptureSet(IOCapability.capture, GlobalCapability.capture) lazy val rootBindings: Bindings = - Bindings(rootTerms.map { case (k, v) => (k, Set(v)) }, rootTypes, rootCaptures, - Map("effekt" -> Bindings(rootTerms.map { case (k, v) => (k, Set(v)) }, rootTypes, rootCaptures, Map.empty))) + Bindings(Map.empty, rootTypes, rootCaptures, Map("effekt" -> Bindings(Map.empty, rootTypes, rootCaptures, Map.empty))) } diff --git a/examples/benchmarks/are_we_fast_yet/permute.effekt b/examples/benchmarks/are_we_fast_yet/permute.effekt index 4741322dc..04214d496 100644 --- a/examples/benchmarks/are_we_fast_yet/permute.effekt +++ b/examples/benchmarks/are_we_fast_yet/permute.effekt @@ -1,5 +1,6 @@ import examples/benchmarks/runner +import ref import array def swap(arr: Array[Int], i: Int, j: Int) = { @@ -9,10 +10,10 @@ def swap(arr: Array[Int], i: Int, j: Int) = { } def run(n: Int) = { - var count in global = 0; + val count = ref(0); def permute(arr: Array[Int], n: Int): Unit = { - count = count + 1; + count.set(count.get + 1); if (n != 0) { val n1 = n - 1; permute(arr, n1); @@ -28,7 +29,7 @@ def run(n: Int) = { array(n, 1).permute(n); - count + count.get } def main() = benchmark(6){run} diff --git a/examples/benchmarks/are_we_fast_yet/storage.effekt b/examples/benchmarks/are_we_fast_yet/storage.effekt index 17ab62fb2..315a83db7 100644 --- a/examples/benchmarks/are_we_fast_yet/storage.effekt +++ b/examples/benchmarks/are_we_fast_yet/storage.effekt @@ -1,5 +1,6 @@ import examples/benchmarks/runner +import ref import array type Tree { @@ -25,10 +26,10 @@ def withRandom[R]{ program: { Random } => R }: R = { } def run(n: Int) = { - var count in global = 0; + val count = ref(0); def buildTreeDepth(depth: Int) { rand: Random }: Tree = { - count = count + 1; + count.set(count.get + 1); if (depth == 1) { Leaf(allocate(mod(rand.next, 10) + 1)) } else { @@ -44,7 +45,7 @@ def run(n: Int) = { withRandom { { rand: Random } => buildTreeDepth(7) {rand}; () } } - count + count.get } def main() = benchmark(1){run} diff --git a/examples/pos/bidirectional/typeparametric.effekt b/examples/pos/bidirectional/typeparametric.effekt index 61b1e9ed5..f9eaf2da9 100644 --- a/examples/pos/bidirectional/typeparametric.effekt +++ b/examples/pos/bidirectional/typeparametric.effekt @@ -3,6 +3,7 @@ extern interface Cap[U, V] extern pure def cap[U, V](): Cap[U, V] at {} = js "42" chez "42" + llvm "ret %Pos undef" interface Foo[S] { def op[A]() {f: Cap[S, A]}: Cap[S, A] at {f} / { Exception[S] } diff --git a/examples/pos/capture/selfregion.effekt b/examples/pos/capture/selfregion.effekt index 165d574b5..b2e14bfb3 100644 --- a/examples/pos/capture/selfregion.effekt +++ b/examples/pos/capture/selfregion.effekt @@ -46,8 +46,8 @@ def ex5() = def ex6() = { - var x in global = 42; - println(x + x) + val x = ref(42); + println(x.get + x.get) } def main() = { diff --git a/examples/stdlib/queue.check b/examples/stdlib/queue.check new file mode 100644 index 000000000..e983f7fb2 --- /dev/null +++ b/examples/stdlib/queue.check @@ -0,0 +1,12 @@ +true +false +Some(3) +Some(1) +Some(2) +Some(4) +Some(5) +Some(6) +Some(7) +Some(8) +Some(9) +None() diff --git a/examples/stdlib/queue.effekt b/examples/stdlib/queue.effekt new file mode 100644 index 000000000..a0069a115 --- /dev/null +++ b/examples/stdlib/queue.effekt @@ -0,0 +1,3 @@ +import queue + +def main() = queue::examples::main() diff --git a/examples/tour/regions.effekt.md b/examples/tour/regions.effekt.md index a0315c4f5..90d4548da 100644 --- a/examples/tour/regions.effekt.md +++ b/examples/tour/regions.effekt.md @@ -117,19 +117,22 @@ Running it will give us the same result: example1Region() ``` -## Global +## Global Mutable State -It is also possible to allocate a variable globally by allocating it into the built-in region `global`. With this, it is possible to write a program which is normally not possible: +It is also possible to allocate a variable globally by using the reference module `ref`. With this, it is possible to write a program which is normally not possible: ``` +import ref + def example5() = { - var x in global = 1 - val closure = box { () => x } + val x = ref(1) + val closure = box { () => x.get } closure } ``` -We can return a closure that closes over a variable. This is only possible because `x` is allocated into the `global` region and therefore has a static lifetime. +We can return a closure that closes over a mutable reference. This is only possible because `x` is allocated on the heap and subject to garbage collection. Note that in most cases it does not make sense to define mutable references using `var`. + ## References diff --git a/libraries/common/buffer.effekt b/libraries/common/buffer.effekt index 2c9abab28..6002bc2c0 100644 --- a/libraries/common/buffer.effekt +++ b/libraries/common/buffer.effekt @@ -3,7 +3,6 @@ module buffer import array // TODO (in Effekt compiler) -// - [ ] fix allocating into actually global region // - [ ] fix exceptions on objects // - [X] allow omitting braces after `at` for singleton regions @@ -35,12 +34,12 @@ def emptyBuffer[T](capacity: Int): Buffer[T] at {global} = { def arrayBuffer[T](initialCapacity: Int): Buffer[T] at {global} = { // TODO allocate buffer (and array) into a region r. val contents = array::allocate[T](initialCapacity) - var head in global = 0 - var tail in global = 0 + val head = ref(0) + val tail = ref(0) def size(): Int = - if (tail >= head) { tail - head } - else { initialCapacity - head + tail } + if (tail.get >= head.get) { tail.get - head.get } + else { initialCapacity - head.get + tail.get } def capacity(): Int = initialCapacity - size() @@ -51,36 +50,36 @@ def arrayBuffer[T](initialCapacity: Int): Buffer[T] at {global} = { def read() = { if (buffer.empty?) None() else { - val result: T = contents.unsafeGet(head); - head = mod(head + 1, initialCapacity) + val result: T = contents.unsafeGet(head.get) + head.set(mod(head.get + 1, initialCapacity)) Some(result) } } def write(el: T) = { if (buffer.full?) <> // raise(BufferOverflow()) - contents.unsafeSet(tail, el) - tail = mod(tail + 1, initialCapacity) + contents.unsafeSet(tail.get, el) + tail.set(mod(tail.get + 1, initialCapacity)) } } buffer } def refBuffer[T](): Buffer[T] at {global} = { - var content: Option[T] in global = None() + val content = ref(None()) new Buffer[T] { - def capacity() = if (content.isEmpty) 1 else 0 - def full?() = content.isDefined - def empty?() = isEmpty(content) + def capacity() = if (content.get.isEmpty) 1 else 0 + def full?() = content.get.isDefined + def empty?() = isEmpty(content.get) def read() = { - val res = content - content = None() + val res = content.get + content.set(None()) res } - def write(el: T) = content match { + def write(el: T) = content.get match { case Some(v) => <> // do raise(BufferOverflow(), "Cannot read element from buffer") case None() => - content = Some(el) + content.set(Some(el)) } } } @@ -88,24 +87,24 @@ def refBuffer[T](): Buffer[T] at {global} = { namespace examples { def main() = ignore[BufferOverflow] { // Buffer with capacity 1 - def b = emptyBuffer[Int](1); - println(b.capacity); - println(b.full?); + def b = emptyBuffer[Int](1) + println(b.capacity) + println(b.full?) - b.write(17); - println(b.read()); + b.write(17) + println(b.read()) // buffer with capacity 3 - def ringbuffer = emptyBuffer[Int](3); - ringbuffer.write(1); - ringbuffer.write(2); - println(ringbuffer.read()); - ringbuffer.write(3); - println(ringbuffer.read()); - println(ringbuffer.read()); - ringbuffer.write(4); - ringbuffer.write(5); - println(ringbuffer.read()); - println(ringbuffer.read()); + def ringbuffer = emptyBuffer[Int](3) + ringbuffer.write(1) + ringbuffer.write(2) + println(ringbuffer.read()) + ringbuffer.write(3) + println(ringbuffer.read()) + println(ringbuffer.read()) + ringbuffer.write(4) + ringbuffer.write(5) + println(ringbuffer.read()) + println(ringbuffer.read()) } } diff --git a/libraries/common/queue.effekt b/libraries/common/queue.effekt index d01c28640..8fe07ee9a 100644 --- a/libraries/common/queue.effekt +++ b/libraries/common/queue.effekt @@ -25,82 +25,82 @@ def emptyQueue[T](): Queue[T] at {global} = def emptyQueue[T](initialCapacity: Int): Queue[T] at {global} = { - var contents in global = array[Option[T]](initialCapacity, None()) - var head in global = 0 - var tail in global = 0 - var size in global = 0 - var capacity in global = initialCapacity + val contents = ref(array[Option[T]](initialCapacity, None())) + val head = ref(0) + val tail = ref(0) + val size = ref(0) + val capacity = ref(initialCapacity) def remove(arr: Array[Option[T]], index: Int): Option[T] = { - with on[OutOfBounds].default { None() }; - val value = arr.get(index); - arr.set(index, None()); + with on[OutOfBounds].default { None() } + val value = arr.get(index) + arr.set(index, None()) value } def nonEmpty[T] { p: => Option[T] / Exception[OutOfBounds] }: Option[T] = - if (size <= 0) None() else on[OutOfBounds].default { None() } { p() } + if (size.get <= 0) None() else on[OutOfBounds].default { None() } { p() } // Exponential back-off def resizeTo(requiredSize: Int): Unit = - if (requiredSize <= capacity) () else { + if (requiredSize <= capacity.get) () else { with on[OutOfBounds].ignore // should not happen - val oldSize = capacity - val newSize = capacity * 2 - val oldContents = contents + val oldSize = capacity.get + val newSize = capacity.get * 2 + val oldContents = contents.get val newContents = array::allocate[Option[T]](newSize) - if (head < tail) { + if (head.get < tail.get) { // The queue does not wrap around; direct copy is possible. - copy(oldContents, head, newContents, 0, size) // changed tail to size - } else if (size > 0) { + copy(oldContents, head.get, newContents, 0, size.get) // changed tail to size + } else if (size.get > 0) { // The queue wraps around; copy in two segments. - copy(oldContents, head, newContents, 0, oldSize - head) // changed oldSize to oldSize - head - copy(oldContents, 0, newContents, oldSize - head, tail) // changed oldSize - head to oldSize - head + copy(oldContents, head.get, newContents, 0, oldSize - head.get) // changed oldSize to oldSize - head + copy(oldContents, 0, newContents, oldSize - head.get, tail.get) // changed oldSize - head to oldSize - head } - contents = newContents - capacity = newSize - head = 0 - tail = oldSize + contents.set(newContents) + capacity.set(newSize) + head.set(0) + tail.set(oldSize) } def queue = new Queue[T] { - def empty?() = size <= 0 + def empty?() = size.get <= 0 def popFront() = nonEmpty { - val result = contents.remove(head) - head = mod(head + 1, capacity) - size = size - 1 + val result = contents.get.remove(head.get) + head.set(mod(head.get + 1, capacity.get)) + size.set(size.get - 1) result } def popBack() = nonEmpty { - tail = mod(tail - 1 + capacity, capacity) - val result = contents.remove(tail) - size = size - 1 + tail.set(mod(tail.get - 1 + capacity.get, capacity.get)) + val result = contents.get.remove(tail.get) + size.set(size.get - 1) result } - def peekFront() = nonEmpty { contents.get(head) } + def peekFront() = nonEmpty { contents.get.get(head.get) } - def peekBack() = nonEmpty { contents.get(tail) } + def peekBack() = nonEmpty { contents.get.get(tail.get) } def pushFront(el: T) = { - resizeTo(size + 1); - head = mod(head - 1 + capacity, capacity); - size = size + 1; - contents.unsafeSet(head, Some(el)) + resizeTo(size.get + 1) + head.set(mod(head.get - 1 + capacity.get, capacity.get)) + size.set(size.get + 1) + contents.get.unsafeSet(head.get, Some(el)) } def pushBack(el: T) = { - resizeTo(size + 1); - contents.unsafeSet(tail, Some(el)); - size = size + 1; - tail = mod(tail + 1, capacity) + resizeTo(size.get + 1) + contents.get.unsafeSet(tail.get, Some(el)) + size.set(size.get + 1) + tail.set(mod(tail.get + 1, capacity.get)) } } queue @@ -109,30 +109,30 @@ def emptyQueue[T](initialCapacity: Int): Queue[T] at {global} = { namespace examples { def main() = { // queue with initial capacity 4 - def b = emptyQueue[Int](4); - println(b.empty?); - b.pushFront(1); - b.pushBack(2); - b.pushFront(3); - b.pushBack(4); + def b = emptyQueue[Int](4) + println(b.empty?) + b.pushFront(1) + b.pushBack(2) + b.pushFront(3) + b.pushBack(4) // this will cause resizing: - b.pushBack(5); - b.pushBack(6); - b.pushBack(7); - b.pushBack(8); + b.pushBack(5) + b.pushBack(6) + b.pushBack(7) + b.pushBack(8) // and again: - b.pushBack(9); - - println(b.empty?); - println(b.popFront()); // Some(3) - println(b.popFront()); // Some(1) - println(b.popFront()); // Some(2) - println(b.popFront()); // Some(4) - println(b.popFront()); // Some(5) - println(b.popFront()); // Some(6) - println(b.popFront()); // Some(7) - println(b.popFront()); // Some(8) - println(b.popFront()); // Some(9) - println(b.popFront()); // None() + b.pushBack(9) + + println(b.empty?) + println(b.popFront()) // Some(3) + println(b.popFront()) // Some(1) + println(b.popFront()) // Some(2) + println(b.popFront()) // Some(4) + println(b.popFront()) // Some(5) + println(b.popFront()) // Some(6) + println(b.popFront()) // Some(7) + println(b.popFront()) // Some(8) + println(b.popFront()) // Some(9) + println(b.popFront()) // None() } } diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index 57ccfc28d..bb149178d 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -40,16 +40,6 @@ function Arena() { return s } -const global = { - fresh: (v) => { - const r = { - value: v, - set: (v) => { r.value = v } - }; - return r - } -} - function snapshot(s) { const snap = { store: s, root: s.root, generation: s.generation } s.generation = s.generation + 1 diff --git a/libraries/llvm/rts.ll b/libraries/llvm/rts.ll index 52dbc9ed6..861665905 100644 --- a/libraries/llvm/rts.ll +++ b/libraries/llvm/rts.ll @@ -678,14 +678,7 @@ define private void @freeStack(%StackPointer %stackPointer) alwaysinline { ; RTS initialization define private tailcc void @topLevel(%Pos %val, %Stack %stack) { - %rest = call %Stack @underflowStack(%Stack %stack) - ; rest holds global variables - call void @resume_Pos(%Stack %rest, %Pos %val) - ret void -} - -define private tailcc void @globalsReturn(%Pos %val, %Stack %stack) { - %rest = call %Stack @underflowStack(%Stack %stack) + call %Stack @underflowStack(%Stack %stack) ret void } @@ -699,29 +692,8 @@ define private void @topLevelEraser(%Environment %environment) { ret void } -@global = private global { i64, %Stack } { i64 0, %Stack null } - define private %Stack @withEmptyStack() { - %globals = call %Stack @reset(%Stack null) - - %globalsStackPointer_pointer = getelementptr %StackValue, %Stack %globals, i64 0, i32 1 - %globalsStackPointer = load %StackPointer, ptr %globalsStackPointer_pointer, !alias.scope !11, !noalias !21 - - %returnAddressPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 0 - %sharerPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 1 - %eraserPointer.0 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 0, i32 2 - - store ptr @globalsReturn, ptr %returnAddressPointer.0, !alias.scope !12, !noalias !22 - store ptr @topLevelSharer, ptr %sharerPointer.0, !alias.scope !12, !noalias !22 - store ptr @topLevelEraser, ptr %eraserPointer.0, !alias.scope !12, !noalias !22 - - %globalsStackPointer_2 = getelementptr %FrameHeader, %StackPointer %globalsStackPointer, i64 1 - store %StackPointer %globalsStackPointer_2, ptr %globalsStackPointer_pointer, !alias.scope !11, !noalias !21 - - %stack = call %Stack @reset(%Stack %globals) - - %globalStack = getelementptr %PromptValue, %Prompt @global, i64 0, i32 1 - store %Stack %stack, ptr %globalStack, !alias.scope !13, !noalias !23 + %stack = call %Stack @reset(%Stack null) %stackStackPointer = getelementptr %StackValue, %Stack %stack, i64 0, i32 1 %stackPointer = load %StackPointer, ptr %stackStackPointer, !alias.scope !11, !noalias !21 From 739914caffe52e24ec5a1505beb612a3721da9d6 Mon Sep 17 00:00:00 2001 From: phischu Date: Fri, 28 Mar 2025 20:40:52 +0100 Subject: [PATCH 18/41] Go via trampoline in machine transformer (#917) Closes #855 --------- Co-authored-by: Marvin Borner --- .github/actions/run-effekt-tests/action.yml | 4 +- .../scala/effekt/machine/Transformer.scala | 50 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/.github/actions/run-effekt-tests/action.yml b/.github/actions/run-effekt-tests/action.yml index 0eff5407c..5fa78ad63 100644 --- a/.github/actions/run-effekt-tests/action.yml +++ b/.github/actions/run-effekt-tests/action.yml @@ -34,12 +34,12 @@ runs: timeout_minutes: ${{ inputs.retry-timeout }} max_attempts: ${{ inputs.retry-max-attempts }} retry_on: error - command: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt -J-Xss1G clean test + command: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt clean test new_command_on_retry: sbt testQuick - name: Run full test suite without retry if: ${{ inputs.full-test == 'true' && runner.os != 'Windows' && inputs.use-retry != 'true' }} - run: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt -J-Xss1G clean test + run: EFFEKT_VALGRIND=1 EFFEKT_DEBUG=1 sbt clean test shell: bash - name: Assemble fully optimized js file diff --git a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala index 96c4ad1bf..90751be3c 100644 --- a/effekt/shared/src/main/scala/effekt/machine/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/machine/Transformer.scala @@ -7,6 +7,7 @@ import effekt.symbols.{ Symbol, TermSymbol } import effekt.symbols.builtins.TState import effekt.util.messages.ErrorReporter import effekt.symbols.ErrorMessageInterpolator +import scala.annotation.tailrec object Transformer { @@ -315,7 +316,7 @@ object Transformer { def transformBlockArg(block: core.Block)(using BPC: BlocksParamsContext, DC: DeclarationContext, E: ErrorReporter): Binding[Variable] = block match { case core.BlockVar(id, tpe, _) if BPC.globals contains id => val variable = Variable(transform(id), transform(tpe)) - Binding { k => + shift { k => PushFrame(Clause(List(variable), k(variable)), Jump(BPC.globals(id))) } case core.BlockVar(id, tpe, capt) => getBlockInfo(id) match { @@ -323,7 +324,7 @@ object Transformer { // Passing a top-level function directly, so we need to eta-expand turning it into a closure // TODO cache the closure somehow to prevent it from being created on every call val variable = Variable(freshName(id.name.name ++ "$closure"), Negative()) - Binding { k => + shift { k => New(variable, List(Clause(parameters, // conceptually: Substitute(parameters zip parameters, Jump(...)) but the Substitute is a no-op here Jump(transformLabel(id)) @@ -337,13 +338,13 @@ object Transformer { noteParameters(bparams) val parameters = vparams.map(transform) ++ bparams.map(transform); val variable = Variable(freshName("blockLit"), Negative()) - Binding { k => + shift { k => New(variable, List(Clause(parameters, transform(body))), k(variable)) } case core.New(impl) => val variable = Variable(freshName("new"), Negative()) - Binding { k => + shift { k => New(variable, transform(impl), k(variable)) } @@ -355,7 +356,7 @@ object Transformer { case core.ValueVar(id, tpe) if BC.globals contains id => val variable = Variable(freshName("run"), transform(tpe)) - Binding { k => + shift { k => // TODO this might introduce too many pushes. PushFrame(Clause(List(variable), k(variable)), Substitute(Nil, Jump(BC.globals(id)))) @@ -366,38 +367,38 @@ object Transformer { case core.Literal((), _) => val variable = Variable(freshName("unitLiteral"), Positive()); - Binding { k => + shift { k => Construct(variable, builtins.Unit, List(), k(variable)) } case core.Literal(value: Long, _) => val variable = Variable(freshName("longLiteral"), Type.Int()); - Binding { k => + shift { k => LiteralInt(variable, value, k(variable)) } // for characters case core.Literal(value: Int, _) => val variable = Variable(freshName("intLiteral"), Type.Int()); - Binding { k => + shift { k => LiteralInt(variable, value, k(variable)) } case core.Literal(value: Boolean, _) => val variable = Variable(freshName("booleanLiteral"), Positive()) - Binding { k => + shift { k => Construct(variable, if (value) builtins.True else builtins.False, List(), k(variable)) } case core.Literal(v: Double, _) => val literal_binding = Variable(freshName("doubleLiteral"), Type.Double()); - Binding { k => + shift { k => LiteralDouble(literal_binding, v, k(literal_binding)) } case core.Literal(javastring: String, _) => val literal_binding = Variable(freshName("utf8StringLiteral"), builtins.StringType); - Binding { k => + shift { k => LiteralUTF8String(literal_binding, javastring.getBytes("utf-8"), k(literal_binding)) } @@ -406,7 +407,7 @@ object Transformer { val variable = Variable(freshName("pureApp"), transform(tpe.result)) transform(vargs, Nil).flatMap { (values, blocks) => - Binding { k => + shift { k => ForeignCall(variable, transform(blockName), values ++ blocks, k(variable)) } } @@ -416,7 +417,7 @@ object Transformer { val variable = Variable(freshName("pureApp"), transform(tpe.result)) transform(vargs, bargs).flatMap { (values, blocks) => - Binding { k => + shift { k => ForeignCall(variable, transform(blockName), values ++ blocks, k(variable)) } } @@ -428,14 +429,14 @@ object Transformer { val tag = DeclarationContext.getConstructorTag(constructor) transform(vargs, Nil).flatMap { (values, blocks) => - Binding { k => + shift { k => Construct(variable, tag, values ++ blocks, k(variable)) } } case core.Box(block, annot) => transformBlockArg(block).flatMap { unboxed => - Binding { k => + shift { k => val boxed = Variable(freshName(unboxed.name), Type.Positive()) ForeignCall(boxed, "box", List(unboxed), k(boxed)) } @@ -564,13 +565,28 @@ object Transformer { def getBlockInfo(id: Id)(using BPC: BlocksParamsContext): BlockInfo = BPC.info.getOrElse(id, sys error s"No block info for ${util.show(id)}") - case class Binding[A](run: (A => Statement) => Statement) { + def shift[A](body: (A => Statement) => Statement): Binding[A] = + Binding { k => Trampoline.Done(body { x => trampoline(k(x)) }) } + + case class Binding[A](body: (A => Trampoline[Statement]) => Trampoline[Statement]) { def flatMap[B](rest: A => Binding[B]): Binding[B] = { - Binding(k => run(a => rest(a).run(k))) + Binding(k => Trampoline.More { () => body(a => Trampoline.More { () => rest(a).body(k) }) }) } + def run(k: A => Statement): Statement = trampoline(body { x => Trampoline.Done(k(x)) }) def map[B](f: A => B): Binding[B] = flatMap { a => pure(f(a)) } } + enum Trampoline[A] { + case Done(value: A) + case More(thunk: () => Trampoline[A]) + } + + @tailrec + def trampoline[A](body: Trampoline[A]): A = body match { + case Trampoline.Done(value) => value + case Trampoline.More(thunk) => trampoline(thunk()) + } + def traverse[S, T](l: List[S])(f: S => Binding[T]): Binding[List[T]] = l match { case Nil => pure(Nil) From 846059356d36e443f1a053921c0e0192eb7e93d4 Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:51:41 +0100 Subject: [PATCH 19/41] Fix: dot notation using block params (#918) Fixes #709 by changing `resolveOverloadedFunction` to also check `BlockParam`s in scope. Notably, we only look up the first matching symbol, as we do not (yet?) support overloading on block parameters. --- effekt/shared/src/main/scala/effekt/Namer.scala | 5 ++++- .../shared/src/main/scala/effekt/source/Tree.scala | 2 +- .../src/main/scala/effekt/symbols/Scope.scala | 7 +++++++ .../scala/effekt/typer/BoxUnboxInference.scala | 14 ++++---------- examples/pos/ufcs_blockparams.check | 1 + examples/pos/ufcs_blockparams.effekt | 6 ++++++ 6 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 examples/pos/ufcs_blockparams.check create mode 100644 examples/pos/ufcs_blockparams.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 1f1437ef3..61f3973e6 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -903,7 +903,10 @@ trait NamerOps extends ContextOps { Context: Context => val syms2 = if (syms.isEmpty) scope.lookupOperation(id.path, id.name) else syms - if (syms2.nonEmpty) { assignSymbol(id, CallTarget(syms2.asInstanceOf)); true } else { false } + // lookup first block param and do not collect multiple since we do not (yet?) permit overloading on block parameters + val syms3 = if (syms2.isEmpty) List(scope.lookupFirstBlockParam(id.path, id.name)) else syms2 + + if (syms3.nonEmpty) { assignSymbol(id, CallTarget(syms3.asInstanceOf)); true } else { false } } /** diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index 30e519832..f8ae8fafb 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -646,7 +646,7 @@ object Named { // CallLike case Do => symbols.Operation case Select => symbols.Field - case MethodCall => symbols.Operation | symbols.CallTarget + case MethodCall => symbols.Operation | symbols.CallTarget | symbols.BlockParam case IdTarget => symbols.TermSymbol // Others diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index d871c3539..a74c6006b 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -192,6 +192,13 @@ object scopes { namespace.terms.getOrElse(name, Set.empty).collect { case c: Callable if !c.isInstanceOf[Operation] => c } }.filter { namespace => namespace.nonEmpty } + def lookupFirstBlockParam(path: List[String], name: String)(using ErrorReporter): Set[BlockParam] = + first(path, scope) { namespace => + namespace.terms.get(name).map(set => + set.collect { case bp: BlockParam => bp } + ) + }.getOrElse(Set.empty) + // can be a term OR a type symbol def lookupFirst(path: List[String], name: String)(using E: ErrorReporter): Symbol = lookupFirstOption(path, name) getOrElse { E.abort(s"Could not resolve ${name}") } diff --git a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala index 518e505f8..f2c4b5e2e 100644 --- a/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala +++ b/effekt/shared/src/main/scala/effekt/typer/BoxUnboxInference.scala @@ -87,20 +87,14 @@ object BoxUnboxInference extends Phase[NameResolved, NameResolved] { val vargsTransformed = vargs.map(rewriteAsExpr) val bargsTransformed = bargs.map(rewriteAsBlock) - val syms = m.definition match { + val hasMethods = m.definition match { // an overloaded call target - case symbols.CallTarget(syms) => syms.flatten - case s => C.panic(s"Not a valid method or function: ${id.name}") - } - - val (funs, methods) = syms.partitionMap { - case t: symbols.Operation => Right(t) - case t: symbols.Callable => Left(t) - case t => C.abort(pp"Not a valid method or function: ${t}") + case symbols.CallTarget(syms) => syms.flatten.exists(b => b.isInstanceOf[symbols.Operation]) + case s => false } // we prefer methods over uniform call syntax - if (methods.nonEmpty) { + if (hasMethods) { MethodCall(rewriteAsBlock(receiver), id, targs, vargsTransformed, bargsTransformed) } else { Call(IdTarget(id).inheritPosition(id), targs, rewriteAsExpr(receiver) :: vargsTransformed, bargsTransformed) diff --git a/examples/pos/ufcs_blockparams.check b/examples/pos/ufcs_blockparams.check new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/examples/pos/ufcs_blockparams.check @@ -0,0 +1 @@ +true diff --git a/examples/pos/ufcs_blockparams.effekt b/examples/pos/ufcs_blockparams.effekt new file mode 100644 index 000000000..34b3156af --- /dev/null +++ b/examples/pos/ufcs_blockparams.effekt @@ -0,0 +1,6 @@ +def refl[A](a: A) { eq: (A, A) => Bool }: Bool = + a.eq(a) && eq(a, a) + +def main() = { + println(refl(42) { (x, y) => x == y }) +} From 895cafb60a9aa0641d947bc35d963fed601a2f80 Mon Sep 17 00:00:00 2001 From: "effekt-updater[bot]" <181701480+effekt-updater[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 03:54:06 +0000 Subject: [PATCH 20/41] Bump version to 0.25.0 --- package.json | 2 +- pom.xml | 2 +- project/EffektVersion.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6be421eb7..1747e956f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@effekt-lang/effekt", "author": "Jonathan Brachthäuser", - "version": "0.24.0", + "version": "0.25.0", "repository": { "type": "git", "url": "git+https://github.com/effekt-lang/effekt.git" diff --git a/pom.xml b/pom.xml index a84b1feb6..c05e87cef 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.bstudios Effekt Effekt - 0.24.0 + 0.25.0 4.0.0 diff --git a/project/EffektVersion.scala b/project/EffektVersion.scala index 6cb43b385..8d78673bf 100644 --- a/project/EffektVersion.scala +++ b/project/EffektVersion.scala @@ -1,4 +1,4 @@ // Don't change this file without changing the CI too! import sbt.* import sbt.Keys.* -object EffektVersion { lazy val effektVersion = "0.24.0" } +object EffektVersion { lazy val effektVersion = "0.25.0" } From 387b3e7d0ce52f91b33a0ff96f3849793c7f9ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 4 Apr 2025 16:44:31 +0200 Subject: [PATCH 21/41] CI: Fix npm provenance --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 61dc64947..087605f49 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,5 +1,8 @@ name: Release Artifacts +permissions: + id-token: write + on: push: tags: From 2a9a36cb24f4ded0981635682fb38e7644665a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 4 Apr 2025 16:49:44 +0200 Subject: [PATCH 22/41] CI: Add a way to publish manually to npm --- .github/workflows/manual-npm-publish.yml | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/manual-npm-publish.yml diff --git a/.github/workflows/manual-npm-publish.yml b/.github/workflows/manual-npm-publish.yml new file mode 100644 index 000000000..f636c815d --- /dev/null +++ b/.github/workflows/manual-npm-publish.yml @@ -0,0 +1,69 @@ +# Do not use this, please. This is just for manual NPM publishing in a time of great need. +name: Publish to NPM manually + +permissions: + id-token: write + +on: + workflow_dispatch: # For manual triggering + +jobs: + build-jar: + name: Build and assemble the Effekt compiler + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.VERSION }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'true' + + - uses: ./.github/actions/setup-effekt + + - name: Get the version + id: get_version + run: | + git fetch --tags + LATEST_TAG=$(git describe --tags --abbrev=0) + echo "VERSION=${LATEST_TAG#v}" >> $GITHUB_OUTPUT + + - name: Assemble jar file + run: sbt clean deploy + + - name: Generate npm package + run: mv $(npm pack) effekt.tgz + + - name: Upload Effekt binary + uses: actions/upload-artifact@v4 + with: + name: effekt + path: bin/effekt + + - name: Upload the npm package + uses: actions/upload-artifact@v4 + with: + name: effekt-npm-package + path: effekt.tgz + + publish-npm: + name: Publish NPM Package + runs-on: ubuntu-latest + needs: [build-jar] + steps: + - name: Download npm package + uses: actions/download-artifact@v4 + with: + name: effekt-npm-package + + - name: Set up NodeJS ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: 'https://registry.npmjs.org' + + - name: Publish to NPM as @effekt-lang/effekt + run: npm publish effekt.tgz --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 717940aaf0bde56117cb3110e1db8c9faf18eeef Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:58:13 +0200 Subject: [PATCH 23/41] Fix invalid links (#927) In conjunction with https://github.com/effekt-lang/effekt-website/pull/85, this fixes all invalid URLs and file paths. --- examples/casestudies/naturalisticdsls.effekt.md | 14 +++++++------- examples/tour/captures.effekt.md | 2 +- examples/tour/objects.effekt.md | 2 +- examples/tour/regions.effekt.md | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/casestudies/naturalisticdsls.effekt.md b/examples/casestudies/naturalisticdsls.effekt.md index 8bccf749e..3fd0bae90 100644 --- a/examples/casestudies/naturalisticdsls.effekt.md +++ b/examples/casestudies/naturalisticdsls.effekt.md @@ -234,11 +234,11 @@ def main() = { } ``` -[@hudak96building]: https://dl.acm.org/doi/10.1145/242224.242477 -[@lopes2003beyond]: https://dl.acm.org/doi/abs/10.1145/966051.966058 +[@hudak96building]: https://doi.org/10.1145/242224.242477 +[@lopes2003beyond]: https://doi.org/10.1145/966051.966058 [@marvsik2016introducing]: https://hal.archives-ouvertes.fr/hal-01079206/ -[@erdweg11sugarj]: https://dl.acm.org/doi/abs/10.1145/2048066.2048099 -[@shan2005linguistic]: http://homes.sice.indiana.edu/ccshan/dissertation/book.pdf -[@shan2004delimited]: https://arxiv.org/abs/cs/0404006 -[@barker2004continuations]: https://www.nyu.edu/projects/barker/barker-cw.pdf -[@yallop2017staged]: https://dl.acm.org/doi/abs/10.1145/2847538.2847546 \ No newline at end of file +[@erdweg11sugarj]: https://doi.org/10.1145/2048066.2048099 +[@shan2005linguistic]: https://homes.sice.indiana.edu/ccshan/dissertation/book.pdf +[@shan2004delimited]: https://doi.org/10.48550/arXiv.cs/0404006 +[@barker2014continuations]: https://archive.org/details/continuationsnat0000bark/ +[@yallop2017staged]: https://doi.org/10.1145/2847538.2847546 diff --git a/examples/tour/captures.effekt.md b/examples/tour/captures.effekt.md index 53b27309e..3ce9b5a73 100644 --- a/examples/tour/captures.effekt.md +++ b/examples/tour/captures.effekt.md @@ -131,4 +131,4 @@ An example of a builtin resource is `io`, which is handled by the runtime. Other ## References -- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://dl.acm.org/doi/10.1145/3527320) +- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://doi.org/10.1145/3527320) diff --git a/examples/tour/objects.effekt.md b/examples/tour/objects.effekt.md index 137e2bed0..6c452c50c 100644 --- a/examples/tour/objects.effekt.md +++ b/examples/tour/objects.effekt.md @@ -85,4 +85,4 @@ def counterAsEffect2() = { counterAsEffect2() ``` As part of its compilation pipeline, and guided by the type-and-effect system, -the Effekt compiler performs this translation from implicitly handled effects to explicitly passed capabilities [Brachthäuser et al., 2020](https://dl.acm.org/doi/10.1145/3428194). +the Effekt compiler performs this translation from implicitly handled effects to explicitly passed capabilities [Brachthäuser et al., 2020](https://doi.org/10.1145/3428194). diff --git a/examples/tour/regions.effekt.md b/examples/tour/regions.effekt.md index 90d4548da..3e209697b 100644 --- a/examples/tour/regions.effekt.md +++ b/examples/tour/regions.effekt.md @@ -136,5 +136,5 @@ We can return a closure that closes over a mutable reference. This is only possi ## References -- [Region-based Resource Management and Lexical Exception Handlers in Continuation-Passing Style](https://link.springer.com/chapter/10.1007/978-3-030-99336-8_18) -- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://dl.acm.org/doi/10.1145/3527320) +- [Region-based Resource Management and Lexical Exception Handlers in Continuation-Passing Style](https://doi.org/10.1007/978-3-030-99336-8_18) +- [Effects, capabilities, and boxes: from scope-based reasoning to type-based reasoning and back](https://doi.org/10.1145/3527320) From a8d3b937c2255f41593365a924f34018c4759ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattis=20B=C3=B6ckle?= Date: Thu, 10 Apr 2025 10:17:58 +0200 Subject: [PATCH 24/41] Reimplement C utilities in llvm (#910) Reimplement various C utilities in LLVM and inline the functions inside of common effekt. This should allow LLVM to better optimize code containing these funtions, as the optimizer can see the actual implementation instead of guessing. --- libraries/common/array.effekt | 74 +++++++++++++++++++++++++++---- libraries/common/bytearray.effekt | 48 ++++++++++++++++---- libraries/common/ref.effekt | 71 +++++++++++++++++++++++++---- libraries/llvm/array.c | 57 ------------------------ libraries/llvm/main.c | 2 - libraries/llvm/ref.c | 47 -------------------- 6 files changed, 168 insertions(+), 131 deletions(-) delete mode 100644 libraries/llvm/array.c delete mode 100644 libraries/llvm/ref.c diff --git a/libraries/common/array.effekt b/libraries/common/array.effekt index 5cc584902..af9a01c0f 100644 --- a/libraries/common/array.effekt +++ b/libraries/common/array.effekt @@ -8,14 +8,54 @@ import option /// A mutable 0-indexed fixed-sized array. extern type Array[T] +/** We represent arrays like positive types. + * The tag is 0 and the obj points to memory with the following layout: + * + * +--[ Header ]--+------+------------+ + * | Rc | Eraser | Size | Fields ... | + * +--------------+------+------------+ + */ + +extern llvm """ +declare noalias ptr @calloc(i64, i64) + +define void @array_erase_fields(ptr %array_pointer) { +entry: + %data_pointer = getelementptr inbounds i64, ptr %array_pointer, i64 1 + %size = load i64, ptr %array_pointer, align 8 + %size_eq_0 = icmp eq i64 %size, 0 + br i1 %size_eq_0, label %exit, label %loop +loop: + %loop_phi = phi i64 [ %inc, %loop ], [ 0, %entry ] + + %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 %loop_phi + %element = load %Pos, ptr %element_pointer + + call void @erasePositive(%Pos %element) + %inc = add nuw i64 %loop_phi, 1 + %cmp = icmp ult i64 %inc, %size + br i1 %cmp, label %loop, label %exit +exit: + ret void +} +""" + /// Allocates a new array of size `size`, keeping its values _undefined_. /// Prefer using `array` constructor instead to ensure that values are defined. extern global def allocate[T](size: Int): Array[T] = js "(new Array(${size}))" chez "(make-vector ${size})" // creates an array filled with 0s on CS llvm """ - %z = call %Pos @c_array_new(%Int ${size}) - ret %Pos %z + %size0 = shl i64 ${size}, 4 + %size1 = add i64 %size0, 24 + %calloc = tail call noalias ptr @calloc(i64 %size1, i64 1) + %eraser_pointer = getelementptr ptr, ptr %calloc, i64 1 + %size_pointer = getelementptr ptr, ptr %calloc, i64 2 + store i64 0, ptr %calloc + store ptr @array_erase_fields, ptr %eraser_pointer + store i64 ${size}, ptr %size_pointer + %ret_pos = insertvalue %Pos { i64 0, ptr poison }, ptr %calloc, 1 + ret %Pos %ret_pos """ vm "array::allocate(Int)" @@ -49,8 +89,12 @@ extern pure def size[T](arr: Array[T]): Int = js "${arr}.length" chez "(vector-length ${arr})" llvm """ - %z = call %Int @c_array_size(%Pos ${arr}) - ret %Int %z + %pos_data = extractvalue %Pos ${arr}, 1 + %size_pointer = getelementptr inbounds i64, ptr %pos_data, i64 2 + %size = load i64, ptr %size_pointer + + tail call void @erasePositive(%Pos ${arr}) + ret i64 %size """ vm "array::size[T](Array[T])" @@ -62,8 +106,15 @@ extern global def unsafeGet[T](arr: Array[T], index: Int): T = js "${arr}[${index}]" chez "(vector-ref ${arr} ${index})" llvm """ - %z = call %Pos @c_array_get(%Pos ${arr}, %Int ${index}) - ret %Pos %z + %pos_data = extractvalue %Pos ${arr}, 1 + + %data_pointer = getelementptr inbounds ptr, ptr %pos_data, i64 3 + %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 ${index} + %element = load %Pos, ptr %element_pointer + + tail call void @sharePositive(%Pos %element) + tail call void @erasePositive(%Pos ${arr}) + ret %Pos %element """ vm "array::unsafeGet[T](Array[T], Int)" @@ -82,8 +133,15 @@ extern global def unsafeSet[T](arr: Array[T], index: Int, value: T): Unit = js "array$set(${arr}, ${index}, ${value})" chez "(begin (vector-set! ${arr} ${index} ${value}) #f)" llvm """ - %z = call %Pos @c_array_set(%Pos ${arr}, %Int ${index}, %Pos ${value}) - ret %Pos %z + %array_data = extractvalue %Pos ${arr}, 1 + + %data_pointer = getelementptr inbounds i64, ptr %array_data, i64 3 + %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 ${index} + %element = load %Pos, ptr %element_pointer, align 8 + tail call void @erasePositive(%Pos %element) + store %Pos ${value}, ptr %element_pointer, align 8 + tail call void @erasePositive(%Pos ${arr}) + ret %Pos zeroinitializer """ vm "array::unsafeSet[T](Array[T], Int, T)" diff --git a/libraries/common/bytearray.effekt b/libraries/common/bytearray.effekt index 70c757ae2..c14a88f0d 100644 --- a/libraries/common/bytearray.effekt +++ b/libraries/common/bytearray.effekt @@ -2,6 +2,17 @@ module bytearray /** * A memory managed, mutable, fixed-length array of bytes. + * + * We represent bytearrays like positive types. + * + * - The field `tag` contains the size + * - The field `obj` points to memory with the following layout: + * + * +--[ Header ]--+--------------+ + * | Rc | Eraser | Contents ... | + * +--------------+--------------+ + * + * The eraser does nothing. */ extern type ByteArray // = llvm "%Pos" @@ -12,32 +23,47 @@ extern type ByteArray extern global def allocate(size: Int): ByteArray = js "(new Uint8Array(${size}))" llvm """ - %arr = call %Pos @c_bytearray_new(%Int ${size}) - ret %Pos %arr + %object_size = add i64 ${size}, 16 + %object_alloc = tail call noalias ptr @malloc(i64 noundef %object_size) + store i64 0, ptr %object_alloc, align 8 + %object_data_ptr = getelementptr inbounds i8, ptr %object_alloc, i64 8 + store ptr @bytearray_erase_noop, ptr %object_data_ptr, align 8 + %ret_object1 = insertvalue %Pos poison, i64 ${size}, 0 + %ret_object2 = insertvalue %Pos %ret_object1, ptr %object_alloc, 1 + ret %Pos %ret_object2 """ chez "(make-bytevector ${size})" extern pure def size(arr: ByteArray): Int = js "${arr}.length" llvm """ - %size = call %Int @c_bytearray_size(%Pos ${arr}) - ret %Int %size + %size = extractvalue %Pos ${arr}, 0 + tail call void @erasePositive(%Pos ${arr}) + ret i64 %size """ chez "(bytevector-length ${arr})" extern global def unsafeGet(arr: ByteArray, index: Int): Byte = js "(${arr})[${index}]" llvm """ - %byte = call %Byte @c_bytearray_get(%Pos ${arr}, %Int ${index}) - ret %Byte %byte + %arr_ptr = extractvalue %Pos ${arr}, 1 + %arr_data_ptr = getelementptr inbounds i8, ptr %arr_ptr, i64 16 + %element_ptr = getelementptr inbounds i8, ptr %arr_data_ptr, i64 ${index} + %element = load i8, ptr %element_ptr, align 1 + tail call void @erasePositive(%Pos ${arr}) + ret i8 %element """ chez "(bytevector-u8-ref ${arr} ${index})" extern global def unsafeSet(arr: ByteArray, index: Int, value: Byte): Unit = js "bytearray$set(${arr}, ${index}, ${value})" llvm """ - %z = call %Pos @c_bytearray_set(%Pos ${arr}, %Int ${index}, %Byte ${value}) - ret %Pos %z + %arr_ptr = extractvalue %Pos ${arr}, 1 + %arr_data_ptr = getelementptr inbounds i8, ptr %arr_ptr, i64 16 + %element_ptr = getelementptr inbounds i8, ptr %arr_data_ptr, i64 ${index} + store i8 ${value}, ptr %element_ptr, align 1 + tail call void @erasePositive(%Pos ${arr}) + ret %Pos zeroinitializer """ chez "(bytevector-u8-set! ${arr} ${index} ${value})" @@ -121,6 +147,12 @@ extern js """ } """ +extern llvm """ +define void @bytearray_erase_noop(ptr readnone %0) { + ret void +} +""" + extern chez """ (define (bytearray$compare b1 b2) (let ([len1 (bytevector-length b1)] diff --git a/libraries/common/ref.effekt b/libraries/common/ref.effekt index a38484966..de875dff5 100644 --- a/libraries/common/ref.effekt +++ b/libraries/common/ref.effekt @@ -9,17 +9,44 @@ extern js """ } """ +extern llvm """ +define void @c_ref_erase_field(ptr %0) { + %field = load %Pos, ptr %0, align 8 + tail call void @erasePositive(%Pos %field) + ret void +} +""" + /// Global, mutable references extern type Ref[T] +/** We represent references like positive types in LLVM + * The tag is 0 and the obj points to memory with the following layout: + * + * +--[ Header ]--------------+------------+ + * | ReferenceCount | Eraser | Field | + * +--------------------------+------------+ + */ + /// Allocates a new reference, keeping its value _undefined_. /// Prefer using `ref` constructor instead to ensure that the value is defined. extern global def allocate[T](): Ref[T] = js "{ value: undefined }" chez "(box #f)" llvm """ - %z = call %Pos @c_ref_fresh(%Pos zeroinitializer) - ret %Pos %z + ; sizeof Header + sizeof Pos = 32 + %ref = tail call noalias ptr @malloc(i64 noundef 32) + %refEraser = getelementptr ptr, ptr %ref, i64 1 + %fieldTag = getelementptr ptr, ptr %ref, i64 2 + %fieldData_pointer = getelementptr ptr, ptr %ref, i64 3 + + store i64 0, ptr %ref, align 8 + store ptr @c_ref_erase_field, ptr %refEraser, align 8 + store i64 0, ptr %fieldTag, align 8 + store ptr null, ptr %fieldData_pointer, align 8 + + %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1 + ret %Pos %refWrap """ /// Creates a new reference with the initial value `init`. @@ -27,8 +54,20 @@ extern global def ref[T](init: T): Ref[T] = js "{ value: ${init} }" chez "(box ${init})" llvm """ - %z = call %Pos @c_ref_fresh(%Pos ${init}) - ret %Pos %z + %initTag = extractvalue %Pos ${init}, 0 + %initObject_pointer = extractvalue %Pos ${init}, 1 + ; sizeof Header + sizeof Pos = 32 + %ref = tail call noalias ptr @malloc(i64 noundef 32) + %refEraser = getelementptr ptr, ptr %ref, i64 1 + %refField = getelementptr ptr, ptr %ref, i64 2 + + store i64 0, ptr %ref, align 8 + store ptr @c_ref_erase_field, ptr %refEraser, align 8 + store %Pos ${init}, ptr %refField, align 8 + + %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1 + + ret %Pos %refWrap """ vm "ref::ref[T](T)" @@ -36,9 +75,15 @@ extern global def ref[T](init: T): Ref[T] = extern global def get[T](ref: Ref[T]): T = js "${ref}.value" chez "(unbox ${ref})" - llvm """ - %z = call %Pos @c_ref_get(%Pos ${ref}) - ret %Pos %z +llvm """ + %ref_pointer = extractvalue %Pos ${ref}, 1 + %refField_pointer = getelementptr inbounds ptr, ptr %ref_pointer, i64 2 + %refField = load %Pos, ptr %refField_pointer, align 8 + + tail call void @sharePositive(%Pos %refField) + tail call void @erasePositive(%Pos ${ref}) + + ret %Pos %refField """ vm "ref::get[T](Ref[T])" @@ -47,7 +92,15 @@ extern global def set[T](ref: Ref[T], value: T): Unit = js "set$impl(${ref}, ${value})" chez "(set-box! ${ref} ${value})" llvm """ - %z = call %Pos @c_ref_set(%Pos ${ref}, %Pos ${value}) - ret %Pos %z + %refField_pointer = extractvalue %Pos ${ref}, 1 + + %field_pointer = getelementptr ptr, ptr %refField_pointer, i64 2 + %field = load %Pos, ptr %field_pointer, align 8 + + tail call void @erasePositive(%Pos %field) + store %Pos ${value}, ptr %field_pointer + tail call void @erasePositive(%Pos ${ref}) + + ret %Pos zeroinitializer """ vm "ref::set[T](Ref[T], T)" diff --git a/libraries/llvm/array.c b/libraries/llvm/array.c deleted file mode 100644 index df0950b82..000000000 --- a/libraries/llvm/array.c +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef EFFEKT_ARRAY_C -#define EFFEKT_ARRAY_C - -/** We represent arrays like positive types. - * The tag is 0 and the obj points to memory with the following layout: - * - * +--[ Header ]--+------+------------+ - * | Rc | Eraser | Size | Fields ... | - * +--------------+------+------------+ - */ - -void c_array_erase_fields(void *envPtr) { - uint64_t *sizePtr = envPtr; - struct Pos *dataPtr = envPtr + sizeof(uint64_t); - uint64_t size = *sizePtr; - for (uint64_t i = 0; i < size; i++) { - erasePositive(dataPtr[i]); - } -} - -struct Pos c_array_new(const Int size) { - void *objPtr = calloc(sizeof(struct Header) + sizeof(uint64_t) + size * sizeof(struct Pos), 1); - struct Header *headerPtr = objPtr; - uint64_t *sizePtr = objPtr + sizeof(struct Header); - *headerPtr = (struct Header) { .rc = 0, .eraser = c_array_erase_fields, }; - *sizePtr = size; - return (struct Pos) { - .tag = 0, - .obj = objPtr, - }; -} - -Int c_array_size(const struct Pos arr) { - uint64_t *sizePtr = arr.obj + sizeof(struct Header); - uint64_t size = *sizePtr; - erasePositive(arr); - return size; -} - -struct Pos c_array_get(const struct Pos arr, const Int index) { - struct Pos *dataPtr = arr.obj + sizeof(struct Header) + sizeof(uint64_t); - struct Pos element = dataPtr[index]; - sharePositive(element); - erasePositive(arr); - return element; -} - -struct Pos c_array_set(const struct Pos arr, const Int index, const struct Pos value) { - struct Pos *dataPtr = arr.obj + sizeof(struct Header) + sizeof(uint64_t); - struct Pos element = dataPtr[index]; - erasePositive(element); - dataPtr[index] = value; - erasePositive(arr); - return Unit; -} - -#endif diff --git a/libraries/llvm/main.c b/libraries/llvm/main.c index 2f54374c2..38c5f7133 100644 --- a/libraries/llvm/main.c +++ b/libraries/llvm/main.c @@ -13,8 +13,6 @@ #include "bytearray.c" #include "io.c" #include "panic.c" -#include "ref.c" -#include "array.c" extern void effektMain(); diff --git a/libraries/llvm/ref.c b/libraries/llvm/ref.c deleted file mode 100644 index 48620a42c..000000000 --- a/libraries/llvm/ref.c +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef EFFEKT_REF_C -#define EFFEKT_REF_C - -/** We represent references like positive types. - * The tag is 0 and the obj points to memory with the following layout: - * - * +--[ Header ]--------------+------------+ - * | ReferenceCount | Eraser | Field | - * +--------------------------+------------+ - */ - -void c_ref_erase_field(void *envPtr) { - struct Pos *fieldPtr = envPtr; - struct Pos element = *fieldPtr; - erasePositive(element); -} - -struct Pos c_ref_fresh(const struct Pos value) { - void *objPtr = malloc(sizeof(struct Header) + sizeof(struct Pos)); - struct Header *headerPtr = objPtr; - struct Pos *fieldPtr = objPtr + sizeof(struct Header); - *headerPtr = (struct Header) { .rc = 0, .eraser = c_ref_erase_field, }; - *fieldPtr = value; - return (struct Pos) { - .tag = 0, - .obj = objPtr, - }; -} - -struct Pos c_ref_get(const struct Pos ref) { - struct Pos *fieldPtr = ref.obj + sizeof(struct Header); - struct Pos element = *fieldPtr; - sharePositive(element); - erasePositive(ref); - return element; -} - -struct Pos c_ref_set(const struct Pos ref, const struct Pos value) { - struct Pos *fieldPtr = ref.obj + sizeof(struct Header); - struct Pos element = *fieldPtr; - erasePositive(element); - *fieldPtr = value; - erasePositive(ref); - return Unit; -} - -#endif From e417c0765d644ecac69033d6fc7a552f76dd33cd Mon Sep 17 00:00:00 2001 From: dvdvgt <40773635+dvdvgt@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:50:18 +0200 Subject: [PATCH 25/41] JSWeb: report all collected errors upon aborting (#931) Currently, when encountering an error (e.g. through calling `Context.abort`) in the JSWeb backend, all encountered errors are discarded and a JS exception is thrown. All information is lost there, which leads to a singular (unhelpful) error message `Cannot compile interactive.effekt`. It would be more helpful for the user to see the cause. https://github.com/effekt-lang/effekt/blob/8abbff5c34d71f8d3396c9db4b75ad6fdb16a4f5/effekt/js/src/main/scala/effekt/LanguageServer.scala#L101-L112 In line 103 this exception is thrown. Notice that the following line attempt to catch a `FatalPhaseError`, however `mainOutputPath` is a string and thus not lazily evaluated. Furthermore, `Phase.apply` already catches `FatalPhaseError`s and converts them to `None`. Hence, the exception handler is never executed. Instead, we report all collected error messages. Before: ![image](https://github.com/user-attachments/assets/26ee6ae5-c20f-4c16-ba7f-3f4b20058934) After: image (the increased resolution is not part of this PR) --- .../src/main/scala/effekt/LanguageServer.scala | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/effekt/js/src/main/scala/effekt/LanguageServer.scala b/effekt/js/src/main/scala/effekt/LanguageServer.scala index c724f6157..6c62143f2 100644 --- a/effekt/js/src/main/scala/effekt/LanguageServer.scala +++ b/effekt/js/src/main/scala/effekt/LanguageServer.scala @@ -98,18 +98,12 @@ class LanguageServer extends Intelligence { file(path).lastModified @JSExport - def compileFile(path: String): String = { - val mainOutputPath = compileCached(VirtualFileSource(path)).getOrElseAborting { - throw js.JavaScriptException(s"Cannot compile ${path}") + def compileFile(path: String): String = + compileCached(VirtualFileSource(path)).getOrElseAborting { + // report all collected error messages + val formattedErrors = context.messaging.formatMessages(context.messaging.get) + throw js.JavaScriptException(formattedErrors) } - try { - mainOutputPath - } catch { - case FatalPhaseError(msg) => - val formattedError = context.messaging.formatMessage(msg) - throw js.JavaScriptException(formattedError) - } - } @JSExport def showCore(path: String): String = { From 6d9b29b64f0b0b8cd72468b564f28f2f2e3dc6ef Mon Sep 17 00:00:00 2001 From: "effekt-updater[bot]" <181701480+effekt-updater[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 03:54:41 +0000 Subject: [PATCH 26/41] Bump version to 0.26.0 --- package.json | 2 +- pom.xml | 2 +- project/EffektVersion.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1747e956f..d989d3163 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@effekt-lang/effekt", "author": "Jonathan Brachthäuser", - "version": "0.25.0", + "version": "0.26.0", "repository": { "type": "git", "url": "git+https://github.com/effekt-lang/effekt.git" diff --git a/pom.xml b/pom.xml index c05e87cef..3298e3baf 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.bstudios Effekt Effekt - 0.25.0 + 0.26.0 4.0.0 diff --git a/project/EffektVersion.scala b/project/EffektVersion.scala index 8d78673bf..01eafd1e7 100644 --- a/project/EffektVersion.scala +++ b/project/EffektVersion.scala @@ -1,4 +1,4 @@ // Don't change this file without changing the CI too! import sbt.* import sbt.Keys.* -object EffektVersion { lazy val effektVersion = "0.25.0" } +object EffektVersion { lazy val effektVersion = "0.26.0" } From f669047755c976b0e4cecea71f3b048ee72ed0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 16 Apr 2025 17:42:22 +0200 Subject: [PATCH 27/41] Remove debug print in Normalizer (#937) :) --- .../shared/src/main/scala/effekt/core/optimizer/Normalizer.scala | 1 - 1 file changed, 1 deletion(-) 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 dceb07a8e..c4a870521 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala @@ -400,7 +400,6 @@ object Normalizer { normal => renamedIds.foreach(copyUsage) - val newUsage = usage.collect { case (id, usage) if util.show(id) contains "foreach" => (id, usage) } // (2) substitute val body = substitutions.substitute(renamedLit, targs, vargs, bvars) From 70fb2837f72fda0a68b7402f9d387da08819760c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 17 Apr 2025 12:16:30 +0200 Subject: [PATCH 28/41] Simplify core.Renamer to produce smaller symbols (#938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit General idea: instead of building `name_123_109238_190238_109283019283_1092830192838019283` chains, what if we use the automagic `fresh.next` to get just a darn fresh number? This will limit the identifiers to stuff like `b_k2_123_10928310283`, which is--at least to me--a pretty big improvement. We use the full capabilities of a `Symbol`: it has a `name` (which is really a `String`) and a fresh `id` that gets assigned lazily. Instead of changing the name to be `${oldName}_${oldId}` and then putting a fresh `id` on top of that (to get `${oldName}_${oldId}_${newId}`), we just make a new `Symbol` with the same name, which forces a new, _fresher_, `id`. --------- Co-authored-by: Marcial Gaißert --- .../test/scala/effekt/core/CoreTests.scala | 4 +- .../scala/effekt/core/OptimizerTests.scala | 2 +- .../test/scala/effekt/core/RenamerTests.scala | 112 +++-------- .../test/scala/effekt/core/TestRenamer.scala | 120 ++++++++++++ .../scala/effekt/core/TestRenamerTests.scala | 179 ++++++++++++++++++ .../src/main/scala/effekt/core/Renamer.scala | 11 +- .../src/main/scala/effekt/core/Tree.scala | 7 +- .../core/optimizer/StaticArguments.scala | 3 +- .../main/scala/effekt/symbols/Symbol.scala | 2 +- .../main/scala/effekt/symbols/symbols.scala | 4 +- .../scala/effekt/util/PrettyPrinter.scala | 2 +- 11 files changed, 341 insertions(+), 105 deletions(-) create mode 100644 effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala create mode 100644 effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala diff --git a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala index f2132c35a..7d8f89167 100644 --- a/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/CoreTests.scala @@ -46,14 +46,14 @@ trait CoreTests extends munit.FunSuite { expected: ModuleDecl, clue: => Any = "values are not alpha-equivalent", names: Names = Names(defaultNames))(using Location): Unit = { - val renamer = Renamer(names, "$") + val renamer = TestRenamer(names, "$") shouldBeEqual(renamer(obtained), renamer(expected), clue) } def assertAlphaEquivalentStatements(obtained: Stmt, expected: Stmt, clue: => Any = "values are not alpha-equivalent", names: Names = Names(defaultNames))(using Location): Unit = { - val renamer = Renamer(names, "$") + val renamer = TestRenamer(names, "$") shouldBeEqual(renamer(obtained), renamer(expected), clue) } def parse(input: String, diff --git a/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala b/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala index fe5f2381a..f46836d99 100644 --- a/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/OptimizerTests.scala @@ -22,7 +22,7 @@ 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 = Renamer(names).rewrite(pInput) + val renamed = TestRenamer(names).rewrite(pInput) val obtained = transform(renamed) assertAlphaEquivalent(obtained, pExpected, "Not transformed to") diff --git a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala index f28ff5bbf..a45a438b6 100644 --- a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala @@ -1,17 +1,20 @@ package effekt.core -import effekt.symbols +/** + * This is testing the main/core.Renamer using the test/core.TestRenamer. + */ class RenamerTests extends CoreTests { - def assertRenamedTo(input: String, - renamed: String, - clue: => Any = "Not renamed to given value", - names: Names = Names(defaultNames))(using munit.Location) = { + /** + * Check that the renamed input preserves alpha-equivalence using [[assertAlphaEquivalent]] + */ + def assertRenamingPreservesAlpha(input: String, + clue: => Any = "Not renamed to given value", + names: Names = Names(defaultNames))(using munit.Location) = { val pInput = parse(input, "input", names) - val pExpected = parse(renamed, "expected", names) - val renamer = new Renamer(names, "renamed") // use "renamed" as prefix so we can refer to it + val renamer = new Renamer(names, "renamed") val obtained = renamer(pInput) - shouldBeEqual(obtained, pExpected, clue) + assertAlphaEquivalent(obtained, pInput, clue) } test("No bound local variables"){ @@ -22,11 +25,11 @@ class RenamerTests extends CoreTests { | return (bar: (Int) => Int @ {})(baz:Int) |} |""".stripMargin - assertRenamedTo(code, code) + assertRenamingPreservesAlpha(code) } test("val binding"){ - val input = + val code = """module main | |def foo = { () => @@ -34,19 +37,11 @@ class RenamerTests extends CoreTests { | return x:Int |} |""".stripMargin - val expected = - """module main - | - |def foo = { () => - | val renamed1 = (foo:(Int)=>Int@{})(4); - | return renamed1:Int - |} - |""".stripMargin - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("var binding"){ - val input = + val code = """module main | |def foo = { () => @@ -54,37 +49,22 @@ class RenamerTests extends CoreTests { | return x:Int |} |""".stripMargin - val expected = - """module main - | - |def foo = { () => - | var renamed1 @ global = (foo:(Int)=>Int@{})(4); - | return renamed1:Int - |} - |""".stripMargin - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("function (value) parameters"){ - val input = + val code = """module main | |def foo = { (x:Int) => | return x:Int |} |""".stripMargin - val expected = - """module main - | - |def foo = { (renamed1:Int) => - | return renamed1:Int - |} - |""".stripMargin - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("match clauses"){ - val input = + val code = """module main | |type Data { X(a:Int, b:Int) } @@ -94,39 +74,22 @@ class RenamerTests extends CoreTests { | } |} |""".stripMargin - val expected = - """module main - | - |type Data { X(a:Int, b:Int) } - |def foo = { () => - | 12 match { - | X : {(renamed1:Int, renamed2:Int) => return renamed1:Int } - | } - |} - |""".stripMargin - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("type parameters"){ - val input = + val code = """module main | |def foo = { ['A](a: A) => | return a:Identity[A] |} |""".stripMargin - val expected = - """module main - | - |def foo = { ['renamed1](renamed2: renamed1) => - | return renamed2:Identity[renamed1] - |} - |""".stripMargin - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("pseudo recursive"){ - val input = + val code = """ module main | | def bar = { () => return 1 } @@ -136,22 +99,10 @@ class RenamerTests extends CoreTests { | (foo : () => Unit @ {})() | } |""".stripMargin - - val expected = - """ module main - | - | def bar = { () => return 1 } - | def main = { () => - | def renamed1 = { () => (bar : () => Unit @ {})() } - | def renamed2 = { () => return 2 } - | (renamed1 : () => Unit @ {})() - | } - |""".stripMargin - - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } test("shadowing let bindings"){ - val input = + val code = """ module main | | def main = { () => @@ -160,17 +111,6 @@ class RenamerTests extends CoreTests { | return x:Int | } |""".stripMargin - - val expected = - """ module main - | - | def main = { () => - | let renamed1 = 1 - | let renamed2 = 2 - | return renamed2:Int - | } - |""".stripMargin - - assertRenamedTo(input, expected) + assertRenamingPreservesAlpha(code) } } diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala new file mode 100644 index 000000000..dee1b6ee3 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -0,0 +1,120 @@ +package effekt.core + +import effekt.{ core, symbols } +import effekt.context.Context + +/** + * Freshens bound names in a given term for tests. + * Please use this _only_ for tests. Otherwise, prefer [[effekt.core.Renamer]]. + * + * @param names used to look up a reference by name to resolve to the same symbols. + * This is only used by tests to deterministically rename terms and check for + * alpha-equivalence. + * @param prefix if the prefix is empty, the original name will be used as a prefix + * + * @param C the context is used to copy annotations from old symbols to fresh symbols + */ +class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends core.Tree.Rewrite { + + // list of scopes that map bound symbols to their renamed variants. + private var scopes: List[Map[Id, Id]] = List.empty + + // Here we track ALL renamings + var renamed: Map[Id, Id] = Map.empty + + private var suffix: Int = 0 + + def freshIdFor(id: Id): Id = + suffix = suffix + 1 + val uniqueName = if prefix.isEmpty then id.name.name + "_" + suffix.toString else prefix + suffix.toString + names.idFor(uniqueName) + + def withBindings[R](ids: List[Id])(f: => R): R = + val before = scopes + try { + val newScope = ids.map { x => x -> freshIdFor(x) }.toMap + scopes = newScope :: scopes + renamed = renamed ++ newScope + f + } finally { scopes = before } + + /** Alias for withBindings(List(id)){...} */ + def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f) + + // free variables are left untouched + override def id: PartialFunction[core.Id, core.Id] = { + case id => scopes.collectFirst { + case bnds if bnds.contains(id) => bnds(id) + }.getOrElse(id) + } + + override def stmt: PartialFunction[Stmt, Stmt] = { + case core.Def(id, block, body) => + // can be recursive + withBinding(id) { core.Def(rewrite(id), rewrite(block), rewrite(body)) } + + case core.Let(id, tpe, binding, body) => + val resolvedBinding = rewrite(binding) + withBinding(id) { core.Let(rewrite(id), rewrite(tpe), resolvedBinding, rewrite(body)) } + + case core.Val(id, tpe, binding, body) => + val resolvedBinding = rewrite(binding) + withBinding(id) { core.Val(rewrite(id), rewrite(tpe), resolvedBinding, rewrite(body)) } + + case core.Alloc(id, init, reg, body) => + val resolvedInit = rewrite(init) + val resolvedReg = rewrite(reg) + withBinding(id) { core.Alloc(rewrite(id), resolvedInit, resolvedReg, rewrite(body)) } + + 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)) } + + case core.Get(id, tpe, ref, capt, body) => + val resolvedRef = rewrite(ref) + val resolvedCapt = rewrite(capt) + withBinding(id) { core.Get(rewrite(id), rewrite(tpe), resolvedRef, resolvedCapt, rewrite(body)) } + + } + + override def block: PartialFunction[Block, Block] = { + case Block.BlockLit(tparams, cparams, vparams, bparams, body) => + withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) { + Block.BlockLit(tparams map rewrite, cparams map rewrite, vparams map rewrite, bparams map rewrite, + rewrite(body)) + } + } + + override def rewrite(o: Operation): Operation = o match { + case Operation(name, tparams, cparams, vparams, bparams, body) => + withBindings(tparams ++ cparams ++ vparams.map(_.id) ++ bparams.map(_.id)) { + Operation(name, + tparams map rewrite, + cparams map rewrite, + vparams map rewrite, + bparams map rewrite, + rewrite(body)) + } + } + + def apply(m: core.ModuleDecl): core.ModuleDecl = + suffix = 0 + m match { + case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) => + core.ModuleDecl(path, includes, declarations, externs, definitions map rewrite, exports) + } + + def apply(s: Stmt): Stmt = { + suffix = 0 + rewrite(s) + } +} + +object TestRenamer { + def rename(b: Block): Block = Renamer().rewrite(b) + def rename(b: BlockLit): (BlockLit, Map[Id, Id]) = + val renamer = Renamer() + val res = renamer.rewrite(b) + (res, renamer.renamed) +} diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala new file mode 100644 index 000000000..4f1a95113 --- /dev/null +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamerTests.scala @@ -0,0 +1,179 @@ +package effekt.core + +/** + * This is testing the test/core.TestRenamer, not the main/core.Renamer :) + * These tests are important, because we use TestRenamer for deciding test-friendly alpha-equivalence in CoreTests. + */ +class TestRenamerTests extends CoreTests { + + def assertRenamedTo(input: String, + renamed: String, + clue: => Any = "Not renamed to given value", + names: Names = Names(defaultNames))(using munit.Location) = { + val pInput = parse(input, "input", names) + val pExpected = parse(renamed, "expected", names) + val renamer = new TestRenamer(names, "renamed") // use "renamed" as prefix so we can refer to it + val obtained = renamer(pInput) + shouldBeEqual(obtained, pExpected, clue) + } + + test("No bound local variables"){ + val code = + """module main + | + |def foo = { () => + | return (bar: (Int) => Int @ {})(baz:Int) + |} + |""".stripMargin + assertRenamedTo(code, code) + } + + test("val binding"){ + val input = + """module main + | + |def foo = { () => + | val x = (foo:(Int)=>Int@{})(4) ; + | return x:Int + |} + |""".stripMargin + val expected = + """module main + | + |def foo = { () => + | val renamed1 = (foo:(Int)=>Int@{})(4); + | return renamed1:Int + |} + |""".stripMargin + assertRenamedTo(input, expected) + } + + test("var binding"){ + val input = + """module main + | + |def foo = { () => + | var x @ global = (foo:(Int)=>Int@{})(4) ; + | return x:Int + |} + |""".stripMargin + val expected = + """module main + | + |def foo = { () => + | var renamed1 @ global = (foo:(Int)=>Int@{})(4); + | return renamed1:Int + |} + |""".stripMargin + assertRenamedTo(input, expected) + } + + test("function (value) parameters"){ + val input = + """module main + | + |def foo = { (x:Int) => + | return x:Int + |} + |""".stripMargin + val expected = + """module main + | + |def foo = { (renamed1:Int) => + | return renamed1:Int + |} + |""".stripMargin + assertRenamedTo(input, expected) + } + + test("match clauses"){ + val input = + """module main + | + |type Data { X(a:Int, b:Int) } + |def foo = { () => + | 12 match { + | X : {(aa:Int, bb:Int) => return aa:Int } + | } + |} + |""".stripMargin + val expected = + """module main + | + |type Data { X(a:Int, b:Int) } + |def foo = { () => + | 12 match { + | X : {(renamed1:Int, renamed2:Int) => return renamed1:Int } + | } + |} + |""".stripMargin + assertRenamedTo(input, expected) + } + + test("type parameters"){ + val input = + """module main + | + |def foo = { ['A](a: A) => + | return a:Identity[A] + |} + |""".stripMargin + val expected = + """module main + | + |def foo = { ['renamed1](renamed2: renamed1) => + | return renamed2:Identity[renamed1] + |} + |""".stripMargin + assertRenamedTo(input, expected) + } + + test("pseudo recursive"){ + val input = + """ module main + | + | def bar = { () => return 1 } + | def main = { () => + | def foo = { () => (bar : () => Unit @ {})() } + | def bar = { () => return 2 } + | (foo : () => Unit @ {})() + | } + |""".stripMargin + + val expected = + """ module main + | + | def bar = { () => return 1 } + | def main = { () => + | def renamed1 = { () => (bar : () => Unit @ {})() } + | def renamed2 = { () => return 2 } + | (renamed1 : () => Unit @ {})() + | } + |""".stripMargin + + assertRenamedTo(input, expected) + } + test("shadowing let bindings"){ + val input = + """ module main + | + | def main = { () => + | let x = 1 + | let x = 2 + | return x:Int + | } + |""".stripMargin + + val expected = + """ module main + | + | def main = { () => + | let renamed1 = 1 + | let renamed2 = 2 + | return renamed2:Int + | } + |""".stripMargin + + assertRenamedTo(input, expected) + } +} diff --git a/effekt/shared/src/main/scala/effekt/core/Renamer.scala b/effekt/shared/src/main/scala/effekt/core/Renamer.scala index 3b76ca1bb..61a2573b2 100644 --- a/effekt/shared/src/main/scala/effekt/core/Renamer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Renamer.scala @@ -4,7 +4,8 @@ import effekt.{ core, symbols } import effekt.context.Context /** - * Freshens bound names in a given term + * Freshens bound names in a given Core term. + * Do not use for tests! See [[effekt.core.TestRenamer]]. * * @param names used to look up a reference by name to resolve to the same symbols. * This is only used by tests to deterministically rename terms and check for @@ -21,12 +22,8 @@ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core // Here we track ALL renamings var renamed: Map[Id, Id] = Map.empty - private var suffix: Int = 0 - def freshIdFor(id: Id): Id = - suffix = suffix + 1 - val uniqueName = if prefix.isEmpty then id.name.name + "_" + suffix.toString else prefix + suffix.toString - names.idFor(uniqueName) + if prefix.isEmpty then Id(id) else Id(id.name.rename { _current => prefix }) def withBindings[R](ids: List[Id])(f: => R): R = val before = scopes @@ -98,14 +95,12 @@ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core } def apply(m: core.ModuleDecl): core.ModuleDecl = - suffix = 0 m match { case core.ModuleDecl(path, includes, declarations, externs, definitions, exports) => core.ModuleDecl(path, includes, declarations, externs, definitions map rewrite, exports) } def apply(s: Stmt): Stmt = { - suffix = 0 rewrite(s) } } diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 43f5e94a7..4c9b24fc7 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -91,10 +91,11 @@ sealed trait Tree extends Product { */ type Id = symbols.Symbol object Id { - def apply(n: String): Id = new symbols.Symbol { - val name = symbols.Name.local(n) + def apply(n: symbols.Name): Id = new symbols.Symbol { + val name = n } - def apply(n: Id): Id = apply(n.name.name) + def apply(n: String): Id = apply(symbols.Name.local(n)) + def apply(n: Id): Id = apply(n.name) } /** diff --git a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala index 09f33c9ca..8f023dfa3 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/StaticArguments.scala @@ -91,7 +91,8 @@ object StaticArguments { // the worker now closes over the static block arguments (`c` in the example above): val newCapture = blockLit.capt ++ selectStatic(staticB, freshCparams).toSet - val workerVar: Block.BlockVar = BlockVar(Id(id.name.name + "_worker"), workerType, newCapture) + val workerId = Id(id.name.rename { original => s"${original}_worker"}) + val workerVar: Block.BlockVar = BlockVar(workerId, workerType, newCapture) ctx.workers(id) = workerVar BlockLit( diff --git a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala index ac659534b..9b2e64966 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Symbol.scala @@ -38,7 +38,7 @@ trait Symbol { case _ => false } - def show: String = name.toString + id + def show: String = s"${name}_${id}" override def toString: String = name.toString } diff --git a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala index b7feb47b8..5a335df84 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/symbols.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/symbols.scala @@ -211,8 +211,8 @@ case class CallTarget(symbols: List[Set[BlockSymbol]]) extends BlockSymbol { val * Introduced by Transformer */ case class Wildcard() extends ValueSymbol { val name = Name.local("_") } -case class TmpValue(hint: String = "tmp") extends ValueSymbol { val name = Name.local("v_" + hint + "_" + Symbol.fresh.next()) } -case class TmpBlock(hint: String = "tmp") extends BlockSymbol { val name = Name.local("b_" + hint + "_" + Symbol.fresh.next()) } +case class TmpValue(hint: String = "tmp") extends ValueSymbol { val name = Name.local("v_" + hint) } +case class TmpBlock(hint: String = "tmp") extends BlockSymbol { val name = Name.local("b_" + hint) } /** * Type Symbols diff --git a/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala index bfa3051fb..97750d950 100644 --- a/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/util/PrettyPrinter.scala @@ -16,7 +16,7 @@ object PrettyPrinter extends ParenPrettyPrinter { def format(value: Any): Document = pretty(toDoc(value), 80) def toDoc(value: Any): Doc = value match { - case sym: effekt.symbols.Symbol => string(sym.name.name + "_" + sym.id.toString) + case sym: effekt.symbols.Symbol => string(sym.show) case Nil => "Nil" case l: List[a] => "List" <> parens(l.map(toDoc)) case l: Map[a, b] => "Map" <> parens(l.map { From d6472ec382b8962cbb5f74eb880211230560aab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Thu, 17 Apr 2025 14:36:13 +0200 Subject: [PATCH 29/41] Don't reallocate StringBuffer when flushing (#939) I think that the allocation here is not needed, so let's avoid it. Also adds more tests for the StringBuffer. --- examples/stdlib/stringbuffer.check | 3 +++ examples/stdlib/stringbuffer.effekt | 3 +++ libraries/common/stringbuffer.effekt | 18 +++++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 examples/stdlib/stringbuffer.check create mode 100644 examples/stdlib/stringbuffer.effekt diff --git a/examples/stdlib/stringbuffer.check b/examples/stdlib/stringbuffer.check new file mode 100644 index 000000000..811f48d9a --- /dev/null +++ b/examples/stdlib/stringbuffer.check @@ -0,0 +1,3 @@ +hello, world + +Effekt = Effekt diff --git a/examples/stdlib/stringbuffer.effekt b/examples/stdlib/stringbuffer.effekt new file mode 100644 index 000000000..f4572882a --- /dev/null +++ b/examples/stdlib/stringbuffer.effekt @@ -0,0 +1,3 @@ +import stringbuffer + +def main() = stringbuffer::examples::main() diff --git a/libraries/common/stringbuffer.effekt b/libraries/common/stringbuffer.effekt index 60b4a3b89..dd4daac2d 100644 --- a/libraries/common/stringbuffer.effekt +++ b/libraries/common/stringbuffer.effekt @@ -35,10 +35,11 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = { resume(()) } def flush() = { - // resize buffer to strip trailing zeros that otherwise would be converted into 0x00 characters + // resize (& copy) buffer to strip trailing zeros that otherwise would be converted into 0x00 characters val str = bytearray::resize(buffer, pos).toString() - // after flushing, the stringbuffer should be empty again - buffer = bytearray::allocate(initialCapacity) + // NOTE: Keep the `buffer` as-is (no wipe, no realloc), + // just reset the `pos` in case we want to use it again. + pos = 0 resume(str) } } @@ -55,11 +56,22 @@ def s { prog: () => Unit / { literal, splice[String] } }: String = namespace examples { def main() = { with stringBuffer + do write("hello") do write(", world") // prints `hello, world` println(do flush()) + // prints the empty string println(do flush()) + + do write("Ef") + do write("fe") + do write("kt") + do write(" = ") + do write("") + do write("Effekt") + // prints `Effekt = Effekt` + println(do flush()) } } From 73edfdc66196253754b586a7327b90554b8a2962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Thu, 17 Apr 2025 16:34:48 +0200 Subject: [PATCH 30/41] Renamer: Add infrastructure for Checking Uniqueness of Generated Names (#941) After #938, the `RenamerTests` do not check that the names are actually unique. This adds assertions checking that every `Id` only occurs in one definition-like construct. (note that https://github.com/effekt-lang/effekt/pull/941/commits/4cf23710d2c4be2202a73d888e19d4df1ddf1645 is intentionally wrong to show it failing on shadowing :) ) --- .../test/scala/effekt/core/RenamerTests.scala | 63 +++++++++++++++++++ .../src/main/scala/effekt/core/Tree.scala | 1 + 2 files changed, 64 insertions(+) diff --git a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala index a45a438b6..84462ced1 100644 --- a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala @@ -1,5 +1,7 @@ package effekt.core +import scala.collection.mutable + /** * This is testing the main/core.Renamer using the test/core.TestRenamer. */ @@ -17,6 +19,59 @@ class RenamerTests extends CoreTests { assertAlphaEquivalent(obtained, pInput, clue) } + def assertDefsUnique(in: ModuleDecl, + clue: => Any = "Duplicate definition") = { + val seen = mutable.HashSet.empty[Id] + + def isFresh(id: Id): Unit = { + assert(!seen.contains(id), clue) + seen.add(id) + } + + object check extends Tree.Query[Unit, Unit] { + override def empty = () + + override def combine = (_, _) => () + + override def visit[T](t: T)(visitor: Unit ?=> T => Unit)(using Unit): Unit = { + visitor(t) + t match { + case m: ModuleDecl => + m.definitions.foreach { d => isFresh(d.id) } + case d: Def => isFresh(d.id) + case v: Val => isFresh(v.id) + case l: Let => isFresh(l.id) + case d: Declaration => isFresh(d.id) + case e: Extern.Def => + isFresh(e.id) + e.tparams.foreach(isFresh); + e.vparams.foreach { p => isFresh(p.id) } + e.bparams.foreach { p => isFresh(p.id) }; + e.cparams.foreach { p => isFresh(p) } + case b: BlockLit => + b.tparams.foreach(isFresh); + b.cparams.foreach(isFresh) + b.vparams.foreach { p => isFresh(p.id) }; + b.bparams.foreach { p => isFresh(p.id) } + case i: Implementation => + i.operations.foreach { o => + o.vparams.foreach { p => isFresh(p.id) }; o.bparams.foreach { p => isFresh(p.id) } + } + case _ => () + } + } + } + check.query(in)(using ()) + } + def assertRenamingMakesDefsUnique(input: String, + clue: => Any = "Duplicate definition", + names: Names = Names(defaultNames))(using munit.Location) = { + val pInput = parse(input, "input", names) + val renamer = new Renamer(names, "renamed") + val obtained = renamer(pInput) + assertDefsUnique(obtained, clue) + } + test("No bound local variables"){ val code = """module main @@ -26,6 +81,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("val binding"){ @@ -38,6 +94,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("var binding"){ @@ -50,6 +107,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("function (value) parameters"){ @@ -61,6 +119,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("match clauses"){ @@ -75,6 +134,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("type parameters"){ @@ -86,6 +146,7 @@ class RenamerTests extends CoreTests { |} |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("pseudo recursive"){ @@ -100,6 +161,7 @@ class RenamerTests extends CoreTests { | } |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } test("shadowing let bindings"){ val code = @@ -112,5 +174,6 @@ class RenamerTests extends CoreTests { | } |""".stripMargin assertRenamingPreservesAlpha(code) + assertRenamingMakesDefsUnique(code) } } diff --git a/effekt/shared/src/main/scala/effekt/core/Tree.scala b/effekt/shared/src/main/scala/effekt/core/Tree.scala index 4c9b24fc7..bdf591826 100644 --- a/effekt/shared/src/main/scala/effekt/core/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/core/Tree.scala @@ -432,6 +432,7 @@ object Tree { case (id, lit) => query(lit) } def query(b: ExternBody)(using Ctx): Res = structuralQuery(b, externBody) + def query(m: ModuleDecl)(using Ctx) = structuralQuery(m, PartialFunction.empty) } class Rewrite extends Structural { From 28edaf28109f660bc6e4065b21ecc32c87498db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Fri, 18 Apr 2025 00:30:23 +0200 Subject: [PATCH 31/41] Make 'core.Renamer' quicker by simplifying scope handling (#940) I don't know what the nested list is for, but we don't seem to use it anywhere. Therefore I'm removing the nesting and using mutable hashmaps: it's on a hot path! Renamer then goes from 2.4-3.4s to 0.6-0.4s :) [measured a few times via profiling] (for reference, before #938, it was somewhere between 3 and 4 seconds) --- .../test/scala/effekt/core/RenamerTests.scala | 19 ++++++++++++ .../test/scala/effekt/core/TestRenamer.scala | 10 +------ .../src/main/scala/effekt/core/Renamer.scala | 30 ++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala index 84462ced1..4b7538dab 100644 --- a/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala +++ b/effekt/jvm/src/test/scala/effekt/core/RenamerTests.scala @@ -176,4 +176,23 @@ class RenamerTests extends CoreTests { assertRenamingPreservesAlpha(code) assertRenamingMakesDefsUnique(code) } + test("shadowing let bindings inside a def") { + val code = + """ module main + | + | def main = { () => + | def foo = { () => + | let x = 1 + | return x: Int + | } + | let x = 2 + | def bar = { () => + | let x = 3 + | return x: Int + | } + | return x:Int + | } + |""".stripMargin + assertRenamingPreservesAlpha(code) + } } diff --git a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala index dee1b6ee3..b32fe7262 100644 --- a/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala +++ b/effekt/jvm/src/test/scala/effekt/core/TestRenamer.scala @@ -109,12 +109,4 @@ class TestRenamer(names: Names = Names(Map.empty), prefix: String = "") extends suffix = 0 rewrite(s) } -} - -object TestRenamer { - def rename(b: Block): Block = Renamer().rewrite(b) - def rename(b: BlockLit): (BlockLit, Map[Id, Id]) = - val renamer = Renamer() - val res = renamer.rewrite(b) - (res, renamer.renamed) -} +} \ No newline at end of file diff --git a/effekt/shared/src/main/scala/effekt/core/Renamer.scala b/effekt/shared/src/main/scala/effekt/core/Renamer.scala index 61a2573b2..2f99bc642 100644 --- a/effekt/shared/src/main/scala/effekt/core/Renamer.scala +++ b/effekt/shared/src/main/scala/effekt/core/Renamer.scala @@ -1,7 +1,8 @@ package effekt.core +import scala.collection.mutable + import effekt.{ core, symbols } -import effekt.context.Context /** * Freshens bound names in a given Core term. @@ -16,32 +17,33 @@ import effekt.context.Context */ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core.Tree.Rewrite { - // list of scopes that map bound symbols to their renamed variants. - private var scopes: List[Map[Id, Id]] = List.empty + // Local renamings: map of bound symbols to their renamed variants in a given scope. + private var scope: Map[Id, Id] = Map.empty - // Here we track ALL renamings - var renamed: Map[Id, Id] = Map.empty + // All renamings: map of bound symbols to their renamed variants, globally! + val renamed: mutable.HashMap[Id, Id] = mutable.HashMap.empty def freshIdFor(id: Id): Id = if prefix.isEmpty then Id(id) else Id(id.name.rename { _current => prefix }) def withBindings[R](ids: List[Id])(f: => R): R = - val before = scopes + val scopeBefore = scope try { - val newScope = ids.map { x => x -> freshIdFor(x) }.toMap - scopes = newScope :: scopes - renamed = renamed ++ newScope + ids.foreach { x => + val fresh = freshIdFor(x) + scope = scope + (x -> fresh) + renamed.put(x, fresh) + } + f - } finally { scopes = before } + } finally { scope = scopeBefore } /** Alias for withBindings(List(id)){...} */ def withBinding[R](id: Id)(f: => R): R = withBindings(List(id))(f) // free variables are left untouched override def id: PartialFunction[core.Id, core.Id] = { - case id => scopes.collectFirst { - case bnds if bnds.contains(id) => bnds(id) - }.getOrElse(id) + id => scope.getOrElse(id, id) } override def stmt: PartialFunction[Stmt, Stmt] = { @@ -107,7 +109,7 @@ class Renamer(names: Names = Names(Map.empty), prefix: String = "") extends core object Renamer { def rename(b: Block): Block = Renamer().rewrite(b) - def rename(b: BlockLit): (BlockLit, Map[Id, Id]) = + def rename(b: BlockLit): (BlockLit, mutable.HashMap[Id, Id]) = val renamer = Renamer() val res = renamer.rewrite(b) (res, renamer.renamed) From 62b505f39d2ab7676a95996b746ccbe67e2916c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Sat, 19 Apr 2025 21:00:34 +0200 Subject: [PATCH 32/41] Add tailrec annotations wherever possible (#945) IntelliJ has a button for adding `@tailrec` wherever needed. So I clicked it. Let's see what happens now! --- effekt/jvm/src/main/scala/effekt/util/PathUtils.scala | 2 +- effekt/shared/src/main/scala/effekt/Namer.scala | 2 ++ .../src/main/scala/effekt/core/optimizer/Normalizer.scala | 1 + effekt/shared/src/main/scala/effekt/core/vm/VM.scala | 1 + .../src/main/scala/effekt/generator/llvm/Transformer.scala | 2 ++ effekt/shared/src/main/scala/effekt/source/Tree.scala | 3 +++ effekt/shared/src/main/scala/effekt/symbols/Scope.scala | 2 ++ effekt/shared/src/main/scala/effekt/util/Structural.scala | 3 +++ 8 files changed, 15 insertions(+), 1 deletion(-) diff --git a/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala b/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala index 3d7e3f89c..e73ada83f 100644 --- a/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala +++ b/effekt/jvm/src/main/scala/effekt/util/PathUtils.scala @@ -44,7 +44,7 @@ object paths extends PathUtils { implicit def file(uri: URI): File = file(uri.getPath) implicit def file(filepath: String) = if (filepath.startsWith("file:")) - file(new URI(filepath).getPath) + file(new URI(filepath)) else file(new JFile(filepath)) } diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 61f3973e6..19c34348c 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -13,6 +13,7 @@ import effekt.util.messages.ErrorMessageReifier import effekt.symbols.scopes.* import effekt.source.FeatureFlag.supportedByFeatureFlags +import scala.annotation.tailrec import scala.util.DynamicVariable /** @@ -944,6 +945,7 @@ trait NamerOps extends ContextOps { Context: Context => * 2) If the tighest scope contains blocks, then we will ignore all values * and resolve to an overloaded target. */ + @tailrec private def resolveFunctionCalltarget(id: IdRef, candidates: List[Set[TermSymbol]]): Either[TermSymbol, List[Set[BlockSymbol]]] = // Mutable variables are treated as values, not as blocks. Maybe we should change the representation. 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 c4a870521..b96da7e12 100644 --- a/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala +++ b/effekt/shared/src/main/scala/effekt/core/optimizer/Normalizer.scala @@ -125,6 +125,7 @@ object Normalizer { normal => * A good testcase to look at for this is: * examples/pos/capture/regions.effekt */ + @tailrec private def active[R](b: Block)(using C: Context): NormalizedBlock = normalize(b) match { case b: Block.BlockLit => NormalizedBlock.Known(b, None) 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 31682d487..541f5d27b 100644 --- a/effekt/shared/src/main/scala/effekt/core/vm/VM.scala +++ b/effekt/shared/src/main/scala/effekt/core/vm/VM.scala @@ -205,6 +205,7 @@ class Interpreter(instrumentation: Instrumentation, runtime: Runtime) { stack match { case Stack.Empty => ??? case Stack.Segment(frames, prompt, rest) => + @tailrec def go(frames: List[Frame], acc: List[Frame]): Stack = frames match { case Nil => diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala index fcbb1937d..709d9e39f 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala @@ -7,6 +7,7 @@ import effekt.util.intercalate import effekt.util.messages.ErrorReporter import effekt.machine.analysis.* +import scala.annotation.tailrec import scala.collection.mutable object Transformer { @@ -633,6 +634,7 @@ object Transformer { } def shareValues(values: machine.Environment, freeInBody: Set[machine.Variable])(using FunctionContext, BlockContext): Unit = { + @tailrec def loop(values: machine.Environment): Unit = { values match { case Nil => () diff --git a/effekt/shared/src/main/scala/effekt/source/Tree.scala b/effekt/shared/src/main/scala/effekt/source/Tree.scala index f8ae8fafb..dd4985350 100644 --- a/effekt/shared/src/main/scala/effekt/source/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/source/Tree.scala @@ -4,6 +4,8 @@ package source import effekt.context.Context import effekt.symbols.Symbol +import scala.annotation.tailrec + /** * Data type representing source program trees. * @@ -125,6 +127,7 @@ enum FeatureFlag extends Tree { } object FeatureFlag { extension (self: List[ExternBody]) { + @tailrec def supportedByFeatureFlags(names: List[String]): Boolean = names match { case Nil => false case name :: other => diff --git a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala index a74c6006b..ae7dad515 100644 --- a/effekt/shared/src/main/scala/effekt/symbols/Scope.scala +++ b/effekt/shared/src/main/scala/effekt/symbols/Scope.scala @@ -4,6 +4,7 @@ package symbols import effekt.source.IdRef import effekt.util.messages.ErrorReporter +import scala.annotation.tailrec import scala.collection.mutable /** @@ -100,6 +101,7 @@ object scopes { case class Scoping(modulePath: List[String], var scope: Scope) { def importAs(imports: Bindings, path: List[String])(using E: ErrorReporter): Unit = + @tailrec def go(path: List[String], in: Namespace): Unit = path match { case pathSeg :: rest => go(rest, in.getNamespace(pathSeg)) case Nil => in.importAll(imports) diff --git a/effekt/shared/src/main/scala/effekt/util/Structural.scala b/effekt/shared/src/main/scala/effekt/util/Structural.scala index 5ca31b7a8..a87330e40 100644 --- a/effekt/shared/src/main/scala/effekt/util/Structural.scala +++ b/effekt/shared/src/main/scala/effekt/util/Structural.scala @@ -1,6 +1,7 @@ package effekt package util +import scala.annotation.tailrec import scala.quoted.* /** @@ -128,6 +129,7 @@ class StructuralMacro[Self: Type, Q <: Quotes](debug: Boolean)(using val q: Q) { val self = TypeRepr.of[Self].typeSymbol val rewriteMethod = + @tailrec def findMethod(sym: Symbol): Symbol = if (sym.isDefDef && !sym.isAnonymousFunction) sym else findMethod(sym.owner) @@ -145,6 +147,7 @@ class StructuralMacro[Self: Type, Q <: Quotes](debug: Boolean)(using val q: Q) { val rewrites: List[RewriteMethod] = self.methodMember(rewriteName).map { m => TypeRepr.of[Self].memberType(m) match { case tpe: MethodType => + @tailrec def findResult(tpe: TypeRepr): TypeRepr = tpe match { case tpe: LambdaType => findResult(tpe.resType) case _ => tpe From d30579403c1de11d3bc3958c3663e645330ddade Mon Sep 17 00:00:00 2001 From: "effekt-updater[bot]" <181701480+effekt-updater[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 03:58:34 +0000 Subject: [PATCH 33/41] Bump version to 0.27.0 --- package.json | 2 +- pom.xml | 2 +- project/EffektVersion.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d989d3163..897933c08 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@effekt-lang/effekt", "author": "Jonathan Brachthäuser", - "version": "0.26.0", + "version": "0.27.0", "repository": { "type": "git", "url": "git+https://github.com/effekt-lang/effekt.git" diff --git a/pom.xml b/pom.xml index 3298e3baf..6a5841c3a 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.bstudios Effekt Effekt - 0.26.0 + 0.27.0 4.0.0 diff --git a/project/EffektVersion.scala b/project/EffektVersion.scala index 01eafd1e7..d31cc98b3 100644 --- a/project/EffektVersion.scala +++ b/project/EffektVersion.scala @@ -1,4 +1,4 @@ // Don't change this file without changing the CI too! import sbt.* import sbt.Keys.* -object EffektVersion { lazy val effektVersion = "0.26.0" } +object EffektVersion { lazy val effektVersion = "0.27.0" } From 1ba0939f2bcdbdcde7763039c581be0f09082756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Immanuel=20Brachth=C3=A4user?= Date: Tue, 22 Apr 2025 21:49:07 +0200 Subject: [PATCH 34/41] Cache substitutions (#954) Substitutions are used multiple times but computed over and over again. This PR caches them until any modification happens. On my machine, this brings down typer from `1150ms` to `720ms` with ``` effekt --time text examples/casestudies/anf.effekt.md ``` This shaves of 25% of the total compile time, which is reduced from 2.5s to 2s. --- .../main/scala/effekt/typer/Constraints.scala | 20 +++++++++++++++---- .../scala/effekt/typer/Substitution.scala | 8 +------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala b/effekt/shared/src/main/scala/effekt/typer/Constraints.scala index a3425938e..2764d3b3f 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Constraints.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Constraints.scala @@ -121,16 +121,25 @@ class Constraints( * Unification variables which are not in scope anymore, but also haven't been solved, yet. */ private var pendingInactive: Set[CNode] = Set.empty - )(using C: ErrorReporter) { + /** + * Caches type substitutions, which are only invalidated if the mapping from node to typevar changes. + * + * This significantly improves the performance of Typer (see https://github.com/effekt-lang/effekt/pull/954) + */ + private var _typeSubstitution: Map[TypeVar, ValueType] = _ + private def invalidate(): Unit = _typeSubstitution = null + /** * The currently known substitutions */ def subst: Substitutions = - val types = classes.flatMap[TypeVar, ValueType] { case (k, v) => typeSubstitution.get(v).map { k -> _ } } + if _typeSubstitution == null then { + _typeSubstitution = classes.flatMap[TypeVar, ValueType] { case (k, v) => typeSubstitution.get(v).map { k -> _ } } + } val captures = captSubstitution.asInstanceOf[Map[CaptVar, Captures]] - Substitutions(types, captures) + Substitutions(_typeSubstitution, captures) /** * Should only be called on unification variables where we do not know any types, yet @@ -308,16 +317,18 @@ class Constraints( private [typer] def upperNodes: Map[CNode, Filter] = getData(x).upperNodes private def lower_=(bounds: Set[Capture]): Unit = captureConstraints = captureConstraints.updated(x, getData(x).copy(lower = Some(bounds))) + private def upper_=(bounds: Set[Capture]): Unit = captureConstraints = captureConstraints.updated(x, getData(x).copy(upper = Some(bounds))) + private def addLower(other: CNode, exclude: Filter): Unit = val oldData = getData(x) // compute the intersection of filters val oldFilter = oldData.lowerNodes.get(other) val newFilter = oldFilter.map { _ intersect exclude }.getOrElse { exclude } - captureConstraints = captureConstraints.updated(x, oldData.copy(lowerNodes = oldData.lowerNodes + (other -> newFilter))) + private def addUpper(other: CNode, exclude: Filter): Unit = val oldData = getData(x) @@ -462,6 +473,7 @@ class Constraints( */ private def updateSubstitution(): Unit = val substitution = subst + invalidate() typeSubstitution = typeSubstitution.map { case (node, tpe) => node -> substitution.substitute(tpe) } private def getNode(x: UnificationVar): Node = diff --git a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala b/effekt/shared/src/main/scala/effekt/typer/Substitution.scala index 610e935d5..a736de9de 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Substitution.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Substitution.scala @@ -31,12 +31,6 @@ case class Substitutions( } def get(x: CaptUnificationVar): Option[Captures] = captures.get(x) - // amounts to first substituting this, then other - def updateWith(other: Substitutions): Substitutions = - Substitutions( - values.view.mapValues { t => other.substitute(t) }.toMap, - captures.view.mapValues { t => other.substitute(t) }.toMap) ++ other - // amounts to parallel substitution def ++(other: Substitutions): Substitutions = Substitutions(values ++ other.values, captures ++ other.captures) @@ -95,4 +89,4 @@ object Substitutions { def apply(values: List[(TypeVar, ValueType)], captures: List[(CaptVar, Captures)]): Substitutions = Substitutions(values.toMap, captures.toMap) def types(keys: List[TypeVar], values: List[ValueType]): Substitutions = Substitutions((keys zip values).toMap, Map.empty) def captures(keys: List[CaptVar], values: List[Captures]): Substitutions = Substitutions(Map.empty, (keys zip values).toMap) -} \ No newline at end of file +} From 28200a2db52fcb29fd55715f606013b0076d4cc9 Mon Sep 17 00:00:00 2001 From: Jakub <168542356+JakubSchwenkbeck@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:04:26 +0200 Subject: [PATCH 35/41] Specific info message for equally named effect operations (#956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Enhance Reporting for Missing `do` Syntax in Effect Operations This pull request improves error messages for unresolved function calls in the presence of equally named effect operations, addressing issue [#950](https://github.com/effekt-lang/effekt/issues/950). --- ## Summary This update enhances the info message in the `Namer` component of Effekt when an equally named effect operation exists but has not been invoked using the `do` syntax. It ensures the user is provided with a more descriptive information message, avoiding the generic `"Cannot find a function named _"`. --- ## Changes ### 1. **Improved Information in `Namer.scala`**: - Removed `lookupOverloaded` and with it the never issued information on fields and added `lookupOperation` to capture equally named effect operations the `resolveFunctionCallTarget` method. - When an equally named effect operation is found, an **information message** is provided to the user with the recommended syntax (`do {operationName}()`). ### 3. **Added Negative Tests**: - Introduced three new tests in `examples/neg/namer/issue950` to verify the following cases: - An info message is displayed when an effect operation is invoked without the `do` syntax. - The generic message is displayed when no valid function or operation matches the call. - Overloaded Effects all get captured by the `foreach` --- ## Example Behavior ### Before the Fix ```scala interface Greet { def sayHello(): Unit } def helloWorld() = try { sayHello() // [error] Cannot find a function named `sayHello`. } with Greet { def sayHello() = { println("Hello!"); resume(()) } } def main() = { helloWorld() // Generic error, not helpful } ``` ### After the Fix ```scala interface Greet { def sayHello(): Unit } def helloWorld() = try { sayHello() // [error] There is an equally named effect operation `sayHello` of interface `Greet`. Use syntax `do sayHello()` to call it. } with Greet { def sayHello() = { println("Hello!"); resume(()) } } def main() = { helloWorld() // Specific error with actionable advice } ``` --- ## Testing - **Negative Test 1**: Missing `do` for effect operation: - Confirms that invoking an effect operation without the `do` syntax triggers the improved error message. - **Negative Test 2**: Unresolved function call: - Ensures that the generic error message appears when no matches (operations, fields, or overloads) are found. - **Negative Test 3**: Overloaded effect operations: - Confirms that overloaded effect operations are all captured by the new foreach --------- Co-authored-by: Jiří Beneš --- effekt/shared/src/main/scala/effekt/Namer.scala | 12 ++++++------ .../issue950/ambiguous_interfaces_overload.check | 9 +++++++++ .../issue950/ambiguous_interfaces_overload.effekt | 12 ++++++++++++ .../neg/namer/issue950/cannot_find_function.effekt | 1 + .../issue950/effect_operation_with_same_name.check | 6 ++++++ .../issue950/effect_operation_with_same_name.effekt | 11 +++++++++++ 6 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 examples/neg/namer/issue950/ambiguous_interfaces_overload.check create mode 100644 examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt create mode 100644 examples/neg/namer/issue950/cannot_find_function.effekt create mode 100644 examples/neg/namer/issue950/effect_operation_with_same_name.check create mode 100644 examples/neg/namer/issue950/effect_operation_with_same_name.effekt diff --git a/effekt/shared/src/main/scala/effekt/Namer.scala b/effekt/shared/src/main/scala/effekt/Namer.scala index 19c34348c..40420a814 100644 --- a/effekt/shared/src/main/scala/effekt/Namer.scala +++ b/effekt/shared/src/main/scala/effekt/Namer.scala @@ -921,14 +921,14 @@ trait NamerOps extends ContextOps { Context: Context => assignSymbol(id, value) case Right(blocks) => if (blocks.isEmpty) { - val allSyms = scope.lookupOverloaded(id, term => true).flatten + val ops = scope.lookupOperation(id.path, id.name).flatten - if (allSyms.exists { case o: Operation => true; case _ => false }) - info(pretty"There is an equally named effect operation. Use syntax `do ${id}() to call it.`") - - if (allSyms.exists { case o: Field => true; case _ => false }) - info(pretty"There is an equally named field. Use syntax `obj.${id} to access it.`") + // Provide specific info messages for operations + ops.foreach { op => + info(pretty"There is an equally named effect operation ${op} of interface ${op.interface}. Use syntax `do ${id}()` to call it.") + } + // Always abort with the generic message abort(pretty"Cannot find a function named `${id}`.") } assignSymbol(id, CallTarget(blocks)) diff --git a/examples/neg/namer/issue950/ambiguous_interfaces_overload.check b/examples/neg/namer/issue950/ambiguous_interfaces_overload.check new file mode 100644 index 000000000..eec95e131 --- /dev/null +++ b/examples/neg/namer/issue950/ambiguous_interfaces_overload.check @@ -0,0 +1,9 @@ +[info] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: There is an equally named effect operation sayHello of interface Greet. Use syntax `do sayHello()` to call it. + sayHello() + ^^^^^^^^ +[info] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: There is an equally named effect operation sayHello of interface Welcome. Use syntax `do sayHello()` to call it. + sayHello() + ^^^^^^^^ +[error] examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt:5:3: Cannot find a function named `sayHello`. + sayHello() + ^^^^^^^^ \ No newline at end of file diff --git a/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt b/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt new file mode 100644 index 000000000..2aeb884c4 --- /dev/null +++ b/examples/neg/namer/issue950/ambiguous_interfaces_overload.effekt @@ -0,0 +1,12 @@ +interface Greet { def sayHello(): Unit } +interface Welcome { def sayHello(): Unit } + +def helloWorld() = try { + sayHello() +} with Greet { + def sayHello() = { println("Hello from Greet!"); resume(()) } +} + +def main() = { + helloWorld() +} \ No newline at end of file diff --git a/examples/neg/namer/issue950/cannot_find_function.effekt b/examples/neg/namer/issue950/cannot_find_function.effekt new file mode 100644 index 000000000..7ac08fbc1 --- /dev/null +++ b/examples/neg/namer/issue950/cannot_find_function.effekt @@ -0,0 +1 @@ +def main() = doesNotExist() // ERROR Cannot find a function named `doesNotExist`. diff --git a/examples/neg/namer/issue950/effect_operation_with_same_name.check b/examples/neg/namer/issue950/effect_operation_with_same_name.check new file mode 100644 index 000000000..35c1f3ed6 --- /dev/null +++ b/examples/neg/namer/issue950/effect_operation_with_same_name.check @@ -0,0 +1,6 @@ +[info] examples/neg/namer/issue950/effect_operation_with_same_name.effekt:4:3: There is an equally named effect operation sayHello of interface Greet. Use syntax `do sayHello()` to call it. + sayHello() + ^^^^^^^^ +[error] examples/neg/namer/issue950/effect_operation_with_same_name.effekt:4:3: Cannot find a function named `sayHello`. + sayHello() + ^^^^^^^^ \ No newline at end of file diff --git a/examples/neg/namer/issue950/effect_operation_with_same_name.effekt b/examples/neg/namer/issue950/effect_operation_with_same_name.effekt new file mode 100644 index 000000000..78e615947 --- /dev/null +++ b/examples/neg/namer/issue950/effect_operation_with_same_name.effekt @@ -0,0 +1,11 @@ +interface Greet { def sayHello(): Unit } + +def helloWorld() = try { + sayHello() +} with Greet { + def sayHello() = { println("Hello!"); resume(()) } +} + +def main() = { + helloWorld() +} \ No newline at end of file From 16f0ce8b567d191ad3eee4ead3e0a5baa8fc9755 Mon Sep 17 00:00:00 2001 From: "effekt-updater[bot]" <181701480+effekt-updater[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 03:56:13 +0000 Subject: [PATCH 36/41] Bump version to 0.28.0 --- package.json | 2 +- pom.xml | 2 +- project/EffektVersion.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 897933c08..ce5b60d60 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@effekt-lang/effekt", "author": "Jonathan Brachthäuser", - "version": "0.27.0", + "version": "0.28.0", "repository": { "type": "git", "url": "git+https://github.com/effekt-lang/effekt.git" diff --git a/pom.xml b/pom.xml index 6a5841c3a..3e50f66b7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ de.bstudios Effekt Effekt - 0.27.0 + 0.28.0 4.0.0 diff --git a/project/EffektVersion.scala b/project/EffektVersion.scala index d31cc98b3..d36205607 100644 --- a/project/EffektVersion.scala +++ b/project/EffektVersion.scala @@ -1,4 +1,4 @@ // Don't change this file without changing the CI too! import sbt.* import sbt.Keys.* -object EffektVersion { lazy val effektVersion = "0.27.0" } +object EffektVersion { lazy val effektVersion = "0.28.0" } From 990fe1056083eea3ba209553972a0912ea714e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcial=20Gai=C3=9Fert?= Date: Mon, 28 Apr 2025 17:25:59 +0200 Subject: [PATCH 37/41] Add wellformedness check for type of `var ... in ...` (#961) Fixes #919 by adding a wellformedness check as suggested [here](https://github.com/effekt-lang/effekt/issues/919#issuecomment-2769562532). Resulting error message: Screenshot 2025-04-25 at 14 27 57 --- .../src/main/scala/effekt/typer/Wellformedness.scala | 4 ++++ examples/neg/issue919.effekt | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 examples/neg/issue919.effekt diff --git a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala index e8f4d5079..3ab2440b6 100644 --- a/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala +++ b/effekt/shared/src/main/scala/effekt/typer/Wellformedness.scala @@ -315,6 +315,10 @@ object Wellformedness extends Phase[Typechecked, Typechecked], Visit[WFContext] val boundTypes = tps.map(_.symbol.asTypeParam).toSet[TypeVar] val boundCapts = bps.map(_.id.symbol.asBlockParam.capture).toSet binding(types = boundTypes, captures = boundCapts) { bodies.foreach(query) } + + case tree @ source.RegDef(id, annot, region, init) => + wellformed(Context.typeOf(id.symbol), tree, pp" inferred as type of region-allocated variable") + query(init) } // Can only compute free capture on concrete sets diff --git a/examples/neg/issue919.effekt b/examples/neg/issue919.effekt new file mode 100644 index 000000000..1943f2d38 --- /dev/null +++ b/examples/neg/issue919.effekt @@ -0,0 +1,11 @@ +effect tick(): Unit + +def main() = { + region r { + var k in r = box { () } // ERROR tick escapes + try { + k = box { do tick() } + } with tick { () => () } + k() + } +} \ No newline at end of file From 298d186b7080be905d0311e85f252e63f9e38a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattis=20B=C3=B6ckle?= Date: Mon, 28 Apr 2025 17:36:42 +0200 Subject: [PATCH 38/41] LLVM: Emit most declared definitions as private (#946) I was experimenting with making emitted function definitions `private` by default in llvm. This seemed to only reduce the code size of our *.opt.ll files, which might not even be wanted, because now all the unused/inlined function definitions are gone. Benchmarking this showed no improvement *except* for "examples/benchmarks/are_we_fast_yet/queens.effekt" which unexpectedly got 30% faster. Not sure what is going on --- .../main/scala/effekt/generator/llvm/PrettyPrinter.scala | 9 +++++++-- .../main/scala/effekt/generator/llvm/Transformer.scala | 6 +++--- .../src/main/scala/effekt/generator/llvm/Tree.scala | 8 +++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala index 778f151d3..68910ee20 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/PrettyPrinter.scala @@ -12,9 +12,9 @@ object PrettyPrinter { definitions.map(show).mkString("\n\n") def show(definition: Definition)(using C: Context): LLVMString = definition match { - case Function(callingConvention, returnType, name, parameters, basicBlocks) => + case Function(linkage, callingConvention, returnType, name, parameters, basicBlocks) => s""" -define ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) { +define ${show(linkage)} ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${commaSeparated(parameters.map(show))}) { ${indentedLines(basicBlocks.map(show).mkString)} } """ @@ -38,6 +38,11 @@ define ${show(callingConvention)} ${show(returnType)} ${globalName(name)}(${comm s"@$name = private constant ${show(initializer)}" } + def show(linkage: Linkage): LLVMString = linkage match { + case External() => "external" + case Private() => "private" + } + def show(callingConvention: CallingConvention): LLVMString = callingConvention match { case Ccc() => "ccc" case Tailcc(_) => "tailcc" diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala index 709d9e39f..ded566b62 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala @@ -26,7 +26,7 @@ object Transformer { Call("stack", Ccc(), stackType, withEmptyStack, List()), Call("_", Tailcc(false), VoidType(), transform(entry), List(LocalReference(stackType, "stack")))) val entryBlock = BasicBlock("entry", entryInstructions, RetVoid()) - val entryFunction = Function(Ccc(), VoidType(), "effektMain", List(), List(entryBlock)) + val entryFunction = Function(External(), Ccc(), VoidType(), "effektMain", List(), List(entryBlock)) declarations.map(transform) ++ globals :+ entryFunction } @@ -434,7 +434,7 @@ object Transformer { val instructions = BC.instructions; BC.instructions = null; val entryBlock = BasicBlock("entry", instructions, terminator); - val function = Function(Ccc(), VoidType(), name, parameters, entryBlock :: basicBlocks); + val function = Function(Private(), Ccc(), VoidType(), name, parameters, entryBlock :: basicBlocks); emit(function) } @@ -449,7 +449,7 @@ object Transformer { val instructions = BC.instructions; BC.instructions = null; val entryBlock = BasicBlock("entry", instructions, terminator); - val function = Function(Tailcc(true), VoidType(), name, parameters :+ Parameter(stackType, "stack"), entryBlock :: basicBlocks); + val function = Function(Private(), Tailcc(true), VoidType(), name, parameters :+ Parameter(stackType, "stack"), entryBlock :: basicBlocks); emit(function) } diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala index 6306d1f0e..6bcd5666e 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala @@ -7,13 +7,19 @@ package llvm * see: https://hackage.haskell.org/package/llvm-hs-pure-9.0.0/docs/LLVM-AST.html#t:Definition */ enum Definition { - case Function(callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], basicBlocks: List[BasicBlock]) + case Function(linkage: Linkage, callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], basicBlocks: List[BasicBlock]) case VerbatimFunction(callingConvention: CallingConvention, returnType: Type, name: String, parameters: List[Parameter], body: String) case Verbatim(content: String) case GlobalConstant(name: String, initializer: Operand) // initializer should be constant } export Definition.* +enum Linkage { + case External() + case Private() +} +export Linkage.* + enum CallingConvention { case Ccc() case Tailcc(musttail: Boolean) From d42294bfe115e7f6b97752b0b64ed6bf423ac13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Tue, 29 Apr 2025 15:57:19 +0200 Subject: [PATCH 39/41] Add bytestream-based 'random' module with PRNG and /dev/urandom handlers & fix float ops on Chez (#966) Straightforward streaming randomness with an interface supporting both bytes-based PRNG and /dev/urandom. --- examples/stdlib/acme.effekt | 1 + examples/stdlib/random.check | 65 ++++++++++++ examples/stdlib/random.effekt | 3 + libraries/common/effekt.effekt | 6 +- libraries/common/random.effekt | 186 +++++++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 examples/stdlib/random.check create mode 100644 examples/stdlib/random.effekt create mode 100644 libraries/common/random.effekt diff --git a/examples/stdlib/acme.effekt b/examples/stdlib/acme.effekt index 7ae412dfa..cffcd9178 100644 --- a/examples/stdlib/acme.effekt +++ b/examples/stdlib/acme.effekt @@ -24,6 +24,7 @@ import map import option import process import queue +import random import ref import regex import resizable_array diff --git a/examples/stdlib/random.check b/examples/stdlib/random.check new file mode 100644 index 000000000..cd10a502c --- /dev/null +++ b/examples/stdlib/random.check @@ -0,0 +1,65 @@ +prng +int32s: +-1472445053 +-935901669 +-1244218020 +492812375 +-894738723 +1372722888 +-1723450959 +2005696606 +536774910 +2111603542 +int32s, part2: +348632114 +-493311473 +521105902 +-441336655 +1315564179 +-245050234 +1432006216 +-2018660684 +349983049 +-1541851413 +1242068606 +-953174617 +728164170 +-558026150 +812040776 +-225070679 +125608749 +-1547184487 +2026319992 +-627925429 +doubles: +0.009 +0.758 +0.769 +0.032 +0.15 +0.118 +0.03 +0.946 +0.049 +0.565 +randomInt: +3 4 3 +343 +0 6 8 +68 +2 3 0 +230 +6 1 0 +610 +4 0 1 +401 +2 3 4 +234 +9 3 2 +932 +8 2 2 +822 +3 3 2 +332 +8 5 1 +851 diff --git a/examples/stdlib/random.effekt b/examples/stdlib/random.effekt new file mode 100644 index 000000000..c75109486 --- /dev/null +++ b/examples/stdlib/random.effekt @@ -0,0 +1,3 @@ +import random + +def main() = random::examples::main() diff --git a/libraries/common/effekt.effekt b/libraries/common/effekt.effekt index 06e271db0..e34c6804c 100644 --- a/libraries/common/effekt.effekt +++ b/libraries/common/effekt.effekt @@ -344,7 +344,7 @@ extern pure def infixSub(x: Double, y: Double): Double = extern pure def infixDiv(x: Double, y: Double): Double = js "(${x} / ${y})" - chez "(/ ${x} ${y})" + chez "(fl/ ${x} ${y})" llvm "%z = fdiv %Double ${x}, ${y} ret %Double %z" vm "effekt::infixDiv(Double, Double)" @@ -442,14 +442,14 @@ extern pure def toInt(d: Double): Int = extern pure def toDouble(d: Int): Double = js "${d}" - chez "${d}" + chez "(fixnum->flonum ${d})" llvm "%z = sitofp i64 ${d} to double ret double %z" vm "effekt::toDouble(Int)" extern pure def round(d: Double): Int = js "Math.round(${d})" - chez "(round ${d})" + chez "(flonum->fixnum (round ${d}))" llvm """ %i = call %Double @llvm.round.f64(double ${d}) %z = fptosi double %i to %Int ret %Int %z diff --git a/libraries/common/random.effekt b/libraries/common/random.effekt new file mode 100644 index 000000000..b1998aec7 --- /dev/null +++ b/libraries/common/random.effekt @@ -0,0 +1,186 @@ +module random + +import stream +import io/error + +/// Infinite pull stream of random bytes. +effect random(): Byte + +// --------------------- +// Sources of randomness + +/// A streaming source (push stream) of byte-level randomness +/// based on Park and Miller's MINSTD with revised parameters. +/// +/// Deterministic: needs a 32bit `seed` -- you can use `bench::timestamp`. +def minstd(seed: Int): Unit / emit[Byte] = { + // Initialize state with seed, ensuring it's not zero + var state = if (seed == 0) 1 else seed + + def nextInt(): Int = { + // Uses only at most 32-bit integers internally + // (Schrage's method: https://en.wikipedia.org/wiki/Lehmer_random_number_generator#Schrage's_method) + val a = 48271 + val m = 2147483647 + + val q = m / a // 44488 + val r = m.mod(a) // 3399 + + val div = state / q // max: M / Q = A = 48271 + val rem = state.mod(q) // max: Q - 1 = 44487 + + val s = rem * a; // max: 44487 * 48271 = 2147431977 + val t = div * r; // max: 48271 * 3399 = 164073129 + + val result = s - t + // keep the state positive + if (result < 0) result + m else result + } + + while (true) { + state = nextInt() + val b = state.mod(256).toByte + do emit(b) + } +} + +/// A thin wrapper over `minstd`, handling a reader of random bytes. +/// +/// Deterministic: needs a 32bit `seed` -- you can use `bench::timestamp`. +/// +/// Implementation is similar to `stream::source`, specialized for bytes and the `random` effect. +def minstd(seed: Int) { randomnessReader: () => Unit / random }: Unit = { + var next = box { 255.toByte } // sentinel value + next = box { + try { + minstd(seed) + <> // safe: randomness generator cannot run out of numbers... + } with emit[Byte] { v => + next = box { resume(()) } + v + } + } + + try randomnessReader() with random { + resume(next()) + } +} + +/// CSPRNG from `/dev/urandom`, handling a reader of random bytes. +/// Only works on Unix-like OSes! +def devurandom { randomnessReader: () => Unit / random }: Unit / Exception[IOError] = + try { + with readFile("/dev/urandom") + try randomnessReader() with random { + resume(do read[Byte]()) + } + } with stop { + do raise(io::error::EOF(), "Unexpected EOF when reading /dev/urandom!") + } + +// ------------------------ +// Functions using `random` +// +// Always two variants: +// - readType(): Type / random +// - readTypes(): Unit / {emit[Type], random} + +def randomByte(): Byte / random = do random() +def randomBytes(): Unit / {emit[Byte], random} = + while (true) do emit(do random()) + +def randomBool(): Bool / random = { + val b = do random() + b.toInt.mod(2) == 1 +} +def randomBools(): Unit / {emit[Bool], random} = + while (true) do emit(randomBool()) + +def randomInt32(): Int / random = { + var result = 0 + repeat(4) { + val b = do random() + result = result * 256 + b.toInt + } + val signBit = result.bitwiseShr(31).bitwiseAnd(1) == 0 + result.mod(1.bitwiseShl(31)).abs * if (signBit) 1 else -1 +} +def randomInt32s(): Unit / {emit[Int], random} = + while (true) do emit(randomInt32()) + +/// `max` is _inclusive_! +def randomInt(min: Int, max: Int): Int / random = { + if (min > max) { + randomInt(max, min) + } else { + val range = max - min + 1 + val bytesNeeded = (log(range.toDouble) / log(256.0)).ceil + + var result = 0 + repeat(bytesNeeded) { + val b = do random() + result = result * 256 + b.toInt + } + + min + (abs(result).mod(range)) + } +} +/// `max` is _inclusive_! +def randomInts(min: Int, max: Int): Unit / {emit[Int], random} = + while (true) do emit(randomInt(min, max)) + + +/// Random double between 0.0 and 1.0 +def randomDouble(): Double / random = + (randomInt32().toDouble / 1.bitwiseShl(31).toDouble).abs + // This is not perfect, but it will do for now. +def randomDoubles(): Unit / {emit[Double], random} = + while (true) do emit(randomDouble()) + + +namespace examples { +def main() = { + println("prng") + prngRandom() + } + + def prngRandom(): Unit = { + with minstd(1337); + + println("int32s:") + repeat(10) { + println(randomInt32()) + } + + println("int32s, part2:") + repeat(10) { + println(randomInt(0, 2147483647)) + println(randomInt(-2147483648, 0)) + } + + println("doubles:") + repeat(10) { + println(randomDouble().round(3)) + } + + println("randomInt:") + repeat(10) { + val a = randomInt(0, 9) + val b = randomInt(0, 9) + val c = randomInt(0, 9) + println(a.show ++ " " ++ b.show ++ " " ++ c.show) + println(a*100 + b*10 + c) + } + } + + def unixRandom(): Unit = { + with on[IOError].report; + with devurandom; + + val a = randomInt32() + val b = randomInt32() + + // This is just to use the generated numbers :) + println((a.show ++ b.show).length != 0) + } +} \ No newline at end of file From 2f58c0df7a3b60fdfaa8b708b16ef1c47862c620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lena=20K=C3=A4ufel?= Date: Wed, 30 Apr 2025 18:56:27 +0200 Subject: [PATCH 40/41] use the random effect instead of FFI --- examples/stdlib/test/list_examples_pbt.effekt | 4 ++ examples/stdlib/test/tree_examples_pbt.effekt | 7 ++- libraries/common/test.effekt | 43 +++++++------------ project/project/metals.sbt | 8 ++++ project/project/project/metals.sbt | 8 ++++ 5 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 project/project/metals.sbt create mode 100644 project/project/project/metals.sbt diff --git a/examples/stdlib/test/list_examples_pbt.effekt b/examples/stdlib/test/list_examples_pbt.effekt index 6af173551..270bfeb69 100644 --- a/examples/stdlib/test/list_examples_pbt.effekt +++ b/examples/stdlib/test/list_examples_pbt.effekt @@ -1,6 +1,10 @@ import test +import bench +import random def main() = { + with minstd(1337); + println(suite("PBT: List Functions and Other Simple Examples") { // example unit tests to show that mixed test suites work diff --git a/examples/stdlib/test/tree_examples_pbt.effekt b/examples/stdlib/test/tree_examples_pbt.effekt index 4e1190165..9b8e75b82 100644 --- a/examples/stdlib/test/tree_examples_pbt.effekt +++ b/examples/stdlib/test/tree_examples_pbt.effekt @@ -1,10 +1,11 @@ import map import stream +import random import test // user-defined generator for arbitrary, unique Integers in ascending order -def uniqueInt{body: => Unit / Generator[Int]}: Unit = { +def uniqueInt{body: => Unit / Generator[Int]}: Unit / random = { try body() with Generator[Int]{ def generate() = resume{ var next = randomInt(-100, 100) @@ -51,6 +52,8 @@ def arbitraryBinTree[K, V] (numKeys: Int) { body: => Unit / Generator[internal:: } def main()= { + with minstd(1337); + with arbitraryChar; with arbitraryString(5); with uniqueInt; @@ -88,7 +91,7 @@ def main()= { t.internal::foreach[Int, String] { (k, _v) => keys = Cons(k, keys)} with on[OutOfBounds].panic - val k = keys.get(randomInt(0, keys.size())) // only check the property for key's present in the tree (= non trivial test case) + val k = keys.get(randomInt(0, keys.size() - 1)) // only check the property for key's present in the tree (= non trivial test case) internal::get(t, compareInt, k) match { case Some(v) => { val newT = internal::put(t, compareInt, k, v) diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index b2174f074..516093195 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -2,18 +2,7 @@ import tty import process import bench import stream - -// FFI for random Integer generation for property testing -extern js""" - function getRandomInt(min, max) { - const minCeiled = Math.ceil(min); - const maxFloored = Math.floor(max); - return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive - } -""" - -extern def randomInt(minInclusive: Int, maxExclusive: Int): Int = - js"getRandomInt(${minInclusive}, ${maxExclusive})" +import random interface Assertion { def assert(condition: Bool, msg: String): Unit @@ -78,7 +67,7 @@ interface Generator[T] { def shrink(v: T): Unit/emit[T] } -def arbitraryInt { body: => Unit / Generator[Int] }: Unit = +def arbitraryInt { body: => Unit / Generator[Int] }: Unit / random = try body() with Generator[Int] { def generate() = resume { //[0, -1, 1, int.minVal, int.maxVal, -2, 2].each // This can reduce the need for shrinking by emitting common edge cases first @@ -99,7 +88,7 @@ def arbitraryInt { body: => Unit / Generator[Int] }: Unit = } // same as other arbitraryInt but for quantifier version that explicitely takes the generator as input -def arbitraryInt { body: {Generator[Int]} => Unit }: Unit = { +def arbitraryInt { body: {Generator[Int]} => Unit }: Unit /random = { try body{g} with g: Generator[Int] { def generate() = resume { while (true) { @@ -119,7 +108,7 @@ def arbitraryInt { body: {Generator[Int]} => Unit }: Unit = { } } -def chooseInt(minInclusive: Int, maxExclusive: Int) { body: => Unit / Generator[Int] }: Unit = +def chooseInt(minInclusive: Int, maxExclusive: Int) { body: => Unit / Generator[Int] }: Unit / random= try body() with Generator[Int] { def generate() = resume { while (true) { @@ -138,11 +127,11 @@ def chooseInt(minInclusive: Int, maxExclusive: Int) { body: => Unit / Generator[ } } -def arbitraryBool { body: => Unit/Generator[Bool] }: Unit = +def arbitraryBool { body: => Unit/Generator[Bool] }: Unit / random= try body() with Generator[Bool]{ def generate() = resume { while (true) { - do emit(random() > 0.5) + do emit(randomBool()) } } @@ -157,13 +146,11 @@ def arbitraryBool { body: => Unit/Generator[Bool] }: Unit = } } -def arbitraryDouble { body: => Unit/Generator[Double] }: Unit = +def arbitraryDouble { body: => Unit/Generator[Double] }: Unit / random = try body() with Generator[Double]{ def generate() = resume { - val min = -1000.0 // TODO use double min value - val max = 1000.0 // TODO use double max value while (true) { - do emit(min + (max - min) * random()) + do emit(randomDouble()) } } @@ -179,7 +166,7 @@ def arbitraryDouble { body: => Unit/Generator[Double] }: Unit = } -def arbitraryChar { body: => Unit / Generator[Char] }: Unit = +def arbitraryChar { body: => Unit / Generator[Char] }: Unit / random = try body() with Generator[Char] { def generate() = resume { // ASCII printable characters range from 32 to 126 @@ -191,7 +178,7 @@ def arbitraryChar { body: => Unit / Generator[Char] }: Unit = def shrink(v: Char) = resume { <> } } -def arbitraryString { body: => Unit / Generator[String] }: Unit / Generator[Char] = { +def arbitraryString { body: => Unit / Generator[String] }: Unit / {Generator[Char], random } = { try body() with Generator[String] { def generate() = resume { while (true) { @@ -208,7 +195,7 @@ def arbitraryString { body: => Unit / Generator[String] }: Unit / Generator[Char } } -def arbitraryString(len: Int) { body: => Unit / Generator[String] }: Unit / Generator[Char] = { +def arbitraryString(len: Int) { body: => Unit / Generator[String] }: Unit / { Generator[Char], random } = { try body() with Generator[String] { def generate() = resume { while (true) { @@ -225,7 +212,7 @@ def arbitraryString(len: Int) { body: => Unit / Generator[String] }: Unit / Gene } } -def arbitraryList[T] { body: => Unit / Generator[List[T]]} : Unit / Generator[T] = { +def arbitraryList[T] { body: => Unit / Generator[List[T]]} : Unit / { Generator[T], random } = { try body() with Generator[List[T]]{ def generate() = resume { while (true) { @@ -242,7 +229,7 @@ def arbitraryList[T] { body: => Unit / Generator[List[T]]} : Unit / Generator[T] } } -def arbitraryList[T](len: Int) { body: => Unit/Generator[List[T]]} : Unit / Generator[T] = { +def arbitraryList[T](len: Int) { body: => Unit/Generator[List[T]]} : Unit / { Generator[T], random } = { try body() with Generator[List[T]]{ def generate() = resume { while (true) { @@ -259,7 +246,7 @@ def arbitraryList[T](len: Int) { body: => Unit/Generator[List[T]]} : Unit / Gene } } -def evenNumbers { body: => Unit / Generator[Int] }: Unit = +def evenNumbers { body: => Unit / Generator[Int] }: Unit / random = try body() with Generator[Int] { def generate() = resume { while(true) { @@ -280,7 +267,7 @@ def evenNumbers { body: => Unit / Generator[Int] }: Unit = } } -def alphabeticChar { body: => Unit / Generator[Char] }: Unit = { +def alphabeticChar { body: => Unit / Generator[Char] }: Unit / random = { try body() with Generator[Char] { def generate() = resume { // ASCII printable characters range from 32 to 126 diff --git a/project/project/metals.sbt b/project/project/metals.sbt new file mode 100644 index 000000000..ce00deb36 --- /dev/null +++ b/project/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.9") + +// format: on diff --git a/project/project/project/metals.sbt b/project/project/project/metals.sbt new file mode 100644 index 000000000..ce00deb36 --- /dev/null +++ b/project/project/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "2.0.9") + +// format: on From 539a8ae0cd13b1886d0aa2ed4ce6fbbf42d51243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lena=20K=C3=A4ufel?= Date: Wed, 30 Apr 2025 19:11:30 +0200 Subject: [PATCH 41/41] added check files for golden tests, removed leftover debug print --- examples/stdlib/test/list_examples_pbt.check | 50 +++++++++++++++++++ examples/stdlib/test/list_examples_pbt.effekt | 9 ++-- examples/stdlib/test/tree_examples_pbt.check | 9 ++++ examples/stdlib/test/tree_examples_pbt.effekt | 7 ++- libraries/common/test.effekt | 1 - 5 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 examples/stdlib/test/list_examples_pbt.check create mode 100644 examples/stdlib/test/tree_examples_pbt.check diff --git a/examples/stdlib/test/list_examples_pbt.check b/examples/stdlib/test/list_examples_pbt.check new file mode 100644 index 000000000..615e5f0a9 --- /dev/null +++ b/examples/stdlib/test/list_examples_pbt.check @@ -0,0 +1,50 @@ +PBT: List Functions and Other Simple Examples +✓ 1 + 1 == 2 [0.87ms] +✕ 2 + 2 == 3 [0.14ms] + Expected: 3 + Obtained: 4 +✓ reverse-singleton, passed 100 tests [3.16ms] +✓ reverse-singleton, explicitely passing the generator, passed 100 tests [1.59ms] +✕ reverse-singleton mistake [0.15ms] + ! Falsified after 0 passed tests: [0.15ms] + Expected: Cons(6, Nil()) + Obtained: Cons(47, Nil()) + failed on input: + 1. 47 +✓ Is the Integer 2 among the generated values? [0.40ms] + Verified after 18 tried inputs + example value: + 1. 2 +✓ reverse: distributivity over append, passed 100 tests [4.36ms] +✕ reverse: distributivity mistake - swapped order [0.72ms] + ! Falsified after 0 passed tests: [0.72ms] + Expected: Cons(-45, Cons(43, Cons(15, Cons(0, Nil())))) + Obtained: Cons(15, Cons(0, Cons(-45, Cons(43, Nil())))) + failed on inputs: + 1. Cons(43, Cons(-45, Nil())) + 2. Cons(0, Cons(15, Nil())) +✓ |zip(xs,ys)| === min(|xs|,|ys|), passed 10 tests [0.83ms] +✕ intended mistake: |zip(xs,ys)| != max(|xs|,|ys|) [0.39ms] + ! Falsified after 0 passed tests: [0.39ms] + Expected: 5 + Obtained: 2 + failed on inputs: + 1. Cons(102, Cons(97, Cons(41, Cons(123, Cons(122, Nil()))))) + 2. Cons(42, Cons(-46, Nil())) +✓ unzip-zip-take relation, passed 10 tests [3.92ms] +✓ drop: concatenation , passed 10 tests [1.94ms] +✓ drop-slice relation, passed 10 tests [0.64ms] +✓ reverseOnto-reverse-append relation , passed 10 tests [0.84ms] +✓ size-foldLeft relation, passed 20 tests [1.14ms] +✓ take-drop: inversion over concatenation, passed 20 tests [0.77ms] +✕ even numbers are even and smaller than 4 [0.11ms] + ! Falsified after 1 passed test: [0.11ms] + Assertion failed + failed on input: + 1. 12 + + 12 pass + 5 fail + 17 test(s) total [41.5ms] + +false \ No newline at end of file diff --git a/examples/stdlib/test/list_examples_pbt.effekt b/examples/stdlib/test/list_examples_pbt.effekt index 270bfeb69..b02e263b0 100644 --- a/examples/stdlib/test/list_examples_pbt.effekt +++ b/examples/stdlib/test/list_examples_pbt.effekt @@ -4,8 +4,9 @@ import random def main() = { with minstd(1337); - - println(suite("PBT: List Functions and Other Simple Examples") { + + // Don't print out times in CI. + suite("PBT: List Functions and Other Simple Examples", false) { // example unit tests to show that mixed test suites work test("1 + 1 == 2") { @@ -165,6 +166,8 @@ def main() = { assertTrue(n.mod(2) == 0) assertTrue(n <= 4) } - }) + }; + + () } \ No newline at end of file diff --git a/examples/stdlib/test/tree_examples_pbt.check b/examples/stdlib/test/tree_examples_pbt.check new file mode 100644 index 000000000..fb65e1c93 --- /dev/null +++ b/examples/stdlib/test/tree_examples_pbt.check @@ -0,0 +1,9 @@ +PBT: Tree Map Examples +✓ get-put law, passed 100 tests +✓ put-put law, passed 100 tests +✓ put-get law, passed 100 tests +✓ forget-map relation, passed 100 tests + + 4 pass + 0 fail + 4 test(s) total \ No newline at end of file diff --git a/examples/stdlib/test/tree_examples_pbt.effekt b/examples/stdlib/test/tree_examples_pbt.effekt index 9b8e75b82..a3a0069d0 100644 --- a/examples/stdlib/test/tree_examples_pbt.effekt +++ b/examples/stdlib/test/tree_examples_pbt.effekt @@ -59,7 +59,8 @@ def main()= { with uniqueInt; with arbitraryBinTree[Int, String](8); - println(suite("PBT: Tree Map Examples") { + // Don't print out times in CI. + suite("PBT: Tree Map Examples", false) { // get-put law: get(put(M, K, V), K) = V forall[String, internal::Tree[Int, String], Int]("get-put law", 100) @@ -106,5 +107,7 @@ def main()= { val tMap = internal::map(t){ (_k, _v) => ()} assertEqual(tForget, tMap) } - }) + }; + + () } \ No newline at end of file diff --git a/libraries/common/test.effekt b/libraries/common/test.effekt index 516093195..92f5d5858 100644 --- a/libraries/common/test.effekt +++ b/libraries/common/test.effekt @@ -510,7 +510,6 @@ def exists[A, B](name: String, n: Int) /// } /// ``` def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = { - println("here") with Formatted::formatting; def ms(duration: Int): String / Formatted =