@effekt-lang/effekt 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +6 -18
  2. package/bin/effekt +0 -0
  3. package/libraries/common/args.effekt +0 -27
  4. package/libraries/common/array.effekt +11 -16
  5. package/libraries/common/bench.effekt +32 -6
  6. package/libraries/common/effekt.effekt +30 -69
  7. package/libraries/common/exception.effekt +0 -1
  8. package/libraries/common/io/console.effekt +32 -36
  9. package/libraries/common/io/error.effekt +111 -2
  10. package/libraries/common/io/filesystem.effekt +274 -0
  11. package/libraries/common/io/network.effekt +30 -54
  12. package/libraries/common/io/time.effekt +9 -12
  13. package/libraries/common/io.effekt +54 -124
  14. package/libraries/common/list.effekt +18 -0
  15. package/libraries/common/process.effekt +0 -1
  16. package/libraries/common/ref.effekt +4 -7
  17. package/libraries/common/string/tty.effekt +66 -0
  18. package/libraries/common/string.effekt +0 -34
  19. package/libraries/common/test.effekt +96 -34
  20. package/libraries/js/unsafe/cont.effekt +1 -1
  21. package/libraries/llvm/io.c +258 -378
  22. package/libraries/llvm/main.c +3 -0
  23. package/libraries/llvm/rts.ll +57 -38
  24. package/libraries/llvm/types.c +10 -1
  25. package/licenses/eclipse public license, version 2.0 - epl-2.0.html +4 -4
  26. package/licenses/mit license - mit-license.html +19 -19
  27. package/licenses/the bsd license - bsd-license.html +19 -19
  28. package/licenses/the mit license - mit.html +20 -20
  29. package/package.json +1 -1
  30. package/libraries/chez/lift/effekt.ss +0 -118
  31. package/libraries/common/io/files.effekt +0 -282
  32. package/libraries/js/io.js +0 -143
  33. package/libraries/llvm/io.ll +0 -19
  34. package/libraries/ml/effekt.sml +0 -65
  35. package/libraries/ml/internal/mllist.effekt +0 -9
  36. package/libraries/ml/internal/mloption.effekt +0 -18
  37. package/libraries/ml/random.effekt +0 -85
  38. package/libraries/ml/text/regex.effekt +0 -102
@@ -3,52 +3,71 @@ module io
3
3
  import ref
4
4
  import queue
5
5
 
6
- extern include llvm "../llvm/io.ll"
7
- extern include js "../js/io.js"
8
-
9
- // Asynchronous I/O
10
- // ----------------
11
-
12
- interface AsyncIO {
13
- /**
14
- * Runs the provided computation and conceptually waits until it completes by
15
- * calling the writer-continuation.
16
- *
17
- * The provided writer-continuation should only ever be called ONCE.
18
- * The computation is passed as a thunk in order to prevent accidental usage of
19
- * other control effects that would violate the contract of calling the writer
20
- * once.
21
- */
22
- def performIO[T](computation: (T => Unit at {io, global}) => Unit at {io, global}): T
23
- }
6
+ // Event Loop
7
+ // ----------
24
8
 
25
- /**
26
- * Run an IO computation (that uses callbacks) here.
27
- */
28
- def blocking(program: () => Unit / AsyncIO at {io, global}): Unit =
29
- try { program() }
30
- with AsyncIO {
31
- def performIO[T](computation) = computation(resume)
32
- }
9
+ type Task[T] = () => T at {io, async, global}
33
10
 
11
+ extern llvm """
12
+ declare void @c_yield(%Stack)
13
+ """
14
+
15
+ extern async def spawn(task: Task[Unit]): Unit =
16
+ js "$effekt.callcc(callback => { setTimeout(() => callback($effekt.unit), 0); ${task}().run()})"
17
+ llvm """
18
+ call void @c_yield(%Stack %stack)
19
+ call void @run(%Neg ${task})
20
+ ret void
21
+ """
22
+
23
+ extern async def yield(): Unit =
24
+ js "$effekt.callcc(callback => setTimeout(() => callback($effekt.unit), 0))"
25
+ llvm """
26
+ call void @c_yield(%Stack %stack)
27
+ ret void
28
+ """
29
+
30
+ extern async def abort(): Nothing =
31
+ js "$effekt.callcc(callback => { return })"
32
+ llvm """
33
+ call void @eraseStack(%Stack %stack)
34
+ ret void
35
+ """
34
36
 
35
- interface Concurrent {
36
- def yield(): Unit
37
- def abort(): Nothing
38
- }
39
37
 
40
38
  // Promises
41
39
  // --------
42
40
 
43
41
  extern type Promise[T]
44
- // = js "{promise: Promise, resolve: ƒ}"
45
- // = llvm "Promise"
42
+ // = js "{resolve: ƒ, promise: Promise}"
43
+ // = llvm "{tag: 0, obj: Promise*}"
46
44
 
47
- interface Promises {
48
- def promise[T](p: Task[T]): Promise[T]
49
- def await[T](p: Promise[T]): T
45
+ def promise[T](task: Task[T]): Promise[T] = {
46
+ val p = promise::make[T]();
47
+ spawn(box { p.resolve(task()) });
48
+ return p
50
49
  }
51
50
 
51
+ extern llvm """
52
+ declare %Pos @c_promise_make()
53
+ declare void @c_promise_resolve(%Pos, %Pos)
54
+ declare void @c_promise_await(%Pos, %Neg)
55
+ """
56
+
57
+ extern async def await[T](promise: Promise[T]): T =
58
+ js "$effekt.callcc(callback => ${promise}.promise.then(res => callback(res)))"
59
+ llvm """
60
+ call void @c_promise_await(%Pos ${promise}, %Stack %stack)
61
+ ret void
62
+ """
63
+
64
+ extern async def resolve[T](promise: Promise[T], value: T): Unit =
65
+ js "$effekt.callcc(callback => { ${promise}.resolve(${value}); callback($effekt.unit)})"
66
+ llvm """
67
+ call void @c_promise_resolve(%Pos ${promise}, %Pos ${value}, %Stack %stack)
68
+ ret void
69
+ """
70
+
52
71
  namespace promise {
53
72
  extern js """
54
73
  function promise$make() {
@@ -63,96 +82,7 @@ namespace promise {
63
82
  extern io def make[T](): Promise[T] =
64
83
  js "promise$make()"
65
84
  llvm """
66
- ; Get the default loop and run it.
67
85
  %promise = call %Pos @c_promise_make()
68
86
  ret %Pos %promise
69
87
  """
70
-
71
- extern io def resolve[T](p: Promise[T], value: T): Unit =
72
- js "${p}.resolve(${value})"
73
- llvm """
74
- ; Get the default loop and run it.
75
- call void @c_promise_resolve(%Pos ${p}, %Pos ${value})
76
- ret %Pos zeroinitializer
77
- """
78
-
79
- extern io def await[T](p: Promise[T], callback: T => Unit at {io, global}): Unit =
80
- js "${p}.promise.then(res => (${callback})(res).run())"
81
- llvm """
82
- ; Get the default loop and run it.
83
- call void @c_promise_await(%Pos ${p}, %Neg ${callback})
84
- ret %Pos zeroinitializer
85
- """
86
- }
87
-
88
-
89
- // TODO we cannot add effects Files, Console, etc. to Task since modules cannot be recursive, at the moment.
90
- effect IO = { AsyncIO, Concurrent, Promises }
91
-
92
- type Task[T] = () => T / IO at {io, global}
93
-
94
-
95
- // Event Loop
96
- // ----------
97
-
98
- def eventloop(prog: Task[Unit]) = {
99
-
100
- def handle(p: Task[Unit]): Unit =
101
- try { p() }
102
- with Concurrent {
103
- def yield() = eventloop::schedule(box { resume(()) })
104
- def abort() = ()
105
- } with AsyncIO {
106
- // TODO there is a bug in our LLVM backend where eta-reducing the following will break
107
- def performIO[T](computation) = computation(box { t =>
108
- eventloop::schedule(box { resume(t) })
109
- })
110
- } with Promises {
111
- def promise[T](task: Task[T]) = {
112
- val p = promise::make[T]();
113
- handle(box {
114
- val t = task()
115
- promise::resolve(p, t)
116
- })
117
- eventloop::schedule( box { resume(p) })
118
- }
119
- def await[T](p: Promise[T]) = promise::await(p, box { t => resume(t) })
120
- }
121
-
122
- handle(prog)
123
-
124
- eventloop::start()
125
- eventloop::stop()
126
- }
127
-
128
- namespace eventloop {
129
- extern io def schedule(program: () => Unit at {io, global}): Unit =
130
- js "setTimeout(() => (${program})().run(), 0)"
131
- llvm """
132
- call void @c_timer_wait(i64 0, %Neg ${program})
133
- ret %Pos zeroinitializer
134
- """
135
-
136
- extern io def start(): Unit =
137
- js "$effekt.unit"
138
- llvm """
139
- ; Get the default loop and run it.
140
- %loop = call ptr @uv_default_loop()
141
- %run_result = call i32 @uv_run(ptr %loop, i32 0)
142
-
143
- ; return Unit
144
- ret %Pos zeroinitializer
145
- """
146
-
147
- extern io def stop(): Unit =
148
- js "$effekt.unit"
149
- llvm """
150
- ; Get the default loop, stop and close it.
151
- %loop = call ptr @uv_default_loop()
152
- call void @uv_stop(ptr %loop)
153
- call void @uv_loop_close(ptr %loop)
154
-
155
- ; return Unit
156
- ret %Pos zeroinitializer
157
- """
158
88
  }
@@ -222,6 +222,24 @@ def any[A](list: List[A]) { predicate: A => Bool }: Bool = {
222
222
  }
223
223
  }
224
224
 
225
+ // Fold a list using `f`, starting from the left given a starting value.
226
+ //
227
+ // O(N)
228
+ def foldLeft[A, B](l: List[A], init: B) { f: (B, A) => B }: B = {
229
+ var acc = init;
230
+ l.foreach { x => acc = f(acc, x) };
231
+ acc
232
+ }
233
+
234
+ // Fold a list using `f`, starting from the right given a starting value.
235
+ //
236
+ // O(N)
237
+ def foldRight[A, B](l: List[A], init: B) { f: (A, B) => B }: B = {
238
+ var acc = init;
239
+ l.reverse.foreach { x => acc = f(x, acc) };
240
+ acc
241
+ }
242
+
225
243
  // Sum the elements of the list.
226
244
  //
227
245
  // O(N)
@@ -7,4 +7,3 @@ extern io def exit(errorCode: Int): Nothing =
7
7
  ret %Pos zeroinitializer
8
8
  """
9
9
  chez "(exit ${errorCode})"
10
- ml "(Posix.Process.exit (Word8.fromInt ${errorCode}))"
@@ -3,10 +3,10 @@ module ref
3
3
  import effekt
4
4
 
5
5
  extern js """
6
- function set$impl(ref, value) {
7
- ref.value = value;
8
- return $effekt.unit;
9
- }
6
+ function set$impl(ref, value) {
7
+ ref.value = value;
8
+ return $effekt.unit;
9
+ }
10
10
  """
11
11
 
12
12
  /**
@@ -32,7 +32,6 @@ extern global def allocate[T](): Ref[T] =
32
32
  extern global def ref[T](init: T): Ref[T] =
33
33
  js "{ value: ${init} }"
34
34
  chez "(box ${init})"
35
- ml "ref ${init}"
36
35
  llvm """
37
36
  %z = call %Pos @c_ref_fresh(%Pos ${init})
38
37
  ret %Pos %z
@@ -44,7 +43,6 @@ extern global def ref[T](init: T): Ref[T] =
44
43
  extern global def get[T](ref: Ref[T]): T =
45
44
  js "${ref}.value"
46
45
  chez "(unbox ${ref})"
47
- ml "!${ref}"
48
46
  llvm """
49
47
  %z = call %Pos @c_ref_get(%Pos ${ref})
50
48
  ret %Pos %z
@@ -56,7 +54,6 @@ extern global def get[T](ref: Ref[T]): T =
56
54
  extern global def set[T](ref: Ref[T], value: T): Unit =
57
55
  js "set$impl(${ref}, ${value})"
58
56
  chez "(set-box! ${ref} ${value})"
59
- ml "${ref} := ${value}"
60
57
  llvm """
61
58
  %z = call %Pos @c_ref_set(%Pos ${ref}, %Pos ${value})
62
59
  ret %Pos %z
@@ -0,0 +1,66 @@
1
+ module string/tty
2
+
3
+ // ANSI escape codes
4
+ namespace ANSI {
5
+ val CSI = "\u001b["
6
+
7
+ def escape(s: String) = CSI ++ s ++ "m"
8
+
9
+ val BLACK = escape("30")
10
+ val RED = escape("31")
11
+ val GREEN = escape("32")
12
+ val YELLOW = escape("33")
13
+ val BLUE = escape("34")
14
+ val MAGENTA = escape("35")
15
+ val CYAN = escape("36")
16
+ val WHITE = escape("37")
17
+
18
+ val BG_BLACK = escape("40")
19
+ val BG_RED = escape("41")
20
+ val BG_GREEN = escape("42")
21
+ val BG_YELLOW = escape("43")
22
+ val BG_BLUE = escape("44")
23
+ val BG_MAGENTA = escape("45")
24
+ val BG_CYAN = escape("46")
25
+ val BG_WHITE = escape("47")
26
+
27
+ val RESET = escape("0")
28
+
29
+ val BOLD = escape("1")
30
+ val FAINT = escape("2")
31
+ val ITALIC = escape("3")
32
+ val UNDERLINE = escape("4")
33
+ val BLINK = escape("5")
34
+ val REVERSE = escape("7")
35
+ val CROSSOUT = escape("9")
36
+ val OVERLINE = escape("53")
37
+ }
38
+
39
+ def red(text: String) = Formatted::colored(text, ANSI::RED)
40
+ def green(text: String) = Formatted::colored(text, ANSI::GREEN)
41
+ def dim(text: String) = Formatted::colored(text, ANSI::FAINT)
42
+ def bold(text: String) = Formatted::colored(text, ANSI::BOLD)
43
+
44
+ interface Formatted {
45
+ def supportsEscape(escape: String): Bool
46
+ }
47
+
48
+ namespace Formatted {
49
+ /// Run given block of code, allowing all formatting
50
+ def formatting[R] { prog : => R / Formatted }: R =
51
+ try { prog() } with Formatted {
52
+ def supportsEscape(escape: String) = resume(true)
53
+ }
54
+
55
+ /// Run given block of code, ignoring all formatting
56
+ def noFormatting[R] { prog : => R / Formatted }: R =
57
+ try { prog() } with Formatted {
58
+ def supportsEscape(escape: String) = resume(false)
59
+ }
60
+
61
+ def tryEmit(escape: String): String / Formatted =
62
+ if (do supportsEscape(escape)) escape else ""
63
+
64
+ def colored(text: String, colorEscape: String): String / Formatted =
65
+ tryEmit(colorEscape) ++ text ++ tryEmit(ANSI::RESET)
66
+ }
@@ -181,11 +181,9 @@ def toInt(str: String, base: Int): Int / Exception[WrongFormat] = {
181
181
  }
182
182
 
183
183
  // Native versions of toInt (unused right now)
184
- // ML: also accepts sml style `~2` for unary minus along with `+2` and `-2`
185
184
  extern pure def unsafeToInt(str: String): Int =
186
185
  js "(Number.isNaN(parseInt(${str})) ? undefined : parseInt(${str}))"
187
186
  chez "(string->number ${str})"
188
- ml "Int.fromString ${str}"
189
187
 
190
188
 
191
189
  /**
@@ -243,36 +241,11 @@ def string[T] { prog: => T / Stream }: (T, String) = <>
243
241
  def printing[T] { prog: => T / Stream }: T = <>
244
242
 
245
243
 
246
- // ANSI escape codes
247
- namespace ANSI {
248
- val BLACK = "\u001b[30m"
249
- val RED = "\u001b[31m"
250
- val GREEN = "\u001b[32m"
251
- val YELLOW = "\u001b[33m"
252
- val BLUE = "\u001b[34m"
253
- val MAGENTA = "\u001b[35m"
254
- val CYAN = "\u001b[36m"
255
- val WHITE = "\u001b[37m"
256
-
257
- val BG_BLACK = "\u001b[40m"
258
- val BG_RED = "\u001b[41m"
259
- val BG_GREEN = "\u001b[42m"
260
- val BG_YELLOW = "\u001b[43m"
261
- val BG_BLUE = "\u001b[44m"
262
- val BG_MAGENTA = "\u001b[45m"
263
- val BG_CYAN = "\u001b[46m"
264
- val BG_WHITE = "\u001b[47m"
265
-
266
- val RESET = "\u001b[0m"
267
- }
268
-
269
-
270
244
  // Characters
271
245
  // ----------
272
246
  //
273
247
  // JS: Int (Unicode codepoints)
274
248
  // CS: ?
275
- // ML: 8-bit https://smlfamily.github.io/Basis/char.html
276
249
  // LLVM: i64 representing utf-8 (varying length 1-4 bytes)
277
250
 
278
251
  extern pure def toString(ch: Char): String =
@@ -287,20 +260,17 @@ extern pure def toString(ch: Char): String =
287
260
  extern pure def toInt(ch: Char): Int =
288
261
  js "${ch}"
289
262
  chez "${ch}"
290
- ml "${ch}"
291
263
  llvm "ret %Int ${ch}"
292
264
 
293
265
  extern pure def toChar(codepoint: Int): Char =
294
266
  js "${codepoint}"
295
267
  chez "${codepoint}"
296
- ml "${codepoint}"
297
268
  llvm "ret %Int ${codepoint}"
298
269
 
299
270
 
300
271
  extern pure def infixLt(x: Char, y: Char): Bool =
301
272
  js "(${x} < ${y})"
302
273
  chez "(< ${x} ${y})"
303
- ml "(${x}: int) < ${y}"
304
274
  llvm """
305
275
  %z = icmp slt %Int ${x}, ${y}
306
276
  %fat_z = zext i1 %z to i64
@@ -311,7 +281,6 @@ extern pure def infixLt(x: Char, y: Char): Bool =
311
281
  extern pure def infixLte(x: Char, y: Char): Bool =
312
282
  js "(${x} <= ${y})"
313
283
  chez "(<= ${x} ${y})"
314
- ml "(${x}: int) <= ${y}"
315
284
  llvm """
316
285
  %z = icmp sle %Int ${x}, ${y}
317
286
  %fat_z = zext i1 %z to i64
@@ -322,7 +291,6 @@ extern pure def infixLte(x: Char, y: Char): Bool =
322
291
  extern pure def infixGt(x: Char, y: Char): Bool =
323
292
  js "(${x} > ${y})"
324
293
  chez "(> ${x} ${y})"
325
- ml "(${x}: int) > ${y}"
326
294
  llvm """
327
295
  %z = icmp sgt %Int ${x}, ${y}
328
296
  %fat_z = zext i1 %z to i64
@@ -333,7 +301,6 @@ extern pure def infixGt(x: Char, y: Char): Bool =
333
301
  extern pure def infixGte(x: Char, y: Char): Bool =
334
302
  js "(${x} >= ${y})"
335
303
  chez "(>= ${x} ${y})"
336
- ml "(${x}: int) >= ${y}"
337
304
  llvm """
338
305
  %z = icmp sge %Int ${x}, ${y}
339
306
  %fat_z = zext i1 %z to i64
@@ -388,7 +355,6 @@ def charAt(str: String, index: Int): Char / Exception[OutOfBounds] =
388
355
  extern pure def unsafeCharAt(str: String, n: Int): Char =
389
356
  js "${str}.codePointAt(${n})"
390
357
  chez "(char->integer (string-ref ${str} ${n}))"
391
- ml "(Char.ord (String.sub (${str}, ${n})))"
392
358
  llvm """
393
359
  %x = call %Int @c_buffer_character_at(%Pos ${str}, i64 ${n})
394
360
  ret %Int %x
@@ -1,5 +1,6 @@
1
- import string
1
+ import string/tty
2
2
  import process
3
+ import bench
3
4
 
4
5
  interface Assertion {
5
6
  def assert(condition: Bool, msg: String): Unit
@@ -29,70 +30,131 @@ def assert(obtained: Char, expected: Char, msg: String): Unit / Assertion =
29
30
  def assert(obtained: String, expected: String, msg: String): Unit / Assertion =
30
31
  assertEqual(obtained, expected, msg) { (x, y) => x == y }
31
32
 
32
- def assert(obtained: Int, expected: Int): Unit / Assertion =
33
+ def assert(obtained: Int, expected: Int): Unit / { Assertion, Formatted } =
33
34
  assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) }
34
35
 
35
- def assert(obtained: Bool, expected: Bool): Unit / Assertion =
36
+ def assert(obtained: Bool, expected: Bool): Unit / { Assertion, Formatted } =
36
37
  assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) }
37
38
 
38
- def assert(obtained: Char, expected: Char): Unit / Assertion =
39
+ def assert(obtained: Char, expected: Char): Unit / { Assertion, Formatted } =
39
40
  assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) }
40
41
 
41
- def assert(obtained: String, expected: String): Unit / Assertion =
42
+ def assert(obtained: String, expected: String): Unit / { Assertion, Formatted } =
42
43
  assertEqual(obtained, expected) { (x, y) => x == y } { x => show(x) }
43
44
 
44
45
  def assertEqual[A](obtained: A, expected: A, msg: String): Unit / Assertion =
45
46
  assertEqual(obtained, expected, msg) { (x, y) => x.equals(y) }
46
47
 
47
- def assertEqual[A](obtained: A, expected: A): Unit / Assertion =
48
+ def assertEqual[A](obtained: A, expected: A): Unit / { Assertion, Formatted } =
48
49
  assertEqual(obtained, expected) { (x, y) => x.equals(y) } { x => x.genericShow }
49
50
 
50
51
  def assertEqual[A](obtained: A, expected: A, msg: String) { equals: (A, A) => Bool }: Unit / Assertion =
51
52
  do assert(equals(obtained, expected), msg)
52
53
 
53
- def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: A => String }: Unit / Assertion =
54
- do assert(equals(obtained, expected), "Obtained: " ++ show(obtained) ++ "\n but expected:" ++ show(expected))
54
+ def assertEqual[A](obtained: A, expected: A) { equals: (A, A) => Bool } { show: A => String }: Unit / { Assertion, Formatted } =
55
+ do assert(equals(obtained, expected), Formatted::tryEmit(ANSI::RESET) ++ "Expected: ".dim ++ show(expected).green ++ "\n Obtained: ".dim ++ show(obtained).red)
56
+ // NOTE: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57
+ // Here's an accidental capture! Can we prevent this somehow nicely?
55
58
 
56
59
 
57
60
  interface Test {
58
- def success(name: String): Unit
59
- def failure(name: String, msg: String): Unit
61
+ def success(name: String, duration: Int): Unit
62
+ def failure(name: String, msg: String, duration: Int): Unit
60
63
  }
61
64
 
62
- def test(name: String) { body: => Unit / Assertion } =
63
- try { body(); do success(name) } with Assertion {
64
- def assert(condition, msg) = if (condition) resume(()) else do failure(name, msg)
65
+ /// Runs the `body` as a test under the given `name`
66
+ /// See `suite` for examples.
67
+ def test(name: String) { body: => Unit / Assertion } = {
68
+ val startTime = bench::relativeTimestamp()
69
+ try {
70
+ body()
71
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
72
+ do success(name, duration)
73
+ } with Assertion {
74
+ def assert(condition, msg) =
75
+ if (condition) resume(())
76
+ else {
77
+ val duration = Duration::diff(startTime, bench::relativeTimestamp())
78
+ do failure(name, msg, duration)
79
+ }
65
80
  }
81
+ }
82
+
83
+ /// Run a test suite with a given `name`.
84
+ /// - If `printTimes` is `true` (or missing), prints out time in milliseconds.
85
+ /// - Formats automatically using ANSI escapes.
86
+ /// - Returns `true` if all tests succeed, otherwise returns `false`. If you want to exit the program on failure, see `mainSuite`.
87
+ ///
88
+ /// Example:
89
+ /// ```effekt
90
+ /// suite("My Tests") {
91
+ /// test("1 + 1 == 2") {
92
+ /// assertEqual(1 + 1, 2)
93
+ /// }
94
+ /// }
95
+ /// ```
96
+ def suite(name: String, printTimes: Bool) { body: => Unit / { Test, Formatted } }: Bool / {} = {
97
+ with Formatted::formatting;
98
+
99
+ def ms(duration: Int): String / Formatted =
100
+ if (printTimes) {
101
+ " " ++ ("[" ++ bench::formatMs(duration) ++ "]").dim
102
+ } else {
103
+ ""
104
+ }
105
+
106
+ def dimWhenZeroElse(s: String, n: Int) { colorIfNonZero: String => String / Formatted } =
107
+ if (n == 0) { dim(s) }
108
+ else { colorIfNonZero(s) }
66
109
 
67
- def suite(name: String) { body: => Unit / Test }: Bool = {
68
- println("Running suite: " ++ name);
69
110
  var failed = 0
70
111
  var passed = 0
71
112
 
72
- try { body() } with Test {
73
- def success(name) = {
74
- passed = passed + 1
75
- println(ANSI::GREEN ++ "+ " ++ name ++ "" ++ ANSI::RESET);
76
- resume(())
77
- }
78
- def failure(name, msg) = {
79
- failed = failed + 1
80
- println(ANSI::RED ++ "- (FAIL) " ++ name ++ "\n " ++ msg ++ ANSI::RESET);
81
- resume(())
113
+ // 1) Print the name of the test
114
+ println(name.bold)
115
+
116
+ // 2) Run the tests, timing them
117
+ val totalDuration = timed {
118
+ try { body() } with Test {
119
+ // 2a) Handle a passing test on success
120
+ def success(name, duration) = {
121
+ passed = passed + 1
122
+ println("✓".green ++ " " ++ name ++ duration.ms)
123
+ resume(())
124
+ }
125
+
126
+ // 2b) Handle a failing test on failure, additionally printing its message
127
+ def failure(name, msg, duration) = {
128
+ failed = failed + 1
129
+ println("✕".red ++ " " ++ name ++ duration.ms)
130
+ println(" " ++ msg.red)
131
+ resume(())
132
+ }
82
133
  }
83
134
  }
84
- if (failed > 0) {
85
- println(ANSI::RED ++ "Some tests failed (" ++ passed.show ++ " passed, " ++ failed.show ++ " failed)" ++ ANSI::RESET)
86
- false
87
- } else {
88
- println(ANSI::GREEN ++ "All tests passed (" ++ passed.show ++ " passed)" ++ ANSI::RESET)
89
- true
90
- }
135
+
136
+ // 3) Format the test results
137
+ println("")
138
+ println(" " ++ (passed.show ++ " pass").dimWhenZeroElse(passed) { green })
139
+ println(" " ++ (failed.show ++ " fail").dimWhenZeroElse(failed) { red })
140
+ println(" " ++ (passed + failed).show ++ " tests total" ++ totalDuration.ms)
141
+
142
+ // 4) Return true if all tests succeeded, otherwise false
143
+ return failed == 0
91
144
  }
92
145
 
146
+ /// See `suite` above.
147
+ def suite(name: String) { body: => Unit / { Test, Formatted } }: Bool / {} =
148
+ suite(name, true) { body }
149
+
93
150
  /// Use as `def main() = mainSuite("...") { ... }`
94
- def mainSuite(name: String) { body: => Unit / Test }: Unit = {
95
- val result = suite(name) { body() }
151
+ /// Recommended for standalone test files ran by CI.
152
+ ///
153
+ /// Exits after running all tests:
154
+ /// - if all tests succeed, exits the program with success (exit code 0)
155
+ /// - otherwise exits the program with failure (exit code 1)
156
+ def mainSuite(name: String) { body: => Unit / { Test, Formatted } }: Unit = {
157
+ val result = suite(name, true) { body }
96
158
  val exitCode = if (result) 0 else 1
97
159
  exit(exitCode)
98
160
  }
@@ -8,4 +8,4 @@ extern io def cont[A, B] { r: A => B / {} }: Cont[A, B] = js "${box r}"
8
8
 
9
9
  // This is unsafe if the continuation left the scope of the
10
10
  // capabilities it closed over
11
- extern control def apply[A, B](k: Cont[A, B], a: A): B / {} = js "${k}(${a})"
11
+ extern async def apply[A, B](k: Cont[A, B], a: A): B / {} = js "${k}(${a})"