@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 CHANGED
@@ -1,418 +1,809 @@
1
- # JojoScript 0.7.1
2
-
3
- JojoScript is a deliberately small source-to-source language that adds a
4
- handful of ergonomic constructs on top of JavaScript while keeping ordinary
5
- JavaScript valid. The compiler is a regex/token-based transpiler (not a full
6
- AST), so it stays tiny and every feature composes with plain JavaScript
7
- around it.
8
-
9
- See [CHANGELOG.md](CHANGELOG.md) for release history and
10
- [example/](example/) for a complete runnable application that exercises
11
- every feature described below.
12
-
13
- ## Install
14
-
15
- ```bash
16
- npm install @panagos/jojoscript
17
- ```
18
-
19
- ## CLI
20
-
21
- ```bash
22
- jojo build <file.jojo|directory> [--out <directory>] [--watch]
23
- jojo run <file.jojo> [--out <directory>]
24
- jojo --version
25
- jojo --help
26
- ```
27
-
28
- - `jojo build src` compiles every `.jojo` file under `src` to `src/dist/*.js` (or `--out <dir>`).
29
- - `jojo build src --watch` rebuilds whenever a `.jojo` file under `src` changes.
30
- - `jojo run app.jojo` compiles a single file and immediately runs it with Node — useful for a fast dev loop.
31
- - 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.
32
-
33
- ## Language features
34
-
35
- ### Immutable declaration
36
-
37
- ```jojo
38
- users := getUsers()
39
- ```
40
-
41
- becomes:
42
-
43
- ```js
44
- const users = getUsers()
45
- ```
46
-
47
- ### Mutable declaration
48
-
49
- ```jojo
50
- mutable count := 0
51
- count++
52
- ```
53
-
54
- becomes:
55
-
56
- ```js
57
- let count = 0
58
- count++
59
- ```
60
-
61
- Existing JavaScript `const`, `let`, and `var` declarations are untouched.
62
-
63
- ### Destructuring declarations
64
-
65
- Object and array patterns are supported with `:=` and `mutable :=`. Only the
66
- declaration head is rewritten — the right-hand side is left completely
67
- untouched and may span multiple lines.
68
-
69
- ```jojo
70
- { id, name } := getUser()
71
- [head, ...rest] := items
72
- mutable { count } := state
73
- ```
74
-
75
- becomes:
76
-
77
- ```js
78
- const { id, name } = getUser()
79
- const [head, ...rest] = items
80
- let { count } = state
81
- ```
82
-
83
- ### Multi-binding (tuple) declarations
84
-
85
- A comma-separated list of names before `:=` destructures a comma-separated
86
- list of values on the right-hand side (single line only):
87
-
88
- ```jojo
89
- a, b := 1, 2
90
- mutable x, y := 10, 20
91
- ```
92
-
93
- becomes:
94
-
95
- ```js
96
- const [a, b] = [1, 2]
97
- let [x, y] = [10, 20]
98
- ```
99
-
100
- This is a distinct construct from bracket destructuring: use `a, b := 1, 2`
101
- to bind a fixed list of values, and `[a, b] := getPair()` to destructure a
102
- single array-producing expression.
103
-
104
- ### Functions
105
-
106
- Block functions:
107
-
108
- ```jojo
109
- fn add(a, b) {
110
- return a + b
111
- }
112
- ```
113
-
114
- Expression functions:
115
-
116
- ```jojo
117
- fn double(value) = value * 2
118
- ```
119
-
120
- Async functions:
121
-
122
- ```jojo
123
- async fn loadUsers() {
124
- return await getUsers()
125
- }
126
- ```
127
-
128
- ### Optional type annotations
129
-
130
- Parameters and return values can be annotated. Annotations are **never
131
- checked** — they are stripped from the emitted JavaScript and re-expressed
132
- as a JSDoc block above the function, so editors/tools that understand JSDoc
133
- get parameter and return type information without JojoScript needing its own
134
- type checker.
135
-
136
- ```jojo
137
- fn add(a: number, b: number): number { return a + b }
138
- ```
139
-
140
- becomes:
141
-
142
- ```js
143
- /**
144
- * @param {number} a
145
- * @param {number} b
146
- * @returns {number}
147
- */
148
- function add(a, b) { return a + b; }
149
- ```
150
-
151
- Functions with no annotations are left completely untouched.
152
-
153
- ### `match` expression
154
-
155
- `match` compares a subject against each pattern with `===`, in order, and
156
- requires a default `_` arm as the last arm. Arms are single expressions
157
- (not statement blocks).
158
-
159
- ```jojo
160
- label := match status {
161
- 200 => "OK",
162
- 404 => "Not Found",
163
- _ => "Unknown"
164
- }
165
- ```
166
-
167
- becomes:
168
-
169
- ```js
170
- const label = (() => {
171
- const __match = status;
172
- if (__match === (200)) return "OK";
173
- if (__match === (404)) return "Not Found";
174
- return "Unknown";
175
- })()
176
- ```
177
-
178
- Patterns can be arbitrary expressions, which enables range-style matching
179
- against `true`:
180
-
181
- ```jojo
182
- category := match true {
183
- value > 100 => "large",
184
- value > 10 => "medium",
185
- _ => "small"
186
- }
187
- ```
188
-
189
- `match` is a value-equality construct, not structural pattern matching:
190
- patterns are plain expressions compared with `===`.
191
-
192
- ### Pipelines
193
-
194
- ```jojo
195
- users
196
- |> filter(isAdult)
197
- |> map(normalize)
198
- |> sort(byName)
199
- |> take(10)
200
- ```
201
-
202
- becomes nested JavaScript calls:
203
-
204
- ```js
205
- take(sort(map(filter(users, isAdult), normalize), byName), 10)
206
- ```
207
-
208
- A stage can have no arguments (`users |> unique`) or arguments
209
- (`users |> take(10)`), and normal JavaScript expressions can be pipeline
210
- inputs, including `await` expressions and pipelines inside `return`
211
- statements, function bodies, and object literals.
212
-
213
- #### Placeholder argument
214
-
215
- By default the piped value is inserted as the stage's first argument. The
216
- `_` placeholder lets you put it anywhere in the argument list instead:
217
-
218
- ```jojo
219
- value |> divide(100, _)
220
- ```
221
-
222
- becomes:
223
-
224
- ```js
225
- divide(100, value)
226
- ```
227
-
228
- #### `catch` / `orElse` control stages
229
-
230
- `catch` and `orElse` provide inline error handling without leaving pipeline
231
- syntax. Both wrap the preceding pipeline in a try/catch, compiled as an
232
- immediately-invoked arrow function:
233
-
234
- ```jojo
235
- risky() |> catch(error => fallbackFor(error))
236
- risky() |> orElse(0)
237
- ```
238
-
239
- `catch(handler)` calls `handler(error)` and returns its result; `orElse(fallback)`
240
- returns `fallback` directly. Both require exactly one argument.
241
-
242
- #### `await` control stage
243
-
244
- `|> await` unwraps a promise at the end of a pipeline, so an async pipeline
245
- still reads as a single chain:
246
-
247
- ```jojo
248
- return ids
249
- |> mapAsync(id => repository.findById(id))
250
- |> toArrayAsync
251
- |> await
252
- ```
253
-
254
- `await`, `catch`, and `orElse` are resolved at compile time and are never
255
- added to the automatic `@panagos/jojoscript/runtime` import.
256
-
257
- ### Standard pipeline runtime
258
-
259
- The following pipeline stages are supplied by `@panagos/jojoscript/runtime`. The
260
- compiler automatically generates only the runtime imports that are actually
261
- used through pipeline syntax (custom/user-defined stages are never
262
- auto-imported):
263
-
264
- | Stage | Kind | Description |
265
- |---|---|---|
266
- | `map` | lazy | Transform each item |
267
- | `filter` | lazy | Keep items matching a predicate |
268
- | `take` | lazy | Keep the first N items |
269
- | `skip` | lazy | Discard the first N items |
270
- | `unique` | lazy | Remove duplicates (by `===`) |
271
- | `flatMap` | lazy | Map then flatten one level |
272
- | `distinctBy` | lazy | Remove duplicates by a derived key |
273
- | `tap` | lazy | Observe each item without changing the pipeline (side effects/debugging) |
274
- | `chunk` | lazy | Group items into fixed-size arrays |
275
- | `window` | lazy | Yield a sliding view of size N |
276
- | `zip` | lazy | Pair items from two iterables, stopping at the shorter one |
277
- | `zipWith` | lazy | Combine items from two iterables with an arity-2 combining function, stopping at the shorter one |
278
- | `scan` | lazy | Yield a running accumulation (like `reduce`, but yields every step) |
279
- | `mapAsync` | lazy, async | Like `map`, awaiting an async transform |
280
- | `filterAsync` | lazy, async | Like `filter`, awaiting an async predicate |
281
- | `sort` | eager | Materializes the input; sorting needs the complete input |
282
- | `sortBy` | eager | Like `sort`, ordered by a derived key |
283
- | `groupBy` | eager | Groups items into a `Map` keyed by a derived key |
284
- | `partition` | eager | Splits into `[matching, nonMatching]` arrays |
285
- | `reduce` | terminal | Accumulate to a single value |
286
- | `toArray` | terminal | Materialize an iterator into an array |
287
- | `toArrayAsync` | terminal, async | Materialize an async iterator into an array |
288
- | `count` | terminal | Count items |
289
- | `first` | terminal | First item, or `undefined` |
290
- | `find` | terminal | First matching item, or `undefined` |
291
- | `some` | terminal | Whether any item matches (short-circuits) |
292
- | `every` | terminal | Whether every item matches (short-circuits) |
293
-
294
- See [ITERATOR_PROPOSAL.md](ITERATOR_PROPOSAL.md) and [ITERATORS.md](ITERATORS.md)
295
- for the full laziness/memory model.
296
-
297
- ## JavaScript compatibility
298
-
299
- JojoScript intentionally leaves ordinary JavaScript alone. You can use:
300
-
301
- - normal `const`, `let`, and `var`
302
- - arrow functions
303
- - classes
304
- - destructuring (as plain JS assignment, not a `:=` declaration)
305
- - spread/rest
306
- - optional chaining
307
- - nullish coalescing
308
- - template literals
309
- - loops
310
- - try/catch
311
- - array methods
312
- - async/await
313
- - built-in Node modules
314
- - npm/bare imports
315
-
316
- Local relative imports receive `.js` after compilation:
317
-
318
- ```jojo
319
- import { users } from "./users"
320
- ```
321
-
322
- becomes:
323
-
324
- ```js
325
- import { users } from "./users.js"
326
- ```
327
-
328
- Bare imports remain untouched:
329
-
330
- ```jojo
331
- import axios from "axios"
332
- ```
333
-
334
- ## Runtime package
335
-
336
- Generated JavaScript imports the runtime using the package subpath:
337
-
338
- ```js
339
- import { map, take } from "@panagos/jojoscript/runtime"
340
- ```
341
-
342
- The package exposes this through `package.json` `exports`, so consumers
343
- install `jojoscript` as a normal dependency. If a file both manually imports
344
- from `@panagos/jojoscript/runtime` (for a stage that's only ever called directly,
345
- never through `|>`) and also uses pipeline syntax with overlapping names,
346
- the compiler merges the auto-imported names into the existing import
347
- instead of emitting a second, colliding import declaration.
348
-
349
- ## Example
350
-
351
- See [example/](example/). It is a complete runnable project covering every
352
- language feature above plus JavaScript compatibility: a Node/SQLite
353
- application that fetches posts from a public API, stores them, and prints a
354
- report, alongside one small focused module (and test) per feature. Every
355
- module is imported and invoked from `example/src/main.jojo`, so both
356
- `npm start` and `npm test` exercise the whole example.
357
-
358
- ```bash
359
- cd example
360
- npm install
361
- npm run build
362
- npm start
363
- npm test
364
- ```
365
-
366
- ## Iterator pipelines
367
-
368
- Collection pipeline stages are lazy by default. `map`, `filter`, `take`,
369
- `skip`, `unique`, `flatMap`, `distinctBy`, `tap`, `chunk`, `window`, `zip`,
370
- `zipWith`, `scan`, `mapAsync`, and `filterAsync` return iterators (or async iterators)
371
- instead of allocating an intermediate array. Use `toArray`/`toArrayAsync`
372
- when an array is actually required.
373
-
374
- ```jojo
375
- const total = users
376
- |> filter(user => user.active)
377
- |> map(user => user.age)
378
- |> reduce((sum, age) => sum + age, 0)
379
- ```
380
-
381
- `sort`, `sortBy`, `groupBy`, and `partition` are intentionally materialization
382
- boundaries because they need the complete input. See
383
- [ITERATORS.md](ITERATORS.md) for the complete proposal and memory model.
384
-
385
- ## Editor support
386
-
387
- See [editors/vscode](editors/vscode) for a VS Code extension providing syntax
388
- highlighting and a `Format Document` provider for `.jojo` files. It is not
389
- published to the marketplace; package it with `vsce package` and install the
390
- resulting `.vsix` locally.
391
-
392
- ## Known limitations
393
-
394
- - **No source maps.** Stack traces from compiled `dist/*.js` point at the
395
- generated JavaScript, not the original `.jojo` source.
396
- - **No `jojo fmt` CLI command.** Formatting is only available through the
397
- VS Code extension's `Format Document` command, not the `jojo` CLI.
398
- - **No regex-literal awareness in the lexer.** A regex containing `{`, `}`,
399
- `(`, or `)` can confuse brace/paren-depth-sensitive transforms (pipelines,
400
- `match`, destructuring, type annotations). Prefer `new RegExp(...)` in code
401
- paths that also use these constructs on the same line, or keep such regexes
402
- simple.
403
- - **Pipeline statement boundaries need a blank line (or a semicolon) between
404
- consecutive `:=` statements that each end in a bare pipeline stage**, e.g.:
405
-
406
- ```jojo
407
- a := x |> map(f)
408
- b := y |> map(g) // may be parsed as a continuation of the line above
409
- ```
410
-
411
- Add a blank line (or a trailing `;`) between them.
412
- - **`match` subject expressions should be simple** (a single identifier,
413
- member access, or a fully-parenthesized expression) and appear on the same
414
- statement as the `{ ... }` block.
415
- - **Multi-binding declarations (`a, b := 1, 2`) are single-line only** and
416
- always wrap the right-hand side in `[...]`; use `[a, b] := expr()` to
417
- destructure a single expression instead.
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
+