diff --git a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala index 1086dbc0f..f74cb0e21 100644 --- a/effekt/jvm/src/test/scala/effekt/StdlibTests.scala +++ b/effekt/jvm/src/test/scala/effekt/StdlibTests.scala @@ -23,7 +23,9 @@ abstract class StdlibChezTests extends StdlibTests { override def ignored: List[File] = List( // Not implemented yet examplesDir / "stdlib" / "bytearray", - examplesDir / "stdlib" / "io" + examplesDir / "stdlib" / "io", + examplesDir / "stdlib" / "stream" / "characters.effekt", + examplesDir / "stdlib" / "stream" / "fuse_newlines.effekt" ) } class StdlibChezSchemeMonadicTests extends StdlibChezTests { @@ -44,6 +46,8 @@ class StdlibLLVMTests extends StdlibTests { // Valgrind leak/failure examplesDir / "stdlib" / "bytearray" / "bytearray.effekt", + examplesDir / "stdlib" / "stream" / "characters.effekt", + examplesDir / "stdlib" / "stream" / "fuse_newlines.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "async_file_io.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "files.effekt", examplesDir / "stdlib" / "io" / "filesystem" / "wordcount.effekt", diff --git a/examples/benchmarks/input_output/word_count_ascii.effekt b/examples/benchmarks/input_output/word_count_ascii.effekt index e085373ad..9f2c0a1df 100644 --- a/examples/benchmarks/input_output/word_count_ascii.effekt +++ b/examples/benchmarks/input_output/word_count_ascii.effekt @@ -28,7 +28,8 @@ def countWords(): Output / read[Byte] = { var lines = 0 var wasSpace = true - exhaustively { do read[Byte]() } { c => + exhaustively { + val c = do read[Byte]() chars = chars + 1 diff --git a/examples/benchmarks/input_output/word_count_utf8.effekt b/examples/benchmarks/input_output/word_count_utf8.effekt index fd8243618..9a15b8297 100644 --- a/examples/benchmarks/input_output/word_count_utf8.effekt +++ b/examples/benchmarks/input_output/word_count_utf8.effekt @@ -17,7 +17,8 @@ def countWords(): Output / read[Char] = { var lines = 0 var wasSpace = true - exhaustively[Char] { do read() } { c => + exhaustively { + val c = do read[Char]() chars = chars + 1 diff --git a/examples/stdlib/stream/characters.check b/examples/stdlib/stream/characters.check new file mode 100644 index 000000000..336942ce6 --- /dev/null +++ b/examples/stdlib/stream/characters.check @@ -0,0 +1,6 @@ +H (72) +e (101) +l (108) +l (108) +o (111) +Cons(H, Cons(e, Cons(l, Cons(l, Cons(o, Nil()))))) \ No newline at end of file diff --git a/examples/stdlib/stream/characters.effekt b/examples/stdlib/stream/characters.effekt new file mode 100644 index 000000000..7bf66205b --- /dev/null +++ b/examples/stdlib/stream/characters.effekt @@ -0,0 +1,11 @@ +import stream + +def main() = { + for[Char] { each("Hello") } { c => + println(show(c) ++ " (" ++ show(c.toInt) ++ ")") + } + + val list = collectList[Char] { each("Hello") } + println(list.map { c => c.show }) +} + diff --git a/examples/stdlib/stream/fibonacci.check b/examples/stdlib/stream/fibonacci.check new file mode 100644 index 000000000..912327036 --- /dev/null +++ b/examples/stdlib/stream/fibonacci.check @@ -0,0 +1,2 @@ +The first 10 Fibonacci numbers: +Cons(0, Cons(1, Cons(1, Cons(2, Cons(3, Cons(5, Cons(8, Cons(13, Cons(21, Cons(34, Nil())))))))))) diff --git a/examples/stdlib/stream/fibonacci.effekt b/examples/stdlib/stream/fibonacci.effekt new file mode 100644 index 000000000..7b3e3a4b2 --- /dev/null +++ b/examples/stdlib/stream/fibonacci.effekt @@ -0,0 +1,20 @@ +import stream + +def main() = { + val max = 10 + + val fibs = collectList[Int] { + var a = 0 + var b = 1 + + replicate(max) { + val current = a + val next = a + b + a = b + b = next + current + } + } + println("The first " ++ show(max) ++ " Fibonacci numbers:") + println(fibs) +} \ No newline at end of file diff --git a/examples/stdlib/stream/fuse_newlines.check b/examples/stdlib/stream/fuse_newlines.check new file mode 100644 index 000000000..252ac0b51 --- /dev/null +++ b/examples/stdlib/stream/fuse_newlines.check @@ -0,0 +1,3 @@ +ab +c +de \ No newline at end of file diff --git a/examples/stdlib/stream/fuse_newlines.effekt b/examples/stdlib/stream/fuse_newlines.effekt new file mode 100644 index 000000000..3c050c347 --- /dev/null +++ b/examples/stdlib/stream/fuse_newlines.effekt @@ -0,0 +1,32 @@ +import stream + +def fuseNewlines(): Nothing / {read[Char], emit[Char], stop} = { + val c = do read[Char]() + if (c == '\n') { + do emit(c) + skipNewlines() + } else { + do emit(c) + fuseNewlines() + } +} + +def skipNewlines(): Nothing / {read[Char], emit[Char], stop} = { + val c = do read[Char]() + if (c == '\n') { + skipNewlines() + } else { + do emit(c) + fuseNewlines() + } +} + +def main() = { + with feed("ab\n\nc\nde") + println(collectString { + with exhaustively + fuseNewlines() + }) +} + + diff --git a/examples/stdlib/stream/neighbours.check b/examples/stdlib/stream/neighbours.check new file mode 100644 index 000000000..f57de8fd3 --- /dev/null +++ b/examples/stdlib/stream/neighbours.check @@ -0,0 +1,35 @@ +The immediate neighbours of [4, 3] are: +[3, 2] +[3, 3] +[3, 4] +[4, 2] +[4, 4] +[5, 2] +[5, 3] +[5, 4] + +The neighbours and their neighbours of [4, 3] are: +[2, 1] +[2, 2] +[2, 3] +[2, 4] +[2, 5] +[3, 1] +[3, 2] +[3, 3] +[3, 4] +[3, 5] +[4, 1] +[4, 2] +[4, 4] +[4, 5] +[5, 1] +[5, 2] +[5, 3] +[5, 4] +[5, 5] +[6, 1] +[6, 2] +[6, 3] +[6, 4] +[6, 5] diff --git a/examples/stdlib/stream/neighbours.effekt b/examples/stdlib/stream/neighbours.effekt new file mode 100644 index 000000000..2cd3639b2 --- /dev/null +++ b/examples/stdlib/stream/neighbours.effekt @@ -0,0 +1,38 @@ +import stream + +record Pos(x: Int, y: Int) + +def equals(left: Pos, right: Pos) = (left, right) match { + case (Pos(lx, ly), Pos(rx, ry)) => lx == rx && ly == ry +} + +def show(p: Pos) = "[" ++ show(p.x) ++ ", " ++ show(p.y) ++ "]" + +/// Gets the neighbours of a given position. +/// with radius=1, those are immediate neighbours (including the diagonal) +/// with radius=2, these are neighbours&their neighbours +/// ... +def neighboursOf(pos: Pos, radius: Int) = { + with val dx = for[Int] { range(neg(radius), radius + 1) } + with val dy = for[Int] { range(neg(radius), radius + 1) } + val newPosition = Pos(pos.x + dx, pos.y + dy) + do emit(newPosition) +} + +def main() = { + val start = Pos(4, 3) + + println("The immediate neighbours of " ++ show(start) ++ " are:") + for[Pos] { start.neighboursOf(1) } { + case p and not(p.equals(start)) => println(show(p)) + case _ => () + } + + println("") + + println("The neighbours and their neighbours of " ++ show(start) ++ " are:") + for[Pos] { start.neighboursOf(2) } { + case p and not(p.equals(start)) => println(show(p)) + case _ => () + } +} \ No newline at end of file diff --git a/examples/stdlib/stream/sum_of_squares.check b/examples/stdlib/stream/sum_of_squares.check new file mode 100644 index 000000000..3c526cc17 --- /dev/null +++ b/examples/stdlib/stream/sum_of_squares.check @@ -0,0 +1,2 @@ +The sum of squares from 1 to 10 is: +385 diff --git a/examples/stdlib/stream/sum_of_squares.effekt b/examples/stdlib/stream/sum_of_squares.effekt new file mode 100644 index 000000000..7f44ab356 --- /dev/null +++ b/examples/stdlib/stream/sum_of_squares.effekt @@ -0,0 +1,16 @@ +import stream + +def squares(): Unit / emit[Int] = + for[Int] { rangeFrom(1) } { n => + do emit(n * n) + } + + +def main() = { + val max = 10 + println("The sum of squares from 1 to " ++ show(max) ++ " is:") + println(sum { + with limit[Int](max + 1) + squares() + }) +} \ No newline at end of file diff --git a/examples/stdlib/stream/zip.check b/examples/stdlib/stream/zip.check new file mode 100644 index 000000000..771bb4464 --- /dev/null +++ b/examples/stdlib/stream/zip.check @@ -0,0 +1,5 @@ +1, H +2, e +3, l +4, l +5, o \ No newline at end of file diff --git a/examples/stdlib/stream/zip.effekt b/examples/stdlib/stream/zip.effekt new file mode 100644 index 000000000..64a4c8d5b --- /dev/null +++ b/examples/stdlib/stream/zip.effekt @@ -0,0 +1,11 @@ +import stream + +def main() = { + def stream1() = rangeFrom(1) + // Ideally this would be an array to demonstrate the capabilities. + def stream2() = ['H', 'e', 'l', 'l', 'o'].each + + zip[Int, Char]{stream1}{stream2} { (a, b) => + println(show(a) ++ ", " ++ show(b)) + } +} \ No newline at end of file diff --git a/libraries/common/stream.effekt b/libraries/common/stream.effekt index 5d11e2709..6337c1f1c 100644 --- a/libraries/common/stream.effekt +++ b/libraries/common/stream.effekt @@ -11,7 +11,7 @@ import io/error /// Describes push streams by emitting values of type `A`. effect emit[A](value: A): Unit -/// Describes pull streams by requiring values of type `A`. +/// Describes pull streams by reading values of type `A`. /// /// The producer can decide to `stop` emitting values. effect read[A](): A / stop @@ -28,20 +28,14 @@ def for[A, R] { stream: () => R / emit[A] } { action: A => Unit }: R = resume(action(value)) } -/// Like `for[A, R]`, but fixes the result of producing computation to the -/// common type `Unit`. This simplifies annotating the type of stream -/// elements `A` without having to also annotate `R`. +/// Like `for[A, R]`, but ignores the result of the stream, and consequently +/// works for any type. Use this to annotate the type of stream elements +/// `A` without having to also annotate `R`. /// /// e.g. for[Int] { prog() } { el => println(el) } -def for[A] { stream: () => Unit / emit[A] } { action: A => Unit }: Unit = - for[A, Unit]{stream}{action} - -def continually[A] { action: () => A }: Nothing / emit[A] = { - def go(): Nothing = { - do emit(action()) - go() - } - go() +def for[A] { stream: () => Any / emit[A] } { action: A => Unit }: Unit = { + for[A, Any]{stream}{action} + () } /// Turns a `list` into a producer of a push stream @@ -91,23 +85,28 @@ def boundary[R] { program: () => R / stop }: Option[R] = try { Some(program()) } with stop { - def stop() = None() + None() } -def exhaustively[A] { program: () => A / stop } { action: A => Unit }: Unit = +def boundary { program: () => Any / stop }: Unit = { + boundary[Any]{program} + () +} + +/// Run `program` forever until `stop` is thrown. +def exhaustively { program: () => Any / stop }: Unit = try { def go(): Unit = { - action(program()) + program() go() } go() } with stop { - def stop() = () + () } -// In Effekt lower bounds are inclusive and upper bounds are exclusive - +/// In Effekt lower bounds are inclusive and upper bounds are exclusive record Indexed[A](index: Int, value: A) def range(lower: Int, upper: Int): Unit / emit[Int] = @@ -137,7 +136,7 @@ def index[A, R] { stream: () => R / emit[A] }: R / emit[Indexed[A]] = { } /// If `number` is zero or negative it does nothing -def take[A, R](number: Int) { stream: () => R / emit[A] }: R / { emit[A], stop } = { +def limit[A, R](number: Int) { stream: () => R / emit[A] }: R / { emit[A], stop } = { if (number <= 0) do stop(); var i = 1; try { @@ -152,6 +151,13 @@ def take[A, R](number: Int) { stream: () => R / emit[A] }: R / { emit[A], stop } } } +/// If `number` is zero or negative it does nothing +def limit[A](number: Int) { stream: () => Any / emit[A] }: Unit / emit[A] = + boundary { + limit[A, Any](number){stream} + } + + /// If `number` is zero or negative it does nothing def replicate[A](number: Int) { action: () => A }: Unit / emit[A] = if (number > 0) { @@ -170,8 +176,8 @@ def sum[R] { stream : () => R / emit[Int] }: (R, Int) = { } } -def sum { stream : () => Unit / emit[Int] }: Int = - sum[Unit]{stream}.second +def sum { stream : () => Any / emit[Int] }: Int = + sum[Any]{stream}.second def collectList[A, R] { stream: () => R / emit[A] }: (R, List[A]) = try { @@ -181,8 +187,8 @@ def collectList[A, R] { stream: () => R / emit[A] }: (R, List[A]) = (r, Cons(v, vs)) } -def collectList[A] { stream: () => Unit / emit[A] }: List[A] = - collectList[A, Unit]{stream}.second +def collectList[A] { stream: () => Any / emit[A] }: List[A] = + collectList[A, Any]{stream}.second def collectArray[A, R] { stream: () => R / emit[A] }: (R, Array[A]) = { var i = 0 @@ -197,8 +203,8 @@ def collectArray[A, R] { stream: () => R / emit[A] }: (R, Array[A]) = { } } -def collectArray[A] { stream: () => Unit / emit[A] }: Array[A] = - collectArray[A, Unit]{stream}.second +def collectArray[A] { stream: () => Any / emit[A] }: Array[A] = + collectArray[A, Any]{stream}.second def collectBytes[R] { stream: () => R / emit[Byte] }: (R, ByteArray) = { var i = 0 @@ -213,10 +219,10 @@ def collectBytes[R] { stream: () => R / emit[Byte] }: (R, ByteArray) = { } } -def collectBytes { stream: () => Unit / emit[Byte] }: ByteArray = - collectBytes[Unit]{stream}.second +def collectBytes { stream: () => Any / emit[Byte] }: ByteArray = + collectBytes[Any]{stream}.second -def feed[T](list: List[T]) { reader: () => Unit / read[T] } = { +def feed[T, R](list: List[T]) { reader: () => R / read[T] }: R = { var l = list try { reader() @@ -232,14 +238,16 @@ def feed[T](list: List[T]) { reader: () => Unit / read[T] } = { } } -def feed[T](array: Array[T]) { reader: () => Unit / read[T] } = { +def feed[T, R](array: Array[T]) { reader: () => R / read[T] }: R = { var i = 0 try { reader() } with read[T] { resume { if (i < array.size) { - array.unsafeGet(i) + val c = i + i = c + 1 + array.unsafeGet(c) } else { do stop() } @@ -247,14 +255,16 @@ def feed[T](array: Array[T]) { reader: () => Unit / read[T] } = { } } -def feed(bytes: ByteArray) { reader: () => Unit / read[Byte] } = { +def feed[R](bytes: ByteArray) { reader: () => R / read[Byte] }: R = { var i = 0 try { reader() } with read[Byte] { resume { if (i < bytes.size) { - bytes.unsafeGet(i) + val c = i + i = c + 1 + bytes.unsafeGet(c) } else { do stop() } @@ -262,7 +272,7 @@ def feed(bytes: ByteArray) { reader: () => Unit / read[Byte] } = { } } -def source[A, R] { stream: () => Unit / emit[A] } { reader: () => R / read[A] }: R = { +def source[A, R] { stream: () => Any / emit[A] } { reader: () => R / read[A] }: R = { var next = box { None() } next = box { try { @@ -282,6 +292,21 @@ def source[A, R] { stream: () => Unit / emit[A] } { reader: () => R / read[A] }: } } +def source[A] { stream: () => Any / emit[A] } { reader: () => Any / read[A] }: Unit = { + source[A, Any]{stream}{reader} + () +} + +/// Combines two streams together producing a stream of pairs in lockstep. +/// Given two streams of length `n` and `m`, it produces a stream of length `min(n, m)`. +def zip[A, B] { stream1: () => Any / emit[A] } { stream2: () => Any / emit[B] } { action: (A, B) => Any }: Unit = { + with source[A] { stream1 } + with source[B] { stream2 } + + exhaustively { + action(do read[A](), do read[B]()) + } +} def writeFile[R](path: String) { stream: () => R / emit[Byte] }: R / Exception[IOError] = { @@ -407,13 +432,15 @@ def feed(string: String) { reader: () => Unit / read[Char] } = def each(string: String): Unit / emit[Char] = feed(string) { - exhaustively[Char] { do read() } { char => do emit(char) } + exhaustively { + do emit[Char](do read[Char]()) + } } def collectString[R] { stream: () => R / emit[Char] }: (R, String) = { - val (result, bytes) = collectBytes { encodeUTF8 { stream } } + val (result, bytes) = collectBytes[R] { encodeUTF8 { stream } } (result, bytes.toString) } -def collectString { stream: () => Unit / emit[Char] }: String = - collectString[Unit]{stream}.second +def collectString { stream: () => Any / emit[Char] }: String = + collectString[Any]{stream}.second