@panagos/jojoscript 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +809 -418
- package/bin/jojo.js +430 -170
- package/package.json +50 -59
- package/src/compiler.js +227 -186
- package/src/effect-parser.js +223 -0
- package/src/index.js +14 -0
- package/src/pipeline-hash.js +27 -0
- package/src/pipeline-ir.js +157 -0
- package/src/pipeline-parser.js +583 -418
- package/src/runtime/checkpoint-store.js +122 -0
- package/src/runtime/checkpoint.js +69 -0
- package/src/runtime/collections.js +467 -327
- package/src/runtime/effect-log.js +90 -0
- package/src/runtime/effects.js +263 -0
- package/src/runtime/errors.js +29 -0
- package/src/runtime/execution-context.js +86 -0
- package/src/runtime/index.js +70 -29
- package/src/runtime/inspect.js +62 -0
- package/src/runtime/metrics.js +41 -0
- package/src/runtime/retry.js +67 -0
- package/src/runtime/trace.js +66 -0
package/README.md
CHANGED
|
@@ -1,418 +1,809 @@
|
|
|
1
|
-
# JojoScript 0.
|
|
2
|
-
|
|
3
|
-
JojoScript is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
jojo --
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
###
|
|
48
|
-
|
|
49
|
-
```jojo
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
`await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
|
277
|
-
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
| `
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
-
|
|
312
|
-
|
|
313
|
-
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1
|
+
# JojoScript 0.9.0
|
|
2
|
+
|
|
3
|
+
JojoScript is a small JavaScript-compatible language and runtime for
|
|
4
|
+
expressive, composable pipelines. It adds lightweight syntax for declarations,
|
|
5
|
+
functions, pattern matching, type annotations, and pipelines while preserving
|
|
6
|
+
ordinary JavaScript. Its compiler uses a lightweight lexer and source
|
|
7
|
+
transformations rather than a full AST, keeping the toolchain compact. The
|
|
8
|
+
project also provides lazy and async collection operators, checkpointing,
|
|
9
|
+
retries, metrics, pipeline graphing, CLI workflows, effect handlers, and
|
|
10
|
+
VS Code editor support.
|
|
11
|
+
|
|
12
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history and
|
|
13
|
+
[example/](example/) for a complete runnable application that exercises
|
|
14
|
+
every feature described below.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @panagos/jojoscript
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
jojo build <file.jojo|directory> [--out <directory>] [--watch]
|
|
26
|
+
jojo run <file.jojo> [--out <directory>]
|
|
27
|
+
jojo graph <file.jojo> [--format text|dot]
|
|
28
|
+
jojo profile <file.jojo>
|
|
29
|
+
jojo resume <file.jojo> [--out <directory>]
|
|
30
|
+
jojo replay <file.jojo> --from <checkpoint> [--out <directory>]
|
|
31
|
+
jojo test <file.jojo>
|
|
32
|
+
jojo --version
|
|
33
|
+
jojo --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `jojo build src` compiles every `.jojo` file under `src` to `src/dist/*.js` (or `--out <dir>`).
|
|
37
|
+
- `jojo build src --watch` rebuilds whenever a `.jojo` file under `src` changes.
|
|
38
|
+
- `jojo run app.jojo` compiles a single file and immediately runs it with Node — useful for a fast dev loop.
|
|
39
|
+
- `jojo graph app.jojo` prints a static pipeline graph; use `--format dot` for Graphviz output.
|
|
40
|
+
- `jojo profile app.jojo` executes a pipeline and prints per-node item counts, duration, and throughput.
|
|
41
|
+
- `jojo resume app.jojo` reuses completed file-backed checkpoints; `jojo replay app.jojo --from name` starts from a selected checkpoint.
|
|
42
|
+
- `jojo test app.jojo` compiles the file and runs its co-located `<name>.test.js` test when present.
|
|
43
|
+
- Compiler and filesystem errors are reported as a single friendly line (`jojo: <message>`, exit code 1) instead of a raw stack trace. Unknown flags are rejected.
|
|
44
|
+
|
|
45
|
+
## Language features
|
|
46
|
+
|
|
47
|
+
### Immutable declaration
|
|
48
|
+
|
|
49
|
+
```jojo
|
|
50
|
+
users := getUsers()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
becomes:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const users = getUsers()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Mutable declaration
|
|
60
|
+
|
|
61
|
+
```jojo
|
|
62
|
+
mutable count := 0
|
|
63
|
+
count++
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
becomes:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
let count = 0
|
|
70
|
+
count++
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Existing JavaScript `const`, `let`, and `var` declarations are untouched.
|
|
74
|
+
|
|
75
|
+
### Destructuring declarations
|
|
76
|
+
|
|
77
|
+
Object and array patterns are supported with `:=` and `mutable :=`. Only the
|
|
78
|
+
declaration head is rewritten — the right-hand side is left completely
|
|
79
|
+
untouched and may span multiple lines.
|
|
80
|
+
|
|
81
|
+
```jojo
|
|
82
|
+
{ id, name } := getUser()
|
|
83
|
+
[head, ...rest] := items
|
|
84
|
+
mutable { count } := state
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
becomes:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const { id, name } = getUser()
|
|
91
|
+
const [head, ...rest] = items
|
|
92
|
+
let { count } = state
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Multi-binding (tuple) declarations
|
|
96
|
+
|
|
97
|
+
A comma-separated list of names before `:=` destructures a comma-separated
|
|
98
|
+
list of values on the right-hand side (single line only):
|
|
99
|
+
|
|
100
|
+
```jojo
|
|
101
|
+
a, b := 1, 2
|
|
102
|
+
mutable x, y := 10, 20
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
becomes:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const [a, b] = [1, 2]
|
|
109
|
+
let [x, y] = [10, 20]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This is a distinct construct from bracket destructuring: use `a, b := 1, 2`
|
|
113
|
+
to bind a fixed list of values, and `[a, b] := getPair()` to destructure a
|
|
114
|
+
single array-producing expression.
|
|
115
|
+
|
|
116
|
+
### Functions
|
|
117
|
+
|
|
118
|
+
Block functions:
|
|
119
|
+
|
|
120
|
+
```jojo
|
|
121
|
+
fn add(a, b) {
|
|
122
|
+
return a + b
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Expression functions:
|
|
127
|
+
|
|
128
|
+
```jojo
|
|
129
|
+
fn double(value) = value * 2
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Async functions:
|
|
133
|
+
|
|
134
|
+
```jojo
|
|
135
|
+
async fn loadUsers() {
|
|
136
|
+
return await getUsers()
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Optional type annotations
|
|
141
|
+
|
|
142
|
+
Parameters and return values can be annotated. Annotations are **never
|
|
143
|
+
checked** — they are stripped from the emitted JavaScript and re-expressed
|
|
144
|
+
as a JSDoc block above the function, so editors/tools that understand JSDoc
|
|
145
|
+
get parameter and return type information without JojoScript needing its own
|
|
146
|
+
type checker.
|
|
147
|
+
|
|
148
|
+
```jojo
|
|
149
|
+
fn add(a: number, b: number): number { return a + b }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
becomes:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
/**
|
|
156
|
+
* @param {number} a
|
|
157
|
+
* @param {number} b
|
|
158
|
+
* @returns {number}
|
|
159
|
+
*/
|
|
160
|
+
function add(a, b) { return a + b; }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Functions with no annotations are left completely untouched.
|
|
164
|
+
|
|
165
|
+
### `match` expression
|
|
166
|
+
|
|
167
|
+
`match` compares a subject against each pattern with `===`, in order, and
|
|
168
|
+
requires a default `_` arm as the last arm. Arms are single expressions
|
|
169
|
+
(not statement blocks).
|
|
170
|
+
|
|
171
|
+
```jojo
|
|
172
|
+
label := match status {
|
|
173
|
+
200 => "OK",
|
|
174
|
+
404 => "Not Found",
|
|
175
|
+
_ => "Unknown"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
becomes:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const label = (() => {
|
|
183
|
+
const __match = status;
|
|
184
|
+
if (__match === (200)) return "OK";
|
|
185
|
+
if (__match === (404)) return "Not Found";
|
|
186
|
+
return "Unknown";
|
|
187
|
+
})()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Patterns can be arbitrary expressions, which enables range-style matching
|
|
191
|
+
against `true`:
|
|
192
|
+
|
|
193
|
+
```jojo
|
|
194
|
+
category := match true {
|
|
195
|
+
value > 100 => "large",
|
|
196
|
+
value > 10 => "medium",
|
|
197
|
+
_ => "small"
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`match` is a value-equality construct, not structural pattern matching:
|
|
202
|
+
patterns are plain expressions compared with `===`.
|
|
203
|
+
|
|
204
|
+
### Pipelines
|
|
205
|
+
|
|
206
|
+
```jojo
|
|
207
|
+
users
|
|
208
|
+
|> filter(isAdult)
|
|
209
|
+
|> map(normalize)
|
|
210
|
+
|> sort(byName)
|
|
211
|
+
|> take(10)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
becomes nested JavaScript calls:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
take(sort(map(filter(users, isAdult), normalize), byName), 10)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A stage can have no arguments (`users |> unique`) or arguments
|
|
221
|
+
(`users |> take(10)`), and normal JavaScript expressions can be pipeline
|
|
222
|
+
inputs, including `await` expressions and pipelines inside `return`
|
|
223
|
+
statements, function bodies, and object literals.
|
|
224
|
+
|
|
225
|
+
#### Placeholder argument
|
|
226
|
+
|
|
227
|
+
By default the piped value is inserted as the stage's first argument. The
|
|
228
|
+
`_` placeholder lets you put it anywhere in the argument list instead:
|
|
229
|
+
|
|
230
|
+
```jojo
|
|
231
|
+
value |> divide(100, _)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
becomes:
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
divide(100, value)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### `catch` / `orElse` control stages
|
|
241
|
+
|
|
242
|
+
`catch` and `orElse` provide inline error handling without leaving pipeline
|
|
243
|
+
syntax. Both wrap the preceding pipeline in a try/catch, compiled as an
|
|
244
|
+
immediately-invoked arrow function:
|
|
245
|
+
|
|
246
|
+
```jojo
|
|
247
|
+
risky() |> catch(error => fallbackFor(error))
|
|
248
|
+
risky() |> orElse(0)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`catch(handler)` calls `handler(error)` and returns its result; `orElse(fallback)`
|
|
252
|
+
returns `fallback` directly. Both require exactly one argument.
|
|
253
|
+
|
|
254
|
+
#### `await` control stage
|
|
255
|
+
|
|
256
|
+
`|> await` unwraps a promise at the end of a pipeline, so an async pipeline
|
|
257
|
+
still reads as a single chain:
|
|
258
|
+
|
|
259
|
+
```jojo
|
|
260
|
+
return ids
|
|
261
|
+
|> mapAsync(id => repository.findById(id))
|
|
262
|
+
|> toArrayAsync
|
|
263
|
+
|> await
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`await`, `catch`, and `orElse` are resolved at compile time and are never
|
|
267
|
+
added to the automatic `@panagos/jojoscript/runtime` import.
|
|
268
|
+
|
|
269
|
+
### Standard pipeline runtime
|
|
270
|
+
|
|
271
|
+
The following pipeline stages are supplied by `@panagos/jojoscript/runtime`. The
|
|
272
|
+
compiler automatically generates only the runtime imports that are actually
|
|
273
|
+
used through pipeline syntax (custom/user-defined stages are never
|
|
274
|
+
auto-imported):
|
|
275
|
+
|
|
276
|
+
| Stage | Kind | Description |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `map` | lazy | Transform each item |
|
|
279
|
+
| `filter` | lazy | Keep items matching a predicate |
|
|
280
|
+
| `take` | lazy | Keep the first N items |
|
|
281
|
+
| `skip` | lazy | Discard the first N items |
|
|
282
|
+
| `unique` | lazy | Remove duplicates (by `===`) |
|
|
283
|
+
| `flatMap` | lazy | Map then flatten one level |
|
|
284
|
+
| `distinctBy` | lazy | Remove duplicates by a derived key |
|
|
285
|
+
| `tap` | lazy | Observe each item without changing the pipeline (side effects/debugging) |
|
|
286
|
+
| `chunk` | lazy | Group items into fixed-size arrays |
|
|
287
|
+
| `window` | lazy | Yield a sliding view of size N |
|
|
288
|
+
| `zip` | lazy | Pair items from two iterables, stopping at the shorter one |
|
|
289
|
+
| `zipWith` | lazy | Combine items from two iterables with an arity-2 combining function, stopping at the shorter one |
|
|
290
|
+
| `scan` | lazy | Yield a running accumulation (like `reduce`, but yields every step) |
|
|
291
|
+
| `mapAsync` | lazy, async | Like `map`, awaiting an async transform |
|
|
292
|
+
| `filterAsync` | lazy, async | Like `filter`, awaiting an async predicate |
|
|
293
|
+
| `sort` | eager | Materializes the input; sorting needs the complete input |
|
|
294
|
+
| `sortBy` | eager | Like `sort`, ordered by a derived key |
|
|
295
|
+
| `groupBy` | eager | Groups items into a `Map` keyed by a derived key |
|
|
296
|
+
| `partition` | eager | Splits into `[matching, nonMatching]` arrays |
|
|
297
|
+
| `reduce` | terminal | Accumulate to a single value |
|
|
298
|
+
| `toArray` | terminal | Materialize an iterator into an array |
|
|
299
|
+
| `toArrayAsync` | terminal, async | Materialize an async iterator into an array |
|
|
300
|
+
| `count` | terminal | Count items |
|
|
301
|
+
| `first` | terminal | First item, or `undefined` |
|
|
302
|
+
| `find` | terminal | First matching item, or `undefined` |
|
|
303
|
+
| `some` | terminal | Whether any item matches (short-circuits) |
|
|
304
|
+
| `every` | terminal | Whether every item matches (short-circuits) |
|
|
305
|
+
|
|
306
|
+
See [ITERATOR_PROPOSAL.md](ITERATOR_PROPOSAL.md) and [ITERATORS.md](ITERATORS.md)
|
|
307
|
+
for the full laziness/memory model.
|
|
308
|
+
|
|
309
|
+
## Effect Handlers
|
|
310
|
+
|
|
311
|
+
> This is a deliberately simplified effect-handler model, not full
|
|
312
|
+
> algebraic effects with delimited continuations (no `resume`, no
|
|
313
|
+
> resuming a suspended computation zero/more-than-once, no re-throwing to
|
|
314
|
+
> an outer handler after partial handling). What it *does* give you: named
|
|
315
|
+
> effects, dynamic handler dispatch, nesting/overriding, deterministic
|
|
316
|
+
> mock handlers, record/replay, and a `parallel()` effect — all with plain
|
|
317
|
+
> JavaScript interop. If you already know algebraic effects from OCaml/Eff/
|
|
318
|
+
> Koka, think "a practical subset", not "the whole theory".
|
|
319
|
+
|
|
320
|
+
The central idea:
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
Pipeline
|
|
324
|
+
describes computation
|
|
325
|
+
|
|
326
|
+
Execution Plan
|
|
327
|
+
describes how the computation can be executed
|
|
328
|
+
|
|
329
|
+
Effect
|
|
330
|
+
describes an external capability/action
|
|
331
|
+
|
|
332
|
+
Effect Handler
|
|
333
|
+
decides how that capability/action is interpreted
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Describe the computation once, choose its interpretation separately.**
|
|
337
|
+
|
|
338
|
+
### What an effect is
|
|
339
|
+
|
|
340
|
+
`effect name(params)` declares a named capability — "this program wants to
|
|
341
|
+
fetch a user", not "this program makes an HTTP GET request". It compiles to
|
|
342
|
+
a plain runtime call:
|
|
343
|
+
|
|
344
|
+
```jojo
|
|
345
|
+
effect fetchUser(id)
|
|
346
|
+
effect saveUser(user)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
becomes:
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
const fetchUser = defineEffect("fetchUser", ["id"])
|
|
353
|
+
const saveUser = defineEffect("saveUser", ["user"])
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Calling `fetchUser(id)` doesn't run anything itself — it looks up the
|
|
357
|
+
currently active handler for `"fetchUser"` and delegates to it. **Calling an
|
|
358
|
+
effect always returns a Promise**, even if the active handler is
|
|
359
|
+
synchronous, so the calling code never has to know or care whether the
|
|
360
|
+
handler in charge is a real (often async) implementation or a fake (often
|
|
361
|
+
sync) one.
|
|
362
|
+
|
|
363
|
+
### What an effect handler is, and how to install one
|
|
364
|
+
|
|
365
|
+
A handler supplies the implementation for one or more named effects, for
|
|
366
|
+
the dynamic extent of a block:
|
|
367
|
+
|
|
368
|
+
```jojo
|
|
369
|
+
effect fetchUser(id)
|
|
370
|
+
effect saveUser(user)
|
|
371
|
+
|
|
372
|
+
users
|
|
373
|
+
|> map(user => fetchUser(user.id))
|
|
374
|
+
|> map(saveUser)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
runs under:
|
|
378
|
+
|
|
379
|
+
```jojo
|
|
380
|
+
handle {
|
|
381
|
+
fetchUser: id => http.get(`/users/${id}`),
|
|
382
|
+
saveUser: user => database.save(user)
|
|
383
|
+
} {
|
|
384
|
+
users
|
|
385
|
+
|> map(user => fetchUser(user.id))
|
|
386
|
+
|> map(saveUser)
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
which compiles to:
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
(await withHandlers({
|
|
394
|
+
fetchUser: id => http.get(`/users/${id}`),
|
|
395
|
+
saveUser: user => database.save(user)
|
|
396
|
+
}, async () => {
|
|
397
|
+
users
|
|
398
|
+
|> map(user => fetchUser(user.id))
|
|
399
|
+
|> map(saveUser)
|
|
400
|
+
}))
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
A handler value can be a plain function, or a `{ handle, deterministic }`
|
|
404
|
+
descriptor when you want to declare it explicitly deterministic (useful for
|
|
405
|
+
inspection — see below):
|
|
406
|
+
|
|
407
|
+
```jojo
|
|
408
|
+
handle {
|
|
409
|
+
fetchUser: { handle: id => fakeUser(id), deterministic: true }
|
|
410
|
+
} {
|
|
411
|
+
...
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Because `handle { ... } { ... }` compiles to `await withHandlers(...)`, it
|
|
416
|
+
must be used inside an `async fn`/`async function`, or at the top level of
|
|
417
|
+
an ES module (Node's top-level `await`) — the same requirement the existing
|
|
418
|
+
`|> await` pipeline stage already has.
|
|
419
|
+
|
|
420
|
+
### How the same program runs under different handlers
|
|
421
|
+
|
|
422
|
+
The program never mentions which handler is active — only the caller
|
|
423
|
+
decides that, by choosing which `handle { ... }` wraps it:
|
|
424
|
+
|
|
425
|
+
```jojo
|
|
426
|
+
effect fetchUser(id)
|
|
427
|
+
effect saveUser(user)
|
|
428
|
+
|
|
429
|
+
async fn syncUser(id) {
|
|
430
|
+
user := await fetchUser(id)
|
|
431
|
+
return await saveUser(user)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async fn syncUserInProduction(id) {
|
|
435
|
+
return handle {
|
|
436
|
+
fetchUser: id => http.get(`/users/${id}`),
|
|
437
|
+
saveUser: user => database.save(user)
|
|
438
|
+
} {
|
|
439
|
+
return await syncUser(id)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async fn syncUserInTests(id) {
|
|
444
|
+
return handle {
|
|
445
|
+
fetchUser: id => ({ id, name: "Fake User" }),
|
|
446
|
+
saveUser: user => ({ ...user, savedTo: "in-memory" })
|
|
447
|
+
} {
|
|
448
|
+
return await syncUser(id)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`syncUser` is identical in both cases; only the handler map differs. See
|
|
454
|
+
[example/src/effects/multiple-handlers.jojo](example/src/effects/multiple-handlers.jojo)
|
|
455
|
+
and
|
|
456
|
+
[example/src/effects/deterministic-testing.jojo](example/src/effects/deterministic-testing.jojo).
|
|
457
|
+
|
|
458
|
+
### Nested handlers and resolution rules
|
|
459
|
+
|
|
460
|
+
`handle` blocks nest. An inner handler for a given effect name **overrides**
|
|
461
|
+
an outer one; an inner handler map that doesn't mention an effect leaves it
|
|
462
|
+
falling through to the next outer handler that does:
|
|
463
|
+
|
|
464
|
+
```jojo
|
|
465
|
+
handle {
|
|
466
|
+
fetchUser: outerFake
|
|
467
|
+
} {
|
|
468
|
+
handle {
|
|
469
|
+
fetchUser: innerFake
|
|
470
|
+
} {
|
|
471
|
+
users |> map(fetchUser) // resolves to innerFake
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
users |> map(fetchUser) // resolves to outerFake, outside the inner block
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Resolution walks from the innermost active `handle` block outward, taking
|
|
479
|
+
the first one that declares the effect. Once a `handle` block's body
|
|
480
|
+
finishes, its handlers are gone — code that runs afterwards (or a lazy
|
|
481
|
+
pipeline consumed afterwards, see the caveat below) no longer sees them.
|
|
482
|
+
|
|
483
|
+
### Error handling
|
|
484
|
+
|
|
485
|
+
| Situation | Result |
|
|
486
|
+
|---|---|
|
|
487
|
+
| Effect invoked with no active handler for it | `UnhandledEffectError`: `` Unhandled effect: fetchUser\nNo handler registered for effect "fetchUser" `` |
|
|
488
|
+
| Handler throws (or its returned Promise rejects) | `EffectHandlerError`, naming the effect and wrapping the original error as `.cause` |
|
|
489
|
+
| Handler value isn't a function or `{ handle, deterministic }` | `TypeError`, thrown immediately when `handle { ... }` installs it, not at call time |
|
|
490
|
+
| A handler recursively invokes the same effect (directly, or indirectly through another effect) | `RecursiveEffectError`, naming the effect |
|
|
491
|
+
| Effect invoked entirely outside any `handle` block | Same as "no active handler" — `UnhandledEffectError` |
|
|
492
|
+
|
|
493
|
+
### `parallel()` as an effect
|
|
494
|
+
|
|
495
|
+
`items |> parallel(n) |> map(fn)` is effect-backed. With no handler
|
|
496
|
+
installed, it behaves exactly as before (a bounded-concurrency worker
|
|
497
|
+
pool — existing programs are unaffected). Installing a `PARALLEL` handler
|
|
498
|
+
lets a different interpretation take over completely:
|
|
499
|
+
|
|
500
|
+
```jojo
|
|
501
|
+
handle {
|
|
502
|
+
PARALLEL: async ({ items, fn, concurrency }) => {
|
|
503
|
+
// deterministic, strictly sequential — great for tests
|
|
504
|
+
results := []
|
|
505
|
+
for await (const item of items) results.push(await fn(item))
|
|
506
|
+
return results
|
|
507
|
+
}
|
|
508
|
+
} {
|
|
509
|
+
numbers |> parallel(8) |> map(process) |> toArrayAsync |> await
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
A `PARALLEL` handler receives `{ items, fn, concurrency, meta }` and returns
|
|
514
|
+
an (async) iterable of results in order — the same contract the built-in
|
|
515
|
+
implementation follows. This lets production code use real concurrency,
|
|
516
|
+
tests force deterministic sequential execution, and a debugging handler
|
|
517
|
+
record scheduling information, all without changing the pipeline. See
|
|
518
|
+
[example/src/effects/parallel-effect.jojo](example/src/effects/parallel-effect.jojo).
|
|
519
|
+
|
|
520
|
+
### Effects in the execution plan
|
|
521
|
+
|
|
522
|
+
`jojo graph`/`analyzePipelines` expose which pipeline nodes reference which
|
|
523
|
+
declared effects, and a best-effort static "is a handler for it declared
|
|
524
|
+
anywhere in this file?" signal:
|
|
525
|
+
|
|
526
|
+
```jojo
|
|
527
|
+
effect fetchUser(id)
|
|
528
|
+
effect saveUser(user)
|
|
529
|
+
|
|
530
|
+
handle {
|
|
531
|
+
fetchUser: id => ({ id })
|
|
532
|
+
} {
|
|
533
|
+
users
|
|
534
|
+
|> map(user => fetchUser(user.id))
|
|
535
|
+
|> parallel(8)
|
|
536
|
+
|> map(saveUser)
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
SOURCE users
|
|
542
|
+
│
|
|
543
|
+
▼
|
|
544
|
+
MAP user => fetchUser(user.id) [effects: fetchUser]
|
|
545
|
+
│
|
|
546
|
+
▼
|
|
547
|
+
PARALLEL 8
|
|
548
|
+
│
|
|
549
|
+
▼
|
|
550
|
+
MAP saveUser [effects: saveUser]
|
|
551
|
+
│
|
|
552
|
+
▼
|
|
553
|
+
RESULT
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Programmatically, `analyzePipelines(source)` returns each pipeline's graph
|
|
557
|
+
with an `effects` array:
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
[
|
|
561
|
+
{ name: "fetchUser", nodeId: "node-1", staticallyHandled: true },
|
|
562
|
+
{ name: "saveUser", nodeId: "node-3", staticallyHandled: false }
|
|
563
|
+
]
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`staticallyHandled` is a textual, best-effort signal (a `handle { name: ... }`
|
|
567
|
+
naming that effect was found somewhere in the file) — it cannot see handlers
|
|
568
|
+
installed conditionally or dynamically at runtime. For that, call
|
|
569
|
+
`describeHandlers()` from inside a running `handle { ... }` block: it
|
|
570
|
+
reports, per currently-handled effect, whether the installed handler
|
|
571
|
+
declared itself `deterministic`, and the declaring effect's `sideEffecting`/
|
|
572
|
+
`replayable` metadata (from `effect name(params)`'s options, or defaults —
|
|
573
|
+
`sideEffecting: true`, `replayable: false`).
|
|
574
|
+
|
|
575
|
+
### Effects and checkpoint/resume
|
|
576
|
+
|
|
577
|
+
Consider:
|
|
578
|
+
|
|
579
|
+
```jojo
|
|
580
|
+
effect fetchUser(id)
|
|
581
|
+
|
|
582
|
+
ids
|
|
583
|
+
|> mapAsync(fetchUser)
|
|
584
|
+
|> checkpoint("users-fetched")
|
|
585
|
+
|> toArrayAsync
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
`checkpoint(name)` either streams items through once and durably saves them
|
|
589
|
+
all, or — if a checkpoint for `name` already completed — returns the saved
|
|
590
|
+
items **without ever touching the upstream iterable at all**. This gives
|
|
591
|
+
effects placed before a checkpoint a specific, non-magical guarantee:
|
|
592
|
+
|
|
593
|
+
- **Once a checkpoint has completed**, a later run does **not** re-invoke
|
|
594
|
+
the upstream effect(s) — the saved items are restored instead. This is
|
|
595
|
+
restoration from checkpoint state, not effect replay: the checkpoint
|
|
596
|
+
store never sees the individual `fetchUser` calls, only the pipeline
|
|
597
|
+
items that came out the other side.
|
|
598
|
+
- **If a checkpoint never completed** (crash, first run, ...), the upstream
|
|
599
|
+
effect(s) run again from scratch on the next attempt — there is no
|
|
600
|
+
per-item resume within an incomplete checkpoint.
|
|
601
|
+
|
|
602
|
+
**This is explicitly not exactly-once effect execution.** It is
|
|
603
|
+
"at-least-once until the checkpoint fully completes, then exactly zero
|
|
604
|
+
additional times after." A non-idempotent, non-replayable effect
|
|
605
|
+
(`replayable: false`, the default) must tolerate being invoked again on a
|
|
606
|
+
crash-and-retry before its checkpoint completes. Mark an effect
|
|
607
|
+
`{ replayable: true }` only if calling it again with the same arguments is
|
|
608
|
+
actually safe. See
|
|
609
|
+
[example/src/effects/checkpoint-resume.jojo](example/src/effects/checkpoint-resume.jojo)
|
|
610
|
+
and `test/effects-checkpoint.test.js` for both cases exercised end to end.
|
|
611
|
+
|
|
612
|
+
### Record/replay
|
|
613
|
+
|
|
614
|
+
A handler map can be wrapped to record every call (arguments + result) to
|
|
615
|
+
an `EffectLog`, and later replayed from that log without running any real
|
|
616
|
+
code:
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
import { recordHandlers, replayHandlers, withHandlers } from "@panagos/jojoscript/runtime"
|
|
620
|
+
import { FileEffectLog } from "@panagos/jojoscript/runtime"
|
|
621
|
+
|
|
622
|
+
const log = new FileEffectLog("./fetchUser.log.json")
|
|
623
|
+
|
|
624
|
+
// Recording run: calls the real handler, and appends each call to the log.
|
|
625
|
+
await withHandlers(recordHandlers({ fetchUser: realFetchUser }, log), program)
|
|
626
|
+
|
|
627
|
+
// Replay run: never calls realFetchUser again, just replays recorded results.
|
|
628
|
+
await withHandlers(replayHandlers(["fetchUser"], log), program)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
`EffectLog` (`MemoryEffectLog`/`FileEffectLog`) is a small, storage-agnostic
|
|
632
|
+
interface (`record`/`replayNext`), mirroring `CheckpointStore` — an
|
|
633
|
+
extension point for a future backend, not a full production-grade
|
|
634
|
+
persistence system.
|
|
635
|
+
|
|
636
|
+
### JavaScript interoperability
|
|
637
|
+
|
|
638
|
+
A handler is just a normal JavaScript function — sync or async — and can
|
|
639
|
+
call anything JavaScript can call:
|
|
640
|
+
|
|
641
|
+
```jojo
|
|
642
|
+
effect fetchUser(id)
|
|
643
|
+
|
|
644
|
+
handle {
|
|
645
|
+
fetchUser: id => fetch(`/users/${id}`)
|
|
646
|
+
} {
|
|
647
|
+
...
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Guarantees and limitations
|
|
652
|
+
|
|
653
|
+
- Handler dispatch is dynamic-scope-based (an `AsyncLocalStorage` stack),
|
|
654
|
+
the same mechanism `checkpoint`/`inspect`/metrics already use for
|
|
655
|
+
execution context. Because pipeline stages are lazy, a `handle { ... }`
|
|
656
|
+
block's handlers are only active while its body is *consumed* (e.g. via
|
|
657
|
+
`toArray`/`toArrayAsync`/`reduce`/...), not merely constructed — always
|
|
658
|
+
consume (or `await`) an effectful pipeline inside the `handle` block
|
|
659
|
+
itself.
|
|
660
|
+
- No delimited continuations: a handler cannot `resume` the computation
|
|
661
|
+
that invoked the effect, inspect intermediate state, or resume it more
|
|
662
|
+
than once.
|
|
663
|
+
- Recursion guard: a handler invoking the same effect it is currently
|
|
664
|
+
handling (directly or through another effect) is rejected with
|
|
665
|
+
`RecursiveEffectError` rather than hanging or overflowing the stack.
|
|
666
|
+
- Existing programs that never use `effect`/`handle` syntax, and existing
|
|
667
|
+
`parallel()` usage with no `PARALLEL` handler installed, are completely
|
|
668
|
+
unaffected — this is additive, opt-in syntax.
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
JojoScript intentionally leaves ordinary JavaScript alone. You can use:
|
|
673
|
+
|
|
674
|
+
- normal `const`, `let`, and `var`
|
|
675
|
+
- arrow functions
|
|
676
|
+
- classes
|
|
677
|
+
- destructuring (as plain JS assignment, not a `:=` declaration)
|
|
678
|
+
- spread/rest
|
|
679
|
+
- optional chaining
|
|
680
|
+
- nullish coalescing
|
|
681
|
+
- template literals
|
|
682
|
+
- loops
|
|
683
|
+
- try/catch
|
|
684
|
+
- array methods
|
|
685
|
+
- async/await
|
|
686
|
+
- built-in Node modules
|
|
687
|
+
- npm/bare imports
|
|
688
|
+
|
|
689
|
+
Local relative imports receive `.js` after compilation:
|
|
690
|
+
|
|
691
|
+
```jojo
|
|
692
|
+
import { users } from "./users"
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
becomes:
|
|
696
|
+
|
|
697
|
+
```js
|
|
698
|
+
import { users } from "./users.js"
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Bare imports remain untouched:
|
|
702
|
+
|
|
703
|
+
```jojo
|
|
704
|
+
import axios from "axios"
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
## Runtime package
|
|
708
|
+
|
|
709
|
+
Generated JavaScript imports the runtime using the package subpath:
|
|
710
|
+
|
|
711
|
+
```js
|
|
712
|
+
import { map, take } from "@panagos/jojoscript/runtime"
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The package exposes this through `package.json` `exports`, so consumers
|
|
716
|
+
install `jojoscript` as a normal dependency. If a file both manually imports
|
|
717
|
+
from `@panagos/jojoscript/runtime` (for a stage that's only ever called directly,
|
|
718
|
+
never through `|>`) and also uses pipeline syntax with overlapping names,
|
|
719
|
+
the compiler merges the auto-imported names into the existing import
|
|
720
|
+
instead of emitting a second, colliding import declaration.
|
|
721
|
+
|
|
722
|
+
## Example
|
|
723
|
+
|
|
724
|
+
See [example/](example/). It is a complete runnable project covering every
|
|
725
|
+
language feature above plus JavaScript compatibility: a Node/SQLite
|
|
726
|
+
application that fetches posts from a public API, stores them, and prints a
|
|
727
|
+
report, alongside one small focused module (and test) per feature. Every
|
|
728
|
+
module is imported and invoked from `example/src/main.jojo`, so both
|
|
729
|
+
`npm start` and `npm test` exercise the whole example.
|
|
730
|
+
|
|
731
|
+
[example/src/effects/](example/src/effects/) has six focused Effect Handlers
|
|
732
|
+
examples (each with a matching test under `example/test/`): basic effects,
|
|
733
|
+
running the same program with multiple handlers, deterministic testing,
|
|
734
|
+
`parallel()` as an effect, effect visibility in the execution plan, and the
|
|
735
|
+
checkpoint/resume interaction.
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
cd example
|
|
739
|
+
npm install
|
|
740
|
+
npm run build
|
|
741
|
+
npm start
|
|
742
|
+
npm test
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## Iterator pipelines
|
|
746
|
+
|
|
747
|
+
Collection pipeline stages are lazy by default. `map`, `filter`, `take`,
|
|
748
|
+
`skip`, `unique`, `flatMap`, `distinctBy`, `tap`, `chunk`, `window`, `zip`,
|
|
749
|
+
`zipWith`, `scan`, `mapAsync`, and `filterAsync` return iterators (or async iterators)
|
|
750
|
+
instead of allocating an intermediate array. Use `toArray`/`toArrayAsync`
|
|
751
|
+
when an array is actually required.
|
|
752
|
+
|
|
753
|
+
```jojo
|
|
754
|
+
const total = users
|
|
755
|
+
|> filter(user => user.active)
|
|
756
|
+
|> map(user => user.age)
|
|
757
|
+
|> reduce((sum, age) => sum + age, 0)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
`sort`, `sortBy`, `groupBy`, and `partition` are intentionally materialization
|
|
761
|
+
boundaries because they need the complete input. See
|
|
762
|
+
[ITERATORS.md](ITERATORS.md) for the complete proposal and memory model.
|
|
763
|
+
|
|
764
|
+
## Editor support
|
|
765
|
+
|
|
766
|
+
See [editors/vscode](editors/vscode) for a VS Code extension providing syntax
|
|
767
|
+
highlighting and a `Format Document` provider for `.jojo` files. It is not
|
|
768
|
+
published to the marketplace; package it with `vsce package` and install the
|
|
769
|
+
resulting `.vsix` locally.
|
|
770
|
+
|
|
771
|
+
## Known limitations
|
|
772
|
+
|
|
773
|
+
- **No source maps.** Stack traces from compiled `dist/*.js` point at the
|
|
774
|
+
generated JavaScript, not the original `.jojo` source.
|
|
775
|
+
- **No `jojo fmt` CLI command.** Formatting is only available through the
|
|
776
|
+
VS Code extension's `Format Document` command, not the `jojo` CLI.
|
|
777
|
+
- **No regex-literal awareness in the lexer.** A regex containing `{`, `}`,
|
|
778
|
+
`(`, or `)` can confuse brace/paren-depth-sensitive transforms (pipelines,
|
|
779
|
+
`match`, destructuring, type annotations). Prefer `new RegExp(...)` in code
|
|
780
|
+
paths that also use these constructs on the same line, or keep such regexes
|
|
781
|
+
simple.
|
|
782
|
+
- **Pipeline statement boundaries need a blank line (or a semicolon) between
|
|
783
|
+
consecutive `:=` statements that each end in a bare pipeline stage**, e.g.:
|
|
784
|
+
|
|
785
|
+
```jojo
|
|
786
|
+
a := x |> map(f)
|
|
787
|
+
b := y |> map(g) // may be parsed as a continuation of the line above
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
Add a blank line (or a trailing `;`) between them.
|
|
791
|
+
- **`match` subject expressions should be simple** (a single identifier,
|
|
792
|
+
member access, or a fully-parenthesized expression) and appear on the same
|
|
793
|
+
statement as the `{ ... }` block.
|
|
794
|
+
- **Multi-binding declarations (`a, b := 1, 2`) are single-line only** and
|
|
795
|
+
always wrap the right-hand side in `[...]`; use `[a, b] := expr()` to
|
|
796
|
+
destructure a single expression instead.
|
|
797
|
+
- **Effect Handlers are a simplified model, not full algebraic effects.**
|
|
798
|
+
No delimited continuations/`resume`; a handler cannot inspect or resume
|
|
799
|
+
the suspended computation, or resume it more than once. See the
|
|
800
|
+
"Effect Handlers" section for the full list of guarantees/limitations.
|
|
801
|
+
- **`handle { ... } { ... }` handlers are only active while their body is
|
|
802
|
+
actually consumed.** Constructing a lazy pipeline inside a `handle` block
|
|
803
|
+
without consuming it there (no `toArray`/`toArrayAsync`/`reduce`/...) means
|
|
804
|
+
effects pulled later, outside the block, won't see its handlers.
|
|
805
|
+
- **Only `PARALLEL` is a built-in effect today.** The other capabilities
|
|
806
|
+
mentioned as a long-term direction (`IO`, `HTTP`, `FILE`, `DATABASE`,
|
|
807
|
+
`SLEEP`, `TIME`, `RANDOM`, `CHECKPOINT`) are not implemented — user-defined
|
|
808
|
+
`effect` declarations cover the same need today.
|
|
809
|
+
|