firefly-compiler 0.4.80 → 0.4.81

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.
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.
@@ -3,9 +3,9 @@
3
3
  There are 5 kinds of functions in Firefly:
4
4
 
5
5
  * Top-level functions
6
+ * Anonymous functions
6
7
  * Local functions
7
8
  * Methods
8
- * Anonymous functions
9
9
  * Trait functions
10
10
 
11
11
  Trait functions are covered in section [Traits and instances](traits-and-instances), the rest are covered below.
@@ -152,24 +152,58 @@ This version introduces an additional parameter, `acc`, which acts as an accumul
152
152
 
153
153
  # Anonymous functions
154
154
 
155
- Firefly has anonymous functions, where the function and the parameters do not have names. They have a concrete type without type parameters. They are first-class values and can be assigned and passed around like other values. Here is an example:
155
+ In firefly anonymous functions are written in curlybrases and constucted like this:
156
156
 
157
157
  ```firefly
158
- let next: Int => Int = {i => i + 1}
158
+ {a, b =>
159
+ let sum = a + b
160
+ sum
161
+ }
162
+ ```
163
+
164
+ This anonymous function takes two arguments and returns their sum. Like named functions, the body is a sequence of statements where the last expression is returned.
165
+
166
+ Anonymous functions are often used right away, like below:
167
+ :
168
+
169
+ ```firefly
170
+ [1, 2, 3].map({x => x + 1}) // Returns [2, 3, 4]
171
+ ```
172
+
173
+ An anonymous function that increments the given value by one is passed as argument to the methods `map` working on lists.
174
+
175
+ These functions are anonymous in the sense that they do not bring a name into scope themselves. They are just expressions that construct a function value. Like all other values, they can be assigned to variables, passed as arguments, or returned from other functions. But unlike other values, they can also be called.
176
+
177
+ This is in contrast to named functions, which are not first-class in Firefly. The name of a top-level function can only be called but is not an expression in Firefly. To pass a top-level function as an argument, for instance, it must be converted to an anonymous function first.
178
+
179
+ The type of function values are writen like this:
180
+
181
+ ```firefly
182
+ Int => Int // One parameter
183
+ (Int, Int) => Int // Multiple parameters
184
+ () => Int // No parameters
159
185
  ```
160
186
 
161
- The fat arrow (`=>`) is used both in the type and in the definition. This arrow is always part of the function type and separates the parameter types from the return type. In the anonymous function definition, the arrow separates the arguments from the function body and can be omitted in some cases, as shown later.
187
+ The type of an anonymous function cannot be written explicitly in the definition but is inferred from its usage. It will always have a monomorphic type where the argument and return types are concrete types.
162
188
 
163
- This is an anonymous function taking no arguments:
189
+ Here are some examples of anonymous functions assigned to variables explicitly given a type.
190
+
191
+ Anonymous function without parameters are written without the arrow (`=>`), like this:
164
192
 
165
193
  ```firefly
166
194
  let life: () => Int = {42}
167
195
  ```
168
196
 
169
- This is the anonymous function taking no arguments, returning `Unit`:
197
+ This is an anonymous function taking no arguments and returning `Unit`:
170
198
 
171
199
  ```firefly
172
- let uhit: () => Unit = {}
200
+ let unit: () => Unit = {}
201
+ ```
202
+
203
+ This is an anonymous function that increments its input:
204
+
205
+ ```firefly
206
+ let next: Int => Int = {i => i + 1}
173
207
  ```
174
208
 
175
209
  This anonymous function takes multiple arguments:
@@ -187,12 +221,25 @@ next(1) // returns 2
187
221
  plus(1, 2) // returns 3
188
222
  ```
189
223
 
190
- Placeholders...
224
+ Parameter names are not part of the function type, and likewise, anonymous functions cannot be called with named arguments. The same goes for default argument values, which are not supported for anonymous functions.
225
+
226
+ The parameter list and the function arrow can be omitted when the parameters are only used once in the function body. In such cases, the parameters in the body are replaced with underscores (`_`), like this:
227
+
191
228
 
192
229
  ```firefly
230
+ let next: Int => Int = {_ + 1}
231
+ let plus: (Int, Int) => Int = {_ + _}
193
232
  let identity: Int => Int = {_}
194
233
  ```
195
234
 
235
+ These underscores, or anonymous parameters, always belong to the nearest anonymous function. Consider the following function:
236
+
237
+ ```firefly
238
+ let f: Int => Int = {{_ + 1}(_)}
239
+ ```
240
+
241
+ In this code, there is an outer and an inner anonymous function, both taking one argument. The first underscore belongs to the inner function, which is called immediately by the outer function with the outer function's anonymous parameter as the argument.
242
+
196
243
 
197
244
  # Local functions
198
245
 
@@ -205,4 +252,87 @@ function square(n: Int): Int {
205
252
  }
206
253
  ```
207
254
 
208
- # Methods
255
+ The above local function definition is a statement, similar to local variables declared with `let`. The function name `square` will be in scope for the rest of the code block.
256
+
257
+ Furthermore, local functions declared in sequence are in scope within each other's bodies, allowing them to be mutually recursive.
258
+
259
+ # Trailing lambda calls
260
+
261
+ ```firefly
262
+ if(x == 1) {"One"}
263
+ ```
264
+
265
+ # Methods
266
+
267
+ Firefly has methods, which are called like this:
268
+
269
+ ```firefly
270
+ Some(1).isEmpty() // False
271
+ Some(1).map {_ + 1} // Some(2)
272
+ ```
273
+
274
+ The examples above, calls the two methods `isEmpty` and `map` defined on `Option`. The code below, shows how these methods are defined in `ff:core` package.
275
+
276
+ ```firefly
277
+ data Option[T] {
278
+ None
279
+ Some(value: T)
280
+ }
281
+
282
+ extend self[T]: Option[T] {
283
+ isEmpty(): Bool {
284
+ self.{
285
+ | None => True
286
+ | Some(_) => False
287
+ }
288
+ }
289
+
290
+ map[R](body: T => R): Option[R] {
291
+ self.{
292
+ | None => None
293
+ | Some(v) => Some(body(v))
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ Methods can be defined for a more narrow targer type, like `flatten` below:
300
+
301
+ ```firefly
302
+ extend self[T]: Option[Option[T]] {
303
+ flatten(): Option[T] {
304
+ self.{
305
+ | None => None
306
+ | Some(v) => v
307
+ }
308
+ }
309
+ }
310
+ ```
311
+
312
+ The extend block above, will only define `flatten` for options types of options.
313
+
314
+ In code below, the extend block defines methods for the target type `Option[T]`, but only when `T` implements the `Equal` trait.
315
+
316
+
317
+ ```firefly
318
+ extend self[T: Equal]: Option[T] {
319
+ contains(value: T): Bool {
320
+ self.{
321
+ | None => False
322
+ | Some(v) => v == value
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+
329
+
330
+ # Special method call syntax
331
+
332
+ ```firefly
333
+ if(x == 1) {"One"} else {"Several"}
334
+ ```
335
+
336
+ # Trait functions
337
+
338
+ Trait functions are covered in the section about [traits and instances](traits-and-instances)
@@ -0,0 +1,134 @@
1
+ # JavaScript interop
2
+
3
+ Firefly compiles to JavaScript, which enables it to run in the browser.
4
+ It uses Node.js as its server side and desktop runtime.
5
+
6
+ The JavaScript interop features enable the wrapping of libraries that are written in JavaScript so that they can be used in Firefly.
7
+
8
+
9
+ # The JsSystem
10
+
11
+ Most JavaScript functionality can be accessed via the `JsSystem` object.
12
+
13
+ ```firefly
14
+ browserMain(system: BrowserSystem): Unit {
15
+ let document = system.js().global().get("document")
16
+ let element = document.call1("getElementById", "my-id")
17
+ element.set("innerText", "Hi!")
18
+ }
19
+ ```
20
+
21
+ This example gets the global `document`, calls `getElementId("my-id")` on it, and sets `innerText = "Hi!"`.
22
+
23
+ The type of the `document` and `element` variables here is `JsValue`, which represents an arbitrary JavaScript value.
24
+
25
+
26
+ # The ff:unsafejs package
27
+
28
+ This package provides access to unsafe JavaScript features:
29
+
30
+ ```firefly
31
+ // Obtains the JsSystem without a capability
32
+ jsSystem(): JsSystem
33
+
34
+ // Imports a JavaScript module (the import gets hoisted to a top level import)
35
+ import(module: String): JsValue
36
+
37
+ // Awaits an async function (only works in async context)
38
+ await[T](body: () => T): T
39
+
40
+ // Throws TaskAbortedException if the current task has been aborted
41
+ throwIfCancelled(): Unit
42
+
43
+ // Returns true if the current task has been aborted
44
+ cancelled(): Bool
45
+ ```
46
+
47
+ In the future, it may be possible to provide a whitelist of dependencies that are allowed to use this package.
48
+
49
+
50
+ # Internal FFI
51
+
52
+ The `target` keyword allows writing almost raw JavaScript.
53
+
54
+ ```firefly
55
+ alertHi(name: String)
56
+ target browser sync """
57
+ alert("Hi " + name_ + "!");
58
+ """
59
+ ```
60
+
61
+ Multiple target keywords are allowed per function or method.
62
+ The target type is `js` or the more specific types `browser` or `node`, and then a mode that's either `sync` for when called synchronously, or `async` for when called asynchronously.
63
+
64
+ Argument names are avaliable with a `_` suffix in the JavaScript code block.
65
+
66
+ JavaScript module imports can be done in the beginning of a JavaScript code block with the specfic syntax `import * as foo from 'bar'`. The import statement will be hoisted to a top level import.
67
+
68
+ In the future, the `target` keyword and its functionality may be removed from the language.
69
+
70
+
71
+ # Emitted JavaScript
72
+
73
+ While most Firefly code maps directly to the JavaScript equivalent, there are two notable exceptions:
74
+
75
+ * I/O appears to be blocking, but compiles down to JavaScript `async`/`await`.
76
+ * Methods are resolved statically in Firefly and become top level functions in JavaScript.
77
+
78
+ In addition, pattern matching doesn't have a direct equivalent in JavaScript, and neither does traits.
79
+
80
+ Consider the following main function:
81
+
82
+ ```firefly
83
+ nodeMain(system: NodeSystem) {
84
+
85
+ let files = ["a.txt", "b.txt"]
86
+
87
+ let contents = files.map {file =>
88
+ system.path(file).readText()
89
+ }
90
+
91
+ let upper = contents.map {content =>
92
+ content.upper()
93
+ }
94
+
95
+ system.writeLine("Result: " + upper.join(""))
96
+
97
+ }
98
+ ```
99
+
100
+ The JavaScript that's emitted looks roughly like this:
101
+
102
+ ```js
103
+ export async function nodeMain$(system) {
104
+
105
+ const files = ["a.txt", "b.txt"]
106
+
107
+ const contents = await List_map$(files, async file => {
108
+ return await Path_readText$(await NodeSystem_path$(system, file))
109
+ })
110
+
111
+ const upper = List_map(contents, content => {
112
+ return String_upper(content)
113
+ })
114
+
115
+ NodeSystem_writeLine$("Result: " + String_join(upper, ""))
116
+
117
+ }
118
+ ```
119
+
120
+ In JavaScript, `nodeMain` becomes an `async` function and gets the `$` suffix to distinguish it from a synchronous function.
121
+
122
+ The `let` keyword in Firefly corresponds to the `const` keyword in JavaScript, and Firefly list literals become JavaScript array literals.
123
+
124
+ 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`.
125
+ A static analysis is performed to decide which version to call.
126
+
127
+ 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,
128
+ the analysis picks the asynchronous version `List_map$` and uses the `await` keyword.
129
+
130
+ 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`.
131
+
132
+ This static analysis is necessarily conservative, and may occasionally call the asynchronous version of a function where the synchrhonous version would suffice.
133
+ When using the VSCode extension, the hover information for a call will note if the call is asynchronous.
134
+
@@ -86,7 +86,7 @@ include "node_modules"
86
86
  This instructs the compiler to copy the file or directory `mypackage/.firefly/include/node_modules` verbatim into the `mypackage/.firefly/ouput/node/mygroup/mypackage/node/node_modules` directory. It doesn't do anything for the browser target.
87
87
 
88
88
 
89
- # Imports
89
+ # Imports and exports
90
90
 
91
91
  To access the symbols that a module exports, it is necessary to import it:
92
92
 
@@ -116,13 +116,10 @@ The symbols can then be accessed using the `W.` prefix:
116
116
  W.new(system, "localhost", 8080)
117
117
  ```
118
118
 
119
-
120
- # Exports
121
-
122
119
  Currently, all top level definitions are automatically exported. This is likely to change in the future.
123
120
 
124
121
 
125
- # Main
122
+ # Main functions
126
123
 
127
124
  In Firefly, there are three targets, each with its own main function:
128
125
 
@@ -140,15 +137,12 @@ buildMain(system: BuildSystem) {
140
137
  }
141
138
  ```
142
139
 
143
- The three main functions may coexist in the same file.
144
-
145
- The `nodeMain` function runs when you run your executable.
146
-
147
- The `browserMain` function runs in the browser.
148
-
149
- The `buildMain` function runs when you build your program.
140
+ The three main functions may coexist in the same file.
141
+ The `nodeMain` function runs when you run your executable,
142
+ the `browserMain` function runs in the browser, and
143
+ the `buildMain` function runs when you build your program.
150
144
 
151
- The `system` parameter is an object that lets you do I/O in the target system.
145
+ The `system` parameter is an object with methods that let you do I/O in the target system.
152
146
 
153
147
 
154
148
  # Constants
@@ -0,0 +1,48 @@
1
+ # Structured concurrency
2
+
3
+ Firefly has a lightweight task system that supports structured concurrency and cancellation.
4
+
5
+ Tasks are structured in a parent-child relationship, where the parent scope waits for all its child tasks to complete before proceeding.
6
+
7
+ If the parent task or a sibling subtask fails with an uncaught exception, the other subtasks are cancelled.
8
+
9
+
10
+ # Spawning a subtask
11
+
12
+ The `system` parameter for the main function has a `mainTask()` method that returns a `Task`.
13
+ This is the main task whose lifecycle corresponds to that of the application.
14
+ A task can `spawn` subtasks:
15
+
16
+ ```firefly
17
+ nodeMain(system: NodeSystem) {
18
+ system.mainTask().spawn {subtask =>
19
+ while {True} {
20
+ Log.trace("Hello from subtask!")
21
+ subtask.sleep(Duration(1.0))
22
+ }
23
+ }
24
+ while {True} {
25
+ Log.trace("Hello from main task!")
26
+ system.mainTask().sleep(Duration(1.0))
27
+ }
28
+ }
29
+ ```
30
+
31
+ In the above example, there's one while loop running in the subtask, and another running in the main task.
32
+ They run concurrently, and the `"Hello..."` messages are thus logged in an interleaved fashion.
33
+
34
+
35
+ # Waiting for subtasks
36
+
37
+ ```firefly
38
+ nodeMain(system: NodeSystem) {
39
+ system.mainTask().spawn {task =>
40
+ task.spawn {subtask =>
41
+ while {True} {
42
+ Log.trace("Hello from subtask!")
43
+ subtask.sleep(Duration(1.0))
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
@@ -0,0 +1,99 @@
1
+ # Structured concurrency
2
+
3
+ Firefly has a lightweight task system that supports structured concurrency and cancellation.
4
+
5
+ Tasks are structured in a parent-child relationship, where the parent scope waits for all its child tasks to complete before proceeding.
6
+
7
+ If the parent task or a sibling subtask fails with an uncaught exception, the other subtasks are cancelled.
8
+
9
+
10
+ # Waiting for results
11
+
12
+ A simple use case is to concurrently run some tasks and waiting for their results.
13
+ This can be done without explicitly spawning tasks, as there's a utility method `task.mapList()` for this:
14
+
15
+ ```firefly
16
+ let results = system.mainTask().mapList(urls) {url =>
17
+ system.httpClient().get(url) {_.readText()}
18
+ }
19
+ ```
20
+
21
+ This fetches all of the URLs concurrently.
22
+ If any fetch throws an exception, the other fetches are cancelled and `task.mapList()` rethrows an exception.
23
+ Otherwise, the list of results is returned.
24
+
25
+ Similarly, `task.raceList()` can be used to run some tasks concurrently, wait for the first one to complete, and cancel the others.
26
+
27
+
28
+ # Spawning tasks
29
+
30
+ A slightly more advanced use case is handling requests concurrently. It might look like this:
31
+
32
+ ```firefly
33
+ while {True} {
34
+ let request = waitForRequest()
35
+ system.mainTask().spawn {subtask =>
36
+ handleRequest(subtask, request)
37
+ }
38
+ }
39
+ ```
40
+
41
+ The infinite loop here calls some function to wait for the next request.
42
+ Then it spawns a subtask to handle the request, and loops back to waiting for the next request.
43
+ The subtask executes concurrently, and thus doesn't block the loop while the request is being handled.
44
+
45
+
46
+ # Channels
47
+
48
+ Concurrently running tasks may need to communicate while executing.
49
+ One mechanism for inter-task communication is the `Channel[T]` type, which represents an unbuffered multi-producer multi-consumer channel for messages of type `T`.
50
+
51
+ A function to read URLs from a channel, fetch JSON from those URLs, and write the results to another channel could look like this:
52
+
53
+ ```firefly
54
+ fetchTask(in: Channel[String], out: Channel[Json]) {
55
+ while {True} {
56
+ let url = in.read()
57
+ let result = system.httpClient().get(url) {_.readJson()}
58
+ out.write(result)
59
+ }
60
+ }
61
+ ```
62
+
63
+ The `in.read()` call waits until there's a task ready to write to the `in` channel.
64
+ Similarly, the `out.write()` call waits until there's a task ready to read from the `out` channel.
65
+
66
+ To do multiple requests concurrently, spawn multiple instances of the task:
67
+
68
+ ```firefly
69
+ let in = system.mainTask().channel()
70
+ let out = system.mainTask().channel()
71
+ 1.to(3).each {_ =>
72
+ system.mainTask().spawn {_ =>
73
+ fetchTask(in, out)
74
+ }
75
+ }
76
+ ```
77
+
78
+ Consider calling an API that either returns `{value: ...}` where `...` is some number, or `{add: ...}` where `...` is a list of API URLs to add up to a total.
79
+ A first attempt could be adding the following code:
80
+
81
+ ```firefly
82
+ mutable total = 0
83
+ in.write("https://example.com/my-api")
84
+ while {True} {
85
+ let json = out.read()
86
+ json.field("value").map {total += _.grabInt()}.else {
87
+ let urls = json.field("add").map {_.grabArray().map {_.grabString()}}
88
+ urls.each {url =>
89
+ in.write(url)
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ However, there are two problems here:
96
+
97
+ * If all the fetch tasks are waiting to write to the `out` channel, `in.write(url)` will block forever.
98
+ * There's no logic to discover that there are no API calls left to do, so the `total` will never be reported.
99
+
package/lux/Lux.ff CHANGED
@@ -5,6 +5,7 @@ import Css
5
5
  capability Lux(
6
6
  document: LuxDocument
7
7
  jsSystem: JsSystem
8
+ mutable dry: Option[Array[DryNode]]
8
9
  mutable cssClasses: StringMap[CssClass]
9
10
  mutable renderLock: Lock
10
11
  mutable task: Task
@@ -25,6 +26,49 @@ class LuxElement(element: JsValue, mutable child: Int, mutable keepChildren: Boo
25
26
 
26
27
  class LuxDocument(document: JsValue)
27
28
 
29
+ data LuxInvaldNameException(name: String)
30
+
31
+ class DryNode {
32
+ DryElement(tagName: String, attributes: StringMap[String], children: Array[DryNode])
33
+ DryFragment(children: Array[DryNode])
34
+ DryText(text: String)
35
+ }
36
+
37
+ extend self: DryNode {
38
+ toHtml(): String {
39
+ // https://www.w3.org/TR/xml/ - wrt. checkXmmlName, we only accept the ASCII subset
40
+ function checkXmlName(name: String): String {
41
+ if(name.size() == 0) {throw(LuxInvaldNameException(name))}
42
+ if(!name.grabFirst().isAsciiLetter() && !name.startsWith(":") && !name.startsWith("_")) {throw(LuxInvaldNameException(name))}
43
+ if(!name.all {c => c.isAsciiLetterOrDigit() || c == ':' || c == '_' || c == '-' || c == '.'}) {throw(LuxInvaldNameException(name))}
44
+ name
45
+ }
46
+ function escapeAttributeValue(value: String): String {
47
+ value.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;").replace("\"", "&#x22;")
48
+ }
49
+ function escapeText(text: String): String {
50
+ text.replace("&", "&#x26;").replace("<", "&#x3C;").replace(">", "&#x3E;")
51
+ }
52
+ let voidHtmlElements = [
53
+ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
54
+ ].toSet()
55
+ function toHtml(node: DryNode): String {
56
+ | DryElement(tagName, attributes, children) =>
57
+ let attributeHtml = attributes.toList().map {| Pair(name, value) =>
58
+ " " + checkXmlName(name) + "=\"" + escapeAttributeValue(value) + "\""
59
+ }.join()
60
+ let childrenHtml = children.toList().map {toHtml(_)}.join()
61
+ let endHtml = if(voidHtmlElements.contains(tagName)) {""} else {"</" + tagName + ">"}
62
+ "<" + checkXmlName(tagName) + attributeHtml + ">" + childrenHtml + endHtml
63
+ | DryFragment(children) =>
64
+ children.toList().map {toHtml(_)}.join()
65
+ | DryText(text) =>
66
+ escapeText(text)
67
+ }
68
+ toHtml(self)
69
+ }
70
+ }
71
+
28
72
  extend self: LuxElement {
29
73
  childAt(index: Int): JsValue {
30
74
  self.element.get("childNodes").get(index)
@@ -69,6 +113,7 @@ extend self: Lux {
69
113
  }
70
114
 
71
115
  text(value: String) {
116
+ self.dry.map {_.push(DryText(value))}.else:
72
117
  let oldNode = self.element.childAt(self.element.child)
73
118
  let oldValue = if(!oldNode.isNullOrUndefined()) {oldNode.get("data")} else {oldNode}
74
119
  if(oldValue.isNullOrUndefined() || oldValue.grabString() != value) {
@@ -79,6 +124,24 @@ extend self: Lux {
79
124
  }
80
125
 
81
126
  add(tagName: String, body: () => Unit = {}) {
127
+ self.dry.map {dry =>
128
+ self.dry = Some([].toArray())
129
+ let savedAttributes = self.attributes
130
+ let savedKeys = self.keys
131
+ self.attributes = None
132
+ self.key = ""
133
+ self.depth += 1
134
+ try {
135
+ body()
136
+ } finally {
137
+ dry.push(DryElement(tagName, self.attributes.else {StringMap.new()}, self.dry.grab()))
138
+ self.depth -= 1
139
+ self.attributes = savedAttributes
140
+ self.keys = savedKeys
141
+ self.element.child += 1
142
+ self.dry = Some(dry)
143
+ }
144
+ }.else:
82
145
  let node = patchElement(self, tagName)
83
146
  if(!node.get("luxHandlers").isNullOrUndefined()) {
84
147
  node.get("luxHandlers").grabArray().each {pair =>
@@ -148,6 +211,7 @@ extend self: Lux {
148
211
  setId(value: String) {self.set("id", value)}
149
212
 
150
213
  setValue(value: String) { // TODO: Not an attribute
214
+ self.dry.map {_ => self.set("value", value)}.else:
151
215
  self.element.element.set("value", value)
152
216
  }
153
217
 
@@ -167,15 +231,22 @@ extend self: Lux {
167
231
  cssClass(class: CssClass) {
168
232
  if(!self.cssClasses.has(class.name())) {
169
233
  self.cssClasses.set(class.name(), class)
234
+ if(self.dry.isEmpty()):
170
235
  let styleSheet = self.document.createElement("style")
171
236
  styleSheet.set("textContent", class.show())
172
237
  self.document.document.get("head").call1("appendChild", styleSheet)
173
238
  }
174
- self.element.element.get("classList").call1("add", class.name())
175
- self.set("class", self.element.element.get("className").grabString())
239
+ if(!self.dry.isEmpty()) {
240
+ let classNames = self.attributes.flatMap {_.get("class")}.else {""}
241
+ self.set("class", (classNames + " " + class.name()).trim())
242
+ } else {
243
+ self.element.element.get("classList").call1("add", class.name())
244
+ self.set("class", self.element.element.get("className").grabString())
245
+ }
176
246
  }
177
247
 
178
248
  on(event: String, handler: LuxEvent => Unit) {
249
+ if(self.dry.isEmpty()):
179
250
  let jsHandler = unsafeAsyncFunction1ToJs(self) {jsEvent =>
180
251
  self.renderLock.do(reentrant = False) {
181
252
  handler(unsafeJsToValue(jsEvent))
@@ -195,6 +266,7 @@ extend self: Lux {
195
266
  onInput(handler: LuxEvent => Unit) {self.on("input", handler)}
196
267
 
197
268
  useState[T: HasAnyTag](initialValue: T, body: (T, T => Unit) => Unit) {
269
+ self.dry.map {_ => body(initialValue, {_ => })}.else:
198
270
  self.depth += 1
199
271
  let value = getStateOnElement(self.element.element, self.depth, initialValue)
200
272
  mutable i = 0
@@ -231,6 +303,7 @@ extend self: Lux {
231
303
  }
232
304
 
233
305
  useCallback1[A1: Equal: HasAnyTag](callback: A1 => Unit, body: (A1 => Unit) => Unit) {
306
+ self.dry.map {_ => body(callback)}.else:
234
307
  self.depth += 1
235
308
  setStateOnElement(self.element.element, self.depth, callback)
236
309
  let element = self.element.element
@@ -246,6 +319,7 @@ extend self: Lux {
246
319
  }
247
320
 
248
321
  useLazy1[A1: Equal: HasAnyTag](a1: A1, body: A1 => Unit = {_ => }) {
322
+ self.dry.map {_ => body(a1)}.else:
249
323
  self.depth += 1
250
324
  try {
251
325
  let old = getStateOnElement(self.element.element, self.depth, None)
@@ -262,6 +336,7 @@ extend self: Lux {
262
336
  }
263
337
 
264
338
  useMemo1[A1: Equal: HasAnyTag, T: HasAnyTag](a1: A1, compute: A1 => T, body: T => Unit) {
339
+ self.dry.map {_ => body(compute(a1))}.else:
265
340
  self.depth += 1
266
341
  try {
267
342
  let old = getStateOnElement(self.element.element, self.depth, Pair(a1, None))
@@ -284,6 +359,7 @@ extend self: Lux {
284
359
  }
285
360
 
286
361
  useSuspense(suspense: () => Unit, body: Lux => Unit) {
362
+ self.dry.map {_ => suspense()}.else:
287
363
  let oldSubtask = getTaskOnElement(self.element.element)
288
364
  oldSubtask.each {task =>
289
365
  forceAsyncAbort(self, task)
@@ -463,10 +539,17 @@ render(browserSystem: BrowserSystem, element: JsValue, body: Lux => Unit) {
463
539
  while {!document.get("parentNode").isNullOrUndefined()} {
464
540
  document = document.get("parentNode")
465
541
  }
542
+ // [...document.querySelectorAll("[lux-class]")].map(x => x.getAttribute("lux-class"))
543
+ let staticCssClasses = StringMap.new()
544
+ let dummyCssClass = CssClass([], [], [])
545
+ document.call1("querySelectorAll", "[lux-class]").each {e =>
546
+ staticCssClasses.set(e.call1("getAttribute", "lux-class").grabString(), dummyCssClass)
547
+ }
466
548
  let lux = Lux(
467
549
  jsSystem = browserSystem.js()
468
550
  renderLock = browserSystem.mainTask().lock()
469
- cssClasses = StringMap.new()
551
+ dry = None
552
+ cssClasses = staticCssClasses
470
553
  task = browserSystem.mainTask()
471
554
  depth = 0
472
555
  document = LuxDocument(document)
@@ -485,3 +568,26 @@ renderById(browserSystem: BrowserSystem, id: String, body: Lux => Unit) {
485
568
  let element = browserSystem.js().global().get("document").call1("getElementById", id)
486
569
  render(browserSystem, element, body)
487
570
  }
571
+
572
+ renderToString(nodeSystem: NodeSystem, body: Lux => Unit): Pair[String, String] {
573
+ let children = [].toArray()
574
+ let lux = Lux(
575
+ jsSystem = nodeSystem.js()
576
+ renderLock = nodeSystem.mainTask().lock()
577
+ dry = Some(children)
578
+ cssClasses = StringMap.new()
579
+ task = nodeSystem.mainTask()
580
+ depth = 0
581
+ document = LuxDocument(nodeSystem.js().null())
582
+ element = LuxElement(nodeSystem.js().null(), 0, keepChildren = False)
583
+ keys = None
584
+ key = ""
585
+ attributes = None
586
+ renderQueue = Array.new()
587
+ )
588
+ body(lux)
589
+ let styleTags = lux.cssClasses.values().map {c =>
590
+ "<style lux-class=\"" + c.name() + "\">" + c.show() + "</style>"
591
+ }.join()
592
+ Pair(children.toList().map {_.toHtml()}.join(), styleTags)
593
+ }
package/lux/TestDry.ff ADDED
@@ -0,0 +1,27 @@
1
+ import Lux
2
+
3
+ nodeMain(system: NodeSystem) {
4
+
5
+
6
+
7
+ let html = Lux.renderToString(system, render)
8
+ Log.trace(html)
9
+
10
+ }
11
+
12
+ render(lux: Lux): Unit {
13
+ lux.div {
14
+ lux.set("id", "its-me")
15
+ lux.span {
16
+ lux.text("Hello")
17
+ lux.div {
18
+ lux.set("id", "it's \"a-me\"")
19
+ lux.span {
20
+ lux.text("Hello ]]>")
21
+ }
22
+ lux.text("Hi")
23
+ }
24
+ }
25
+ lux.text("Hi")
26
+ }
27
+ }
@@ -210,6 +210,22 @@ ff_core_Channel.ChannelAction_timeout(ff_core_Channel.readOr_(ff_core_Task.Task_
210
210
  }))
211
211
  }
212
212
 
213
+ export function Task_mapList(self_, list_, body_) {
214
+ return ff_core_Task.Task_all(self_, ff_core_List.List_map(list_, ((x_) => {
215
+ return (() => {
216
+ return body_(x_)
217
+ })
218
+ })))
219
+ }
220
+
221
+ export function Task_raceList(self_, list_, body_) {
222
+ return ff_core_Task.Task_race(self_, ff_core_List.List_map(list_, ((x_) => {
223
+ return (() => {
224
+ return body_(x_)
225
+ })
226
+ })))
227
+ }
228
+
213
229
  export function Task_all(self_, tasks_) {
214
230
  const successChannel_ = ff_core_Task.Task_channel(self_, 0);
215
231
  const failureChannel_ = ff_core_Task.Task_channel(self_, 0);
@@ -283,6 +299,22 @@ export async function Task_sleep$(self_, duration_, $task) {
283
299
  }), $task))
284
300
  }
285
301
 
302
+ export async function Task_mapList$(self_, list_, body_, $task) {
303
+ return (await ff_core_Task.Task_all$(self_, ff_core_List.List_map(list_, ((x_) => {
304
+ return (async ($task) => {
305
+ return (await body_(x_, $task))
306
+ })
307
+ })), $task))
308
+ }
309
+
310
+ export async function Task_raceList$(self_, list_, body_, $task) {
311
+ return (await ff_core_Task.Task_race$(self_, ff_core_List.List_map(list_, ((x_) => {
312
+ return (async ($task) => {
313
+ return (await body_(x_, $task))
314
+ })
315
+ })), $task))
316
+ }
317
+
286
318
  export async function Task_all$(self_, tasks_, $task) {
287
319
  const successChannel_ = (await ff_core_Task.Task_channel$(self_, 0, $task));
288
320
  const failureChannel_ = (await ff_core_Task.Task_channel$(self_, 0, $task));
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "description": "Firefly compiler",
5
5
  "author": "Firefly team",
6
6
  "license": "MIT",
7
- "version": "0.4.80",
7
+ "version": "0.4.81",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Ahnfelt/firefly-boot"
@@ -4,7 +4,7 @@
4
4
  "description": "Firefly language support",
5
5
  "author": "Firefly team",
6
6
  "license": "MIT",
7
- "version": "0.4.80",
7
+ "version": "0.4.81",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Ahnfelt/firefly-boot"