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 +8 -0
- package/fireflysite/Guide.ff +2 -3
- package/fireflysite/Main.ff +34 -24
- package/fireflysite/ReferenceAll.ff +1 -2
- package/fireflysite/Test.ff +8 -0
- package/fireflysite/assets/markdown/reference/EmittedJavascript.md +66 -0
- package/fireflysite/assets/markdown/reference/Exceptions.md +101 -0
- package/fireflysite/assets/markdown/reference/FunctionsAndMethods.md +139 -9
- package/fireflysite/assets/markdown/reference/JavascriptInterop.md +134 -0
- package/fireflysite/assets/markdown/reference/ModulesAndPackages.md +7 -13
- package/fireflysite/assets/markdown/reference/OldStructuredConcurrency.md +48 -0
- package/fireflysite/assets/markdown/reference/StructuredConcurrency.md +99 -0
- package/lux/Lux.ff +109 -3
- package/lux/TestDry.ff +27 -0
- package/output/js/ff/core/Task.mjs +32 -0
- package/package.json +1 -1
- package/vscode/package.json +1 -1
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()
|
package/fireflysite/Guide.ff
CHANGED
|
@@ -92,12 +92,11 @@ data GuideDocument(
|
|
|
92
92
|
document: Document
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
render(lux: Lux,
|
|
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,
|
|
113
|
+
renderDocument(lux, httpClient, prefix, document, demos, nextDocument)
|
|
115
114
|
}
|
|
116
115
|
renderTopbar(lux, menu, setMenu)
|
|
117
116
|
lux.add("div") {
|
package/fireflysite/Main.ff
CHANGED
|
@@ -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'
|
|
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
|
}
|
package/fireflysite/Test.ff
CHANGED
|
@@ -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
|
-
|
|
155
|
+
In firefly anonymous functions are written in curlybrases and constucted like this:
|
|
156
156
|
|
|
157
157
|
```firefly
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
197
|
+
This is an anonymous function taking no arguments and returning `Unit`:
|
|
170
198
|
|
|
171
199
|
```firefly
|
|
172
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
|
48
|
+
}
|
|
49
|
+
function escapeText(text: String): String {
|
|
50
|
+
text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
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.
|
|
175
|
-
|
|
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
|
-
|
|
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