firefly-compiler 0.4.80 → 0.4.82

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.
@@ -1025,11 +1025,10 @@ extend self: Parser {
1025
1025
  if(self.current().is(LKeyword) && (self.current().rawIs("let") || self.current().rawIs("mutable"))) {self.parseLet()} else:
1026
1026
  if(self.current().is(LKeyword) && self.current().rawIs("function")) {self.parseFunctions()} else:
1027
1027
  let term = self.parseTerm()
1028
- if(!self.current().is(LAssign) && !self.current().is3(LAssignPlus, LAssignMinus, LAssignLink)) {term} else:
1028
+ if(!self.current().is(LAssign) && !self.current().is2(LAssignPlus, LAssignMinus)) {term} else:
1029
1029
  let token = do {
1030
1030
  if(self.current().is(LAssignPlus)) {self.skip(LAssignPlus)} else:
1031
1031
  if(self.current().is(LAssignMinus)) {self.skip(LAssignMinus)} else:
1032
- if(self.current().is(LAssignLink)) {self.skip(LAssignLink)} else:
1033
1032
  self.skip(LAssign)
1034
1033
  }
1035
1034
  let operator = token.raw().dropLast(1)
@@ -1153,7 +1152,7 @@ extend self: Parser {
1153
1152
  True
1154
1153
  } else {False}
1155
1154
  mutable result = self.parseAtom()
1156
- while {self.current().is(LBracketLeft) || self.current().is(LColon) || self.current().is(LDot)} {
1155
+ while {self.current().is4(LBracketLeft, LColon, LDot, LArrowThin)} {
1157
1156
  if(self.current().is(LDot)) {
1158
1157
  self.skip(LDot)
1159
1158
  if(self.current().rawIs("{")) {
@@ -1166,6 +1165,8 @@ extend self: Parser {
1166
1165
  let token = self.skip(LLower)
1167
1166
  result = EField(token.at(), False, result, token.raw())
1168
1167
  }
1168
+ } elseIf {self.current().is(LArrowThin)} {
1169
+ result = self.parseDynamicMember(result)
1169
1170
  } else {
1170
1171
  let at = self.current().at()
1171
1172
  let typeArguments = if(!self.current().rawIs("[")) {[]} else {self.parseTypeArguments()}
@@ -1181,6 +1182,46 @@ extend self: Parser {
1181
1182
  }
1182
1183
  result
1183
1184
  }
1185
+
1186
+ parseDynamicMember(record: Term): Term {
1187
+ self.skip(LArrowThin)
1188
+ let token = self.skip(LLower)
1189
+ let member = EString(token.at(), "\"" + token.raw() + "\"")
1190
+ if(self.current().rawIs("(")) {
1191
+ let arguments = self.parseFunctionArguments(record.at, False)
1192
+ let effect = self.freshUnificationVariable(record.at)
1193
+ let target = DynamicCall(EField(token.at(), False, record, "call" + arguments.first.size()), False)
1194
+ ECall(record.at, target, effect, [], [
1195
+ Argument(member.at, None, member)
1196
+ ...arguments.first
1197
+ ], [])
1198
+ } elseIf {self.current().is3(LAssign, LAssignPlus, LAssignMinus)} {
1199
+ let method =
1200
+ if(self.current().is(LAssign)) {
1201
+ self.skip(LAssign)
1202
+ "set"
1203
+ } elseIf {self.current().is(LAssignPlus)} {
1204
+ self.skip(LAssignPlus)
1205
+ "increment"
1206
+ } else {
1207
+ self.skip(LAssignMinus)
1208
+ "decrement"
1209
+ }
1210
+ let value = self.parseTerm()
1211
+ let effect = self.freshUnificationVariable(record.at)
1212
+ let target = DynamicCall(EField(token.at(), False, record, "set"), False)
1213
+ ECall(record.at, target, effect, [], [
1214
+ Argument(member.at, None, member)
1215
+ Argument(value.at, None, value)
1216
+ ], [])
1217
+ } else {
1218
+ let effect = self.freshUnificationVariable(record.at)
1219
+ let target = DynamicCall(EField(token.at(), False, record, "get"), False)
1220
+ ECall(record.at, target, effect, [], [
1221
+ Argument(member.at, None, member)
1222
+ ], [])
1223
+ }
1224
+ }
1184
1225
 
1185
1226
  parseAtom(): Term {
1186
1227
  if(self.current().is(LString)) {
package/compiler/Token.ff CHANGED
@@ -38,6 +38,10 @@ extend token: Token {
38
38
  token.kind == kind1 || token.kind == kind2 || token.kind == kind3
39
39
  }
40
40
 
41
+ is4(kind1: TokenKind, kind2: TokenKind, kind3: TokenKind, kind4: TokenKind): Bool {
42
+ token.kind == kind1 || token.kind == kind2 || token.kind == kind3 || token.kind == kind4
43
+ }
44
+
41
45
  rawIs(value: String): Bool {
42
46
  token.stopOffset - token.startOffset == value.size() &&
43
47
  token.code.startsWith(value, token.startOffset)
@@ -78,11 +82,11 @@ data TokenKind {
78
82
  LPipe
79
83
  LColon
80
84
  LDotDotDot
85
+ LArrowThin
81
86
  LArrowThick
82
87
  LAssign
83
88
  LAssignPlus
84
89
  LAssignMinus
85
- LAssignLink
86
90
  }
87
91
 
88
92
  extend self: TokenKind {
@@ -109,11 +113,11 @@ extend self: TokenKind {
109
113
  | LPipe => False
110
114
  | LColon => False
111
115
  | LDotDotDot => False
116
+ | LArrowThin => False
112
117
  | LArrowThick => False
113
118
  | LAssign => False
114
119
  | LAssignPlus => False
115
120
  | LAssignMinus => False
116
- | LAssignLink => False
117
121
  }
118
122
  }
119
123
 
@@ -139,11 +143,11 @@ extend self: TokenKind {
139
143
  | LPipe => False
140
144
  | LColon => False
141
145
  | LDotDotDot => True
146
+ | LArrowThin => False
142
147
  | LArrowThick => False
143
148
  | LAssign => False
144
149
  | LAssignPlus => False
145
150
  | LAssignMinus => False
146
- | LAssignLink => False
147
151
  }
148
152
  }
149
153
 
@@ -169,11 +173,11 @@ extend self: TokenKind {
169
173
  | LPipe => False
170
174
  | LColon => False
171
175
  | LDotDotDot => False
176
+ | LArrowThin => False
172
177
  | LArrowThick => False
173
178
  | LAssign => False
174
179
  | LAssignPlus => False
175
180
  | LAssignMinus => False
176
- | LAssignLink => False
177
181
  }
178
182
  }
179
183
 
@@ -226,6 +226,8 @@ tokenize(file: String, code: String, completionAt: Option[Location], attemptFixe
226
226
  LColon
227
227
  } elseIf {i - start == 3 && code.grab(i - 3) == '.' && code.grab(i - 2) == '.' && code.grab(i - 1) == '.'} {
228
228
  LDotDotDot
229
+ } elseIf {i - start == 2 && code.grab(i - 2) == '-' && code.grab(i - 1) == '>'} {
230
+ LArrowThin
229
231
  } elseIf {i - start == 2 && code.grab(i - 2) == '=' && code.grab(i - 1) == '>'} {
230
232
  LArrowThick
231
233
  } elseIf {i - start == 1 && code.grab(i - 1) == '='} {
@@ -234,8 +236,6 @@ tokenize(file: String, code: String, completionAt: Option[Location], attemptFixe
234
236
  LAssignPlus
235
237
  } elseIf {i - start == 2 && code.grab(i - 2) == '-' && code.grab(i - 1) == '='} {
236
238
  LAssignMinus
237
- } elseIf {i - start == 3 && code.grab(i - 3) == ':' && code.grab(i - 2) == ':' && code.grab(i - 1) == '='} {
238
- LAssignLink
239
239
  } else {
240
240
  LOperator
241
241
  }
package/core/JsSystem.ff CHANGED
@@ -5,6 +5,18 @@ extend self: JsSystem {
5
5
  global(): JsValue
6
6
  target js sync "return self_"
7
7
 
8
+ get(key: String): JsValue
9
+ target js sync "return self_[key_]"
10
+
11
+ set[V: IsJsValue](key: String, value: V): Unit
12
+ target js sync "self_[key_] = value_"
13
+
14
+ increment[V: IsJsValue](key: String, value: V): Unit
15
+ target js sync "self_[key_] += value_"
16
+
17
+ decrement[V: IsJsValue](key: String, value: V): Unit
18
+ target js sync "self_[key_] -= value_"
19
+
8
20
  parseJson(json: String): JsValue
9
21
  target js sync "return JSON.parse(json_)"
10
22
 
package/core/JsValue.ff CHANGED
@@ -88,6 +88,12 @@ extend self: JsValue {
88
88
  set[K: IsJsValue, V: IsJsValue](key: K, value: V): Unit
89
89
  target js sync "self_[key_] = value_"
90
90
 
91
+ increment[K: IsJsValue, V: IsJsValue](key: K, value: V): Unit
92
+ target js sync "self_[key_] += value_"
93
+
94
+ decrement[K: IsJsValue, V: IsJsValue](key: K, value: V): Unit
95
+ target js sync "self_[key_] -= value_"
96
+
91
97
  delete[K: IsJsValue](key: K): Unit
92
98
  target js sync "delete self_[key_]"
93
99
 
package/core/Task.ff CHANGED
@@ -85,6 +85,14 @@ extend self: Task {
85
85
  readOr(self.channel()) {_ => }.
86
86
  timeout(duration) {}
87
87
  }
88
+
89
+ mapList[T, R](list: List[T], body: T => R): List[R] {
90
+ self.all(list.map {x => {body(x)}})
91
+ }
92
+
93
+ raceList[T, R](list: List[T], body: T => R): R {
94
+ self.race(list.map {x => {body(x)}})
95
+ }
88
96
 
89
97
  all[T](tasks: List[() => T]): List[T] {
90
98
  let successChannel = self.channel()
@@ -92,12 +92,11 @@ data GuideDocument(
92
92
  document: Document
93
93
  )
94
94
 
95
- render(lux: Lux, browser: BrowserSystem, prefix: String, kebab: String, guides: List[Guide], demos: List[Demo]) {
95
+ render(lux: Lux, httpClient: HttpClient, prefix: String, kebab: String, guides: List[Guide], demos: List[Demo]) {
96
96
  let guide = guides.find {_.prefix == prefix}.else {guides.grabFirst()}
97
97
  let document = guide.documents.find {d =>
98
98
  kebabCase(d.heading()) == kebab
99
99
  }.else {guide.documents.grabFirst()}
100
- browser.js().global().get("document").set("title", document.title(guide))
101
100
  let guideDocuments = guides.flatMap {guide => guide.documents.map {document =>
102
101
  GuideDocument(guide, document)
103
102
  }}
@@ -111,7 +110,7 @@ render(lux: Lux, browser: BrowserSystem, prefix: String, kebab: String, guides:
111
110
  lux.cssClass(Styles.guideCss)
112
111
  lux.add("main") {
113
112
  lux.cssClass(Styles.guideMainCss)
114
- renderDocument(lux, browser.httpClient(), prefix, document, demos, nextDocument)
113
+ renderDocument(lux, httpClient, prefix, document, demos, nextDocument)
115
114
  }
116
115
  renderTopbar(lux, menu, setMenu)
117
116
  lux.add("div") {
@@ -1,5 +1,6 @@
1
1
  import WebServer from ff:webserver
2
2
  import Lux from ff:lux
3
+ import Css from ff:lux
3
4
  import Guide
4
5
  import GettingStarted
5
6
  import ReferenceAll
@@ -18,18 +19,6 @@ nodeMain(system: NodeSystem): Unit {
18
19
  let path = request.readPath()
19
20
  let segments = path.split('/').filter {s => s != "" && s != "." && s != ".."}
20
21
  segments.{
21
- | ["reference", ...] =>
22
- serveGuideHtml("Firefly Reference", request)
23
- | ["getting-started", ...] =>
24
- serveGuideHtml("Firefly Getting Started", request)
25
- | ["examples", ...] =>
26
- serveGuideHtml("Firefly Examples", request)
27
- | ["packages", ...] =>
28
- serveGuideHtml("Firefly Packages", request)
29
- | ["community", ...] =>
30
- serveGuideHtml("Firefly Community", request)
31
- | ["front", ...] =>
32
- serveGuideHtml("Firefly", request)
33
22
  | ["js", ...] =>
34
23
  let asset = if(path == "/js/ff/fireflysite/Main.mjs" && system.assets().exists("/js/Main.bundle.js")) {
35
24
  "/js/Main.bundle.js"
@@ -44,6 +33,7 @@ nodeMain(system: NodeSystem): Unit {
44
33
  | ["assets", "font", "FireflySans.ttf"] =>
45
34
  serveAsset(system, cacheSalt, request, "font/ttf", "/assets/font/NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf")
46
35
  | ["assets", ...rest] => serveAssets(system, cacheSalt, request, rest)
36
+ | _ {serveGuide(system, request)} =>
47
37
  | _ =>
48
38
  let parameters = if(request.readRawQueryString().size() == 0) {""} else {
49
39
  "?" + request.readRawQueryString()
@@ -54,21 +44,40 @@ nodeMain(system: NodeSystem): Unit {
54
44
  }
55
45
  }
56
46
 
47
+ guides = [
48
+ Guide("/front/", [FrontPage.new()])
49
+ Guide("/getting-started/", [GettingStarted.new()])
50
+ Guide("/examples/", ExamplesOverview.mock())
51
+ Guide("/reference/", ReferenceAll.mock())
52
+ Guide("/packages/", [PackagesOverview.new()])
53
+ Guide("/community/", [CommunityOverview.new()])
54
+ ]
55
+
56
+ serveGuide(system: NodeSystem, request: WebRequest[WebResponse]): Bool {
57
+ let demos = ExamplesOverview.demos()
58
+ mutable served = False
59
+ guides.each {guide =>
60
+ if(request.readPath().startsWith(guide.prefix)):
61
+ let kebab = request.readPath().dropFirst(guide.prefix.size())
62
+ let htmlAndCss = Lux.renderToString(system) {lux =>
63
+ Guide.render(lux, system.httpClient(), guide.prefix, kebab, guides, demos)
64
+ }
65
+ let title = guide.documents.find {d =>
66
+ Guide.kebabCase(d.heading()) == kebab
67
+ }.map {_.title(guide)}.else {guide.title()}
68
+ serveGuideHtml(title, htmlAndCss.first, htmlAndCss.second, request)
69
+ served = True
70
+ }
71
+ served
72
+ }
73
+
57
74
  browserMain(system: BrowserSystem): Unit {
58
75
  let demos = ExamplesOverview.demos()
59
- let guides = [
60
- Guide("/front/", [FrontPage.new()])
61
- Guide("/getting-started/", [GettingStarted.new()])
62
- Guide("/examples/", ExamplesOverview.mock())
63
- Guide("/reference/", ReferenceAll.mock())
64
- Guide("/packages/", [PackagesOverview.new()])
65
- Guide("/community/", [CommunityOverview.new()])
66
- ]
67
76
  guides.collect {guide =>
68
77
  if(system.urlPath().startsWith(guide.prefix)):
69
78
  Lux.renderById(system, "main") {lux =>
70
79
  let kebab = system.urlPath().dropFirst(guide.prefix.size())
71
- Guide.render(lux, system, guide.prefix, kebab, guides, demos)
80
+ Guide.render(lux, system.httpClient(), guide.prefix, kebab, guides, demos)
72
81
  }
73
82
  }
74
83
  }
@@ -119,10 +128,11 @@ serveAsset(system: NodeSystem, salt: Buffer, request: WebRequest[WebResponse], c
119
128
  }
120
129
  }
121
130
 
122
- serveGuideHtml(title: String, request: WebRequest[WebResponse]): Unit {
131
+ serveGuideHtml(title: String, contentHtml: String, styleTags: String, request: WebRequest[WebResponse]): Unit {
123
132
  request.writeHeader("Content-Type", "text/html; charset=UTF-8")
124
133
  request.writeText("<!doctype html>")
125
134
  request.writeText("<html lang='en' style='background-color: #ffffff; color: #333333; width: 100%; height: 100%; color-scheme: light;'>")
135
+ request.writeText("<script type='module' src='/js/ff/fireflysite/Main.mjs'></script>")
126
136
  request.writeText("<head>")
127
137
  request.writeText("<title>" + title + "</title>")
128
138
  request.writeText("<meta name='viewport' content='width=device-width, initial-scale=1.0'>")
@@ -132,10 +142,10 @@ serveGuideHtml(title: String, request: WebRequest[WebResponse]): Unit {
132
142
  request.writeText("<link rel='preload' href='/assets/image/firefly-logo-yellow.png' as='image'>")
133
143
  request.writeText("<style>@font-face { font-family: 'Firefly Mono'; font-display: fallback; src: url('/assets/font/FireflyMono.ttf'); unicode-range: U+000-5FF; }</style>")
134
144
  request.writeText("<style>@font-face { font-family: 'Firefly Sans'; font-display: fallback; src: url('/assets/font/FireflySans.ttf'); unicode-range: U+000-5FF; }</style>")
145
+ request.writeText(styleTags)
135
146
  request.writeText("</head>")
136
147
  request.writeText("<body style='margin: 0; padding: 0; width: 100%; height: 100%; touch-action: manipulation;'>")
137
- request.writeText("<div id='main'></div>")
138
- request.writeText("<script type='module' src='/js/ff/fireflysite/Main.mjs'></script>")
148
+ request.writeText("<div id='main'>" + contentHtml + "</div>")
139
149
  request.writeText("</body>")
140
150
  request.writeText("</html>")
141
151
  }
@@ -12,8 +12,7 @@ mock(): List[Document] {
12
12
  UnfetchedDocument("Pattern matching")
13
13
  UnfetchedDocument("Traits and instances")
14
14
  UnfetchedDocument("Exceptions")
15
- UnfetchedDocument("JavaScript interop")
16
- UnfetchedDocument("Async I/O")
17
15
  UnfetchedDocument("Structured concurrency")
16
+ UnfetchedDocument("JavaScript interop")
18
17
  ]
19
18
  }
@@ -19,6 +19,14 @@ nodeMain(system: NodeSystem) {
19
19
 
20
20
  Log.show([1, 2].map(increment))
21
21
 
22
+ Log.show(p(42))
23
+ let f2 = {42}
24
+ Log.show(f2())
25
+ let pairs = {Pair(_, {_})}
26
+ let pp = pairs(42)
27
+ let foo = {{_ + 1}(_)}
28
+ Log.show(foo(1))
29
+
22
30
  }
23
31
 
24
32
  factorial(n: Int): Int {
@@ -0,0 +1,66 @@
1
+ # Emitted JavaScript
2
+
3
+ While most Firefly code maps directly to the JavaScript equivalent, there are two notable exceptions:
4
+
5
+ * I/O appears to be blocking, but compiles down to JavaScript `async`/`await`.
6
+ * Methods are resolved statically in Firefly and become top level functions in JavaScript.
7
+
8
+ In addition, pattern matching doesn't have a direct equivalent in JavaScript, and neither does traits.
9
+
10
+
11
+ # Example
12
+
13
+ Consider the following main function:
14
+
15
+ ```firefly
16
+ nodeMain(system: NodeSystem) {
17
+
18
+ let files = ["a.txt", "b.txt"]
19
+
20
+ let contents = files.map {file =>
21
+ system.path(file).readText()
22
+ }
23
+
24
+ let upper = contents.map {content =>
25
+ content.upper()
26
+ }
27
+
28
+ system.writeLine("Result: " + upper.join(""))
29
+
30
+ }
31
+ ```
32
+
33
+ The JavaScript that's emitted looks roughly like this:
34
+
35
+ ```js
36
+ export async function nodeMain$(system) {
37
+
38
+ const files = ["a.txt", "b.txt"]
39
+
40
+ const contents = await List_map$(files, async file => {
41
+ return await Path_readText$(await NodeSystem_path$(system, file))
42
+ })
43
+
44
+ const upper = List_map(contents, content => {
45
+ return String_upper(content)
46
+ })
47
+
48
+ NodeSystem_writeLine$("Result: " + String_join(upper, ""))
49
+
50
+ }
51
+ ```
52
+
53
+ In JavaScript, `nodeMain` becomes an `async` function and gets the `$` suffix to distinguish it from a synchronous function.
54
+
55
+ The `let` keyword in Firefly corresponds to the `const` keyword in JavaScript, and Firefly list literals become JavaScript array literals.
56
+
57
+ The `map` method becomes a top level function, or rather, one `async` top level function named `List_map$` and another synchronous function named `List_map`.
58
+ A static analysis is performed to decide which version to call.
59
+
60
+ Because the first call to `map` is passed an anonymous function that calls a method on `system`, which is a capability, and the current top level function is asynchronous,
61
+ the analysis picks the asynchronous version `List_map$` and uses the `await` keyword.
62
+
63
+ The second call to `map` is passed an anonymous function that doesn't involve any other capabilities, the analysis picks the synchronous version `List_map`.
64
+
65
+ This static analysis is necessarily conservative, and may occasionally call the asynchronous version of a function where the synchrhonous version would suffice.
66
+ When using the VSCode extension, the hover information for a call will note if the call is asynchronous.
@@ -0,0 +1,101 @@
1
+ # Exceptions
2
+
3
+ Exceptions allow a program to transfer control from the point of error to a designated exception handler, separating error-handling logic from regular program flow.
4
+
5
+
6
+ # Throwing exceptions
7
+
8
+ Exceptions are thrown using the `throw` function. Example:
9
+
10
+ ```firefly
11
+ grabOption[T](option: Option[T]): T {
12
+ | Some(v) => v
13
+ | None => throw(GrabException())
14
+ }
15
+ ```
16
+
17
+ In this example, if the argument is `Some(v)`, then `v` is returned. Otherwise, a `GrabException` is thrown.
18
+
19
+ Any type declared with the `data` or `newtype` keyword can be thrown as an exception, since the type will have an instance for the `HasAnyTag` trait.
20
+
21
+
22
+ # Catching exceptions
23
+
24
+ When an exception is thrown, it propagates up the call chain to the nearest `try`, where it can be caught:
25
+
26
+ ```firefly
27
+ try {
28
+ grabOption(None)
29
+ } catch {| GrabException, error =>
30
+ Log.trace("A GrabException occurred")
31
+ error.rethrow()
32
+ }
33
+ ```
34
+
35
+ The first argument passed to `catch` is the thrown value, and the second parameter contains the stack trace.
36
+ The `rethrow()` method throws the exception again without altering the stack trace.
37
+ It is used when the exception is caught where it can't be fully handled.
38
+
39
+ If the exception reaches the top of the call stack, the exception value and the stack trace will be printed.
40
+
41
+
42
+ # Catching any exception
43
+
44
+ It is also posssible to catch any exception, regardless of its type, using the `catchAny` method:
45
+
46
+ ```firefly
47
+ try {
48
+ let result = fragileOperation()
49
+ Some(result)
50
+ } catchAny {error =>
51
+ None
52
+ }
53
+ ```
54
+
55
+
56
+ # Cleaning up
57
+
58
+ Cleanup often needs to happen whether or not an exception is thrown.
59
+ This is what the `finally` method guarantees:
60
+
61
+ ```firefly
62
+ let fileHandle = path.readHandle()
63
+ try {
64
+ process(fileHandle)
65
+ } finally {
66
+ fileHandle.close()
67
+ }
68
+ ```
69
+
70
+ If the program is terminated abnormally (force closed, power is lost, etc.) there is no guarantee that `finally` will get called.
71
+ In most environments, the operating system will occasionally terminate the program abnormally, so programs should be designed such that they can recover from abnormal termination.
72
+
73
+
74
+ # Catching multiple exceptions
75
+
76
+ The `try` function returns a value of type `Try[T]` that is defined as follows:
77
+
78
+ ```firefly
79
+ data Try[T] {
80
+ Success(value: T)
81
+ Failure(error: Error)
82
+ }
83
+ ```
84
+
85
+ The `catch`, `catchAny` and `finally` methods are defined on this type. However, since they don't return a `Try[T]` value, they can't be chained.
86
+
87
+ However, the alternative `tryCatch`, `tryCatchAny` and `tryFinally` methods do return a `Try[T]` value, and can thus be chained:
88
+
89
+ ```firefly
90
+ try {
91
+ doSomething()
92
+ } tryCatch {| GrabException, error =>
93
+ reportSpecificError()
94
+ } tryCatchAny {error =>
95
+ reportGeneralError()
96
+ } finally {
97
+ performCleanup()
98
+ }
99
+ ```
100
+
101
+ The last method in the chain here is `finally`. If it was `tryFinally`, a value of type `Try[T]` would be returned.