@panagos/jojoscript 0.10.1 → 1.0.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,4 +1,4 @@
1
- # JojoScript 0.10.1
1
+ # JojoScript 1.0.0
2
2
 
3
3
  JojoScript is a lightweight language and runtime for data pipelines and agent
4
4
  workflows, built on JavaScript.
@@ -22,8 +22,17 @@ npm install @panagos/jojoscript
22
22
 
23
23
  ```bash
24
24
  jojo build <file.jojo|directory> [--out <directory>] [--watch]
25
- jojo run <file.jojo> [--out <directory>]
26
- jojo graph <file.jojo> [--format text|dot]
25
+ jojo run <file.jojo> [--out <directory>] [--deterministic [--seed <n>]]
26
+ jojo graph <file.jojo> [--format text|dot] [--pipeline <name>]
27
+ jojo explain <file.jojo> [--format text|json] [--pipeline <name>]
28
+ jojo plan <file.jojo> [--format json] [--pipeline <name>]
29
+ jojo generate <plan.json>
30
+ jojo check <file.jojo|plan.json> [--format text|json] [--machine]
31
+ jojo describe <file.jojo> [--pipeline <name>]
32
+ jojo simulate <file.jojo> [--input <records>] [--format text|json] [--pipeline <name>]
33
+ jojo why <file.jojo> [--format text|json] [--pipeline <name>]
34
+ jojo format <file.jojo|directory> [--check]
35
+ jojo parse <file.jojo> [--json]
27
36
  jojo profile <file.jojo>
28
37
  jojo resume <file.jojo> [--out <directory>]
29
38
  jojo replay <file.jojo> --from <checkpoint> [--out <directory>]
@@ -35,7 +44,17 @@ jojo --help
35
44
  - `jojo build src` compiles every `.jojo` file under `src` to `src/dist/*.js` (or `--out <dir>`).
36
45
  - `jojo build src --watch` rebuilds whenever a `.jojo` file under `src` changes.
37
46
  - `jojo run app.jojo` compiles a single file and immediately runs it with Node — useful for a fast dev loop.
47
+ - `jojo run app.jojo --deterministic --seed 42` runs the program in [deterministic execution mode](#deterministic-execution-mode): seeded `random()`, a fixed clock, sequential `parallel()`, and deterministic effect handlers only.
38
48
  - `jojo graph app.jojo` prints a static pipeline graph; use `--format dot` for Graphviz output.
49
+ - `jojo explain app.jojo` describes what each pipeline means (stages, effects, properties) without running it; use `--format json` for tools and LLMs. See [Explaining pipelines](#explaining-pipelines).
50
+ - `jojo plan app.jojo` prints a canonical JSON execution plan, and `jojo generate plan.json` turns a plan back into validated JojoScript. See [Machine-readable execution plans](#machine-readable-execution-plans).
51
+ - `graph`, `explain`, and `plan` accept `--pipeline <name>` to select one pipeline by its (inferred or [declared](#named-pipelines)) name.
52
+ - `jojo check app.jojo` (or `plan.json`) reports structured diagnostics with codes, lines, and suggestions, and exits 1 on errors; `--machine` prints JSON. See [Checking programs](#checking-programs).
53
+ - `jojo describe app.jojo` prints one JSON document describing the pipelines, effects, functions, and types in a file. See [Describing programs](#describing-programs).
54
+ - `jojo simulate app.jojo --input 1000000` estimates item counts, effect calls, and risks for a given input size without running anything. See [Simulating pipelines](#simulating-pipelines).
55
+ - `jojo why app.jojo` answers questions such as "why is this pipeline not deterministic?" in plain language. See [Asking why](#asking-why).
56
+ - `jojo format src` reformats `.jojo` files in place; `--check` lists unformatted files and exits 1. See [Formatting](#formatting).
57
+ - `jojo parse app.jojo --json` lists every syntax construct (imports, effects, types, functions, pipelines, handle blocks, matches) with its line. See [Parsing programs](#parsing-programs).
39
58
  - `jojo profile app.jojo` executes a pipeline and prints per-node item counts, duration, and throughput.
40
59
  - `jojo resume app.jojo` reuses completed file-backed checkpoints; `jojo replay app.jojo --from name` starts from a selected checkpoint.
41
60
  - `jojo test app.jojo` compiles the file and runs its co-located `<name>.test.js` test when present.
@@ -136,6 +155,69 @@ async fn loadUsers() {
136
155
  }
137
156
  ```
138
157
 
158
+ ### Pure and effectful functions
159
+
160
+ `pure` and `effectful` declare a function's role. They compile away to an
161
+ ordinary function, and may follow `export` or surround `async`:
162
+
163
+ ```jojo
164
+ effect storeUser(user)
165
+
166
+ export pure fn normalizeUser(user) {
167
+ return { ...user, email: user.email.trim().toLowerCase() }
168
+ }
169
+
170
+ effectful fn saveUser(user) {
171
+ return storeUser(user)
172
+ }
173
+ ```
174
+
175
+ The compiler rejects a `pure fn` that uses a declared `effect` or an
176
+ `effectful fn`, whether it calls it or passes it along (`map(saveUser)`):
177
+
178
+ ```
179
+ pure fn "normalizeUser" uses effectful function "saveUser" (line 9)
180
+ ```
181
+
182
+ The check is static and name-based. Strings, comments, property access
183
+ (`user.saveUser`), and recursion are ignored, and calls to unannotated
184
+ functions are allowed. `jojo explain` reports a stage that uses an
185
+ `effectful fn` as `effectful` and the pipeline as non-deterministic, exactly as
186
+ it does for declared effects. See
187
+ [example/src/application/purity-example.jojo](example/src/application/purity-example.jojo).
188
+
189
+ ### Function contracts
190
+
191
+ A block function can state what it expects and what it guarantees. Each
192
+ clause goes on its own line between the parameter list and the opening `{`:
193
+
194
+ ```jojo
195
+ fn applyDiscount(price, percent)
196
+ requires price >= 0
197
+ requires percent >= 0 && percent <= 100
198
+ ensures result >= 0 && result <= price
199
+ {
200
+ return price - price * percent / 100
201
+ }
202
+ ```
203
+
204
+ `requires` clauses are checked on entry, in order, before the body runs.
205
+ `ensures` clauses are checked after it, with the return value available as
206
+ `result` (async functions are awaited first). The body runs inside an arrow
207
+ function, so `this` and `arguments` keep referring to the declared function.
208
+ A broken clause throws `ContractError`:
209
+
210
+ ```
211
+ ContractError: Contract violated: applyDiscount requires percent >= 0 && percent <= 100
212
+ ```
213
+
214
+ The error carries `functionName`, `kind` (`"requires"` or `"ensures"`), and
215
+ `condition`, and is non-retryable, so `retry()` never repeats a call that
216
+ broke its contract. Contract clauses keep the source line count. Contracts
217
+ apply to block functions only, and a function using `ensures` should not
218
+ have a parameter named `result`. See
219
+ [example/src/application/contracts-example.jojo](example/src/application/contracts-example.jojo).
220
+
139
221
  ### Optional type annotations
140
222
 
141
223
  Parameters and return values can be annotated. Annotations are **never
@@ -197,8 +279,77 @@ category := match true {
197
279
  }
198
280
  ```
199
281
 
200
- `match` is a value-equality construct, not structural pattern matching:
201
- patterns are plain expressions compared with `===`.
282
+ #### Structured patterns
283
+
284
+ Object patterns match on the shape of a value. Arms can be separated by
285
+ newlines instead of commas:
286
+
287
+ ```jojo
288
+ message := match result {
289
+ { status: "ok", value } => value
290
+ { status: "error", error: { code: 404 } } => "not found"
291
+ { status: "error", error } => handle(error)
292
+ }
293
+ ```
294
+
295
+ Inside an object pattern:
296
+
297
+ - a literal or expression (`status: "ok"`, `code: Status.OK`) is compared with `===`;
298
+ - a bare name (`value`) binds that field, and the field must be present;
299
+ - `key: name` binds the field under another name, and `key: { ... }` matches a nested object;
300
+ - quoted keys (`"content-type": "json"`) are supported.
301
+
302
+ A match with at least one object pattern may omit the `_` arm. If no arm
303
+ matches, it throws `TypeError: No match arm matched the value`, so an
304
+ unexpected shape fails loudly instead of returning `undefined`. A match whose
305
+ patterns are all plain expressions still requires a final `_` arm and
306
+ compiles exactly as before. See
307
+ [example/src/application/structured-match-example.jojo](example/src/application/structured-match-example.jojo).
308
+
309
+ ### Tagged unions
310
+
311
+ `type` declares a tagged union: a value that is exactly one of a fixed set of
312
+ variants. Each variant has a constructor:
313
+
314
+ ```jojo
315
+ type PaymentResult =
316
+ | Paid(amount)
317
+ | Declined(reason)
318
+ | Pending
319
+ ```
320
+
321
+ becomes:
322
+
323
+ ```js
324
+ const PaymentResult = defineType("PaymentResult", { Paid: ["amount"], Declined: ["reason"], Pending: [] }); const { Paid, Declined, Pending } = PaymentResult
325
+ ```
326
+
327
+ `Paid(40)` returns the frozen plain object `{ tag: "Paid", amount: 40 }`, so
328
+ values serialize to JSON unchanged. Constructors reject the wrong number of
329
+ arguments (`Paid expects 1 argument (amount), got 0`), and fieldless variants
330
+ are called with no arguments (`Pending()`). `export type` exports the type and
331
+ all of its constructors. Variant names must start with an uppercase letter,
332
+ variants and fields must be unique, and no field may be named `tag`.
333
+
334
+ Constructor patterns match a variant by its tag and bind its fields by
335
+ position, so they also match values decoded from JSON:
336
+
337
+ ```jojo
338
+ message := match result {
339
+ Paid(amount) => `paid ${amount}`
340
+ Declined("insufficient funds") => "ask for another card"
341
+ Declined(reason) => `declined: ${reason}`
342
+ Pending() => "waiting for the bank"
343
+ }
344
+ ```
345
+
346
+ Pattern arguments can be literals, `_`, bindings, or nested object and
347
+ constructor patterns. Like object patterns, a match over constructor patterns
348
+ may omit `_` and throws `TypeError` when nothing matches. A capitalized call
349
+ such as `Ok(v)` inside a `match` is always a constructor pattern. Avoid naming
350
+ a variant `Error`, since its constructor would shadow JavaScript's `Error` in
351
+ that module. See
352
+ [example/src/application/tagged-unions-example.jojo](example/src/application/tagged-unions-example.jojo).
202
353
 
203
354
  ### Pipelines
204
355
 
@@ -221,6 +372,52 @@ A stage can have no arguments (`users |> unique`) or arguments
221
372
  inputs, including `await` expressions and pipelines inside `return`
222
373
  statements, function bodies, and object literals.
223
374
 
375
+ #### Named pipelines
376
+
377
+ A `pipeline` block declares a named, reusable pipeline. Its first line names
378
+ the source, and the remaining lines are stages:
379
+
380
+ ```jojo
381
+ export pipeline enrichOrders {
382
+ source orders
383
+
384
+ |> filter(order => order.active)
385
+ |> map(addVat)
386
+ |> checkpoint("enriched")
387
+ |> toArrayAsync
388
+ |> await
389
+ }
390
+ ```
391
+
392
+ It compiles to a function. An identifier source becomes its single parameter
393
+ (`enrichOrders(orders)`); any other source expression gives a function with
394
+ no parameters. A pipeline with a `|> await` stage (or an awaited source) becomes an
395
+ `async` function; an `await` inside a stage callback does not. Also,
396
+ blank lines inside the block are allowed. Tools see the pipeline under its
397
+ declared name, so `jojo explain app.jojo --pipeline enrichOrders` (and the same
398
+ option on `graph` and `plan`) selects it directly. See
399
+ [example/src/application/named-pipeline-example.jojo](example/src/application/named-pipeline-example.jojo).
400
+
401
+ #### Stage IDs
402
+
403
+ A stage can be labelled with a trailing `@id` (letters, digits, `_`, and `-`,
404
+ starting with a letter or `_`):
405
+
406
+ ```jojo
407
+ saved := orders
408
+ |> filter(order => order.paid) @paid-only
409
+ |> map(assignCarrier) @pick-carrier
410
+ |> checkpoint("prepared")
411
+ ```
412
+
413
+ The label is removed from the compiled code. It becomes the stage's node id in
414
+ runtime metadata (checkpoint, inspect, parallel, batch, and `jojo profile`),
415
+ in `jojo graph`, in `jojo explain` (`id` and `@pick-carrier` in the text), and
416
+ in `jojo plan` (an `id` field that `jojo generate` writes back). Tools and
417
+ LLMs can therefore refer to "the `@pick-carrier` stage" instead of a position
418
+ in the source. IDs must be unique within a pipeline. They are not part of the
419
+ pipeline's hash, so renaming a label never invalidates saved checkpoints.
420
+
224
421
  #### Placeholder argument
225
422
 
226
423
  By default the piped value is inserted as the stage's first argument. The
@@ -325,6 +522,381 @@ auto-imported):
325
522
  See [ITERATOR_PROPOSAL.md](ITERATOR_PROPOSAL.md) and [ITERATORS.md](ITERATORS.md)
326
523
  for the full laziness/memory model.
327
524
 
525
+ ## Explaining pipelines
526
+
527
+ `jojo explain` lets a person, a tool, or an LLM ask JojoScript what a program
528
+ means instead of inferring it from source. It is static: nothing is executed.
529
+
530
+ ```jojo
531
+ effect enrichOrder(order)
532
+
533
+ export async fn enrichPaidOrders(orders) {
534
+ return handle {
535
+ enrichOrder: order => ({ ...order, region: "EU" })
536
+ } {
537
+ return orders
538
+ |> filter(order => order.status == "paid")
539
+ |> parallel(4)
540
+ |> map(enrichOrder)
541
+ |> checkpoint("enriched-orders")
542
+ |> toArrayAsync
543
+ |> await
544
+ }
545
+ }
546
+ ```
547
+
548
+ ```
549
+ $ jojo explain orders.jojo
550
+ PIPELINE enrichPaidOrders
551
+
552
+ STAGES
553
+ 1 source orders
554
+ 2 filter pure
555
+ 3 parallel concurrency=4
556
+ 4 map effectful: enrichOrder
557
+ 5 checkpoint enriched-orders
558
+ 6 toArrayAsync pure
559
+ 7 await control
560
+
561
+ EFFECTS
562
+ enrichOrder (handler declared)
563
+
564
+ PROPERTIES
565
+ lazy: true
566
+ parallel: true
567
+ resumable: true
568
+ deterministic: false
569
+ ```
570
+
571
+ `jojo explain orders.jojo --format json` prints the same information as an
572
+ array of `{ name, id, stages, effects, properties }` objects, and
573
+ `explainPipelines(source)` / `renderExplanationText(explanation)` are exported
574
+ from `@panagos/jojoscript` for programmatic use.
575
+
576
+ - **name** is inferred: the variable or property a pipeline is assigned to
577
+ (`paid := orders |> ...`), the `fn` name of an expression function, or the
578
+ enclosing named function of a `return` pipeline; otherwise `pipeline-N`.
579
+ - **stages** list each stage's type, raw `args`, referenced `effects`, and a
580
+ `detail`: the source expression, `concurrency=N` for `parallel`, `size=N` for
581
+ `batch`, the checkpoint name, `control` for `await`/`catch`/`orElse`/`pass`,
582
+ or `pure`/`effectful: <effects>`.
583
+ - **effects** are the declared effects the pipeline references, each with the
584
+ same best-effort `staticallyHandled` signal as `jojo graph`.
585
+ - **properties**: `lazy` (no `sort`/`sortBy`/`groupBy`/`partition`
586
+ materialization boundary), `parallel` (has a `parallel` stage), `resumable`
587
+ (has a `checkpoint` stage), `deterministic` (references no declared effect).
588
+
589
+ Purity and determinism only reflect references to declared `effect`s and
590
+ `effectful fn`s (see [Pure and effectful functions](#pure-and-effectful-functions)): a stage
591
+ calling ordinary JavaScript such as `Math.random()` or `fetch()` directly is
592
+ still reported as `pure`. Route external work through effects to make it
593
+ visible. See
594
+ [example/src/application/explain-example.jojo](example/src/application/explain-example.jojo).
595
+
596
+ ## Machine-readable execution plans
597
+
598
+ `jojo plan` gives every pipeline a stable, simple JSON representation, and
599
+ `jojo generate` turns such a plan back into JojoScript. A tool or an LLM can
600
+ read a program as data, or write a plan and have JojoScript validate it:
601
+
602
+ ```
603
+ Jojo source -> jojo plan -> JSON plan -> LLM
604
+ LLM -> JSON plan -> jojo generate -> validated Jojo program
605
+ ```
606
+
607
+ ```jojo
608
+ export fn topCustomers(customers) {
609
+ return customers
610
+ |> filter(customer => customer.active)
611
+ |> sortBy(customer => -customer.spend)
612
+ |> take(2)
613
+ |> map(customer => customer.name)
614
+ |> toArray
615
+ }
616
+ ```
617
+
618
+ ```
619
+ $ jojo plan customers.jojo --format=json
620
+ {
621
+ "version": 1,
622
+ "pipelines": [
623
+ {
624
+ "pipeline": "topCustomers",
625
+ "id": "…",
626
+ "stages": [
627
+ { "type": "source", "expression": "customers" },
628
+ { "type": "filter", "expression": "customer => customer.active" },
629
+ { "type": "sortBy", "expression": "customer => -customer.spend" },
630
+ { "type": "take", "args": ["2"] },
631
+ { "type": "map", "expression": "customer => customer.name" },
632
+ { "type": "toArray" }
633
+ ]
634
+ }
635
+ ]
636
+ }
637
+ ```
638
+
639
+ (Printed with two-space indentation; condensed here.)
640
+
641
+ The plan format, version 1:
642
+
643
+ | Field | Meaning |
644
+ |---|---|
645
+ | `version` | Always `1` for this format |
646
+ | `pipelines[].pipeline` | Inferred name (same rules as `jojo explain`), or `null` for an unnamed pipeline |
647
+ | `pipelines[].id` | The pipeline's deterministic hash (informational, ignored by `generate`) |
648
+ | `pipelines[].stages[0]` | `{ "type": "source", "expression": "..." }` |
649
+ | `parallel` / `batch` | `concurrency` / `size` as a positive integer |
650
+ | `checkpoint` | `name` as a string |
651
+ | `map`, `filter`, `flatMap`, `tap`, `mapAsync`, `filterAsync`, `find`, `some`, `every`, `sortBy`, `distinctBy`, `groupBy`, `partition`, `catch` | `expression`: the single function argument as source text |
652
+ | any other stage | `args`: argument source strings, omitted when there are none |
653
+ | `.prop` / `.method()` | `{ "type": ".length" }` reads a property; `{ "type": ".trim", "args": [] }` calls it |
654
+ | `id` (any stage after the source) | Optional [stage ID](#stage-ids) such as `"pick-carrier"`, unique within the pipeline |
655
+
656
+ A stage whose setting is not a literal (`parallel(limit)`) keeps it in `args`
657
+ instead, so a plan is always lossless. Arguments stay source text: the plan
658
+ describes computation without evaluating it.
659
+
660
+ `jojo generate plan.json` (or `planToJojo(plan)`) emits one
661
+ `name := source |> ...` statement per pipeline. It rejects unknown fields,
662
+ wrong types, misplaced `source` stages, and duplicate names with the exact path
663
+ of the problem (for example
664
+ `Invalid plan: pipelines[0].stages[2].concurrency must be a positive integer`),
665
+ then compiles the program and checks that it parses as JavaScript, without
666
+ running it. The generated program contains only the pipelines: their source
667
+ variables, custom stage functions, and effects must be supplied by
668
+ surrounding code. Validation is not a sandbox: expressions are code, so
669
+ review a generated program as you would any code you did not write.
670
+
671
+ `planPipelines(source)` and `planToJojo(plan)` are exported from
672
+ `@panagos/jojoscript`. See
673
+ [example/src/application/plan-example.jojo](example/src/application/plan-example.jojo).
674
+
675
+ ## Checking programs
676
+
677
+ `jojo check` validates a program, or a JSON plan, without running it. It is
678
+ designed for LLM-generated code: every problem is a structured diagnostic with
679
+ a stable code, a line, and a concrete suggestion, instead of a raw stack trace.
680
+
681
+ ```
682
+ $ jojo check generated.jojo
683
+ ERROR E301
684
+ line 8
685
+ Call divide(1, 0) violates "requires b != 0"
686
+ suggestion: Pass arguments to "divide" that satisfy b != 0
687
+
688
+ 1 error, 0 warnings
689
+ ```
690
+
691
+ `--machine` (or `--format json`) prints the same result as JSON:
692
+
693
+ ```json
694
+ {
695
+ "valid": false,
696
+ "errors": [
697
+ {
698
+ "code": "E301",
699
+ "severity": "error",
700
+ "line": 8,
701
+ "message": "Call divide(1, 0) violates \"requires b != 0\"",
702
+ "suggestion": "Pass arguments to \"divide\" that satisfy b != 0"
703
+ }
704
+ ],
705
+ "warnings": []
706
+ }
707
+ ```
708
+
709
+ The command exits with code 1 when there is at least one error, so a
710
+ generate-check-fix loop can rely on the exit code.
711
+
712
+ | Code | Severity | Meaning |
713
+ |---|---|---|
714
+ | `E100` | error | The compiler rejects the program (with its line when known) |
715
+ | `E101` | error | The compiled JavaScript does not parse (checked by Node's own parser) |
716
+ | `W102` | warning | A declared effect is used but no `handle { ... }` in the file handles it; a caller may still install one |
717
+ | `E201` | error | A `pure fn` uses a declared effect or an `effectful fn` |
718
+ | `E301` | error | A call with literal arguments breaks a `requires` clause |
719
+ | `E401` | error | A `match` over a declared tagged union misses a variant and has no `_` arm |
720
+ | `E402` | error | A constructor pattern has a different number of fields than its variant |
721
+ | `E700` | error | `jojo check plan.json`: the plan is invalid (message names the exact field) |
722
+
723
+ Contract clauses are only evaluated statically when they use nothing but the
724
+ function's parameters, literals, and operators, so `jojo check` never runs
725
+ program code. `checkSource(source)`, `checkPlan(plan)`, and
726
+ `renderCheckText(result)` are exported from `@panagos/jojoscript`. See
727
+ [example/src/application/check-example.jojo](example/src/application/check-example.jojo).
728
+
729
+ ## Describing programs
730
+
731
+ `jojo describe` prints one self-describing JSON document for a file, so a tool
732
+ or an LLM can learn what a program contains without reading its source:
733
+
734
+ ```json
735
+ {
736
+ "language": "jojo",
737
+ "version": "1",
738
+ "pipelines": [
739
+ {
740
+ "name": "openInvoices",
741
+ "inputs": ["records"],
742
+ "effects": [],
743
+ "stages": [
744
+ { "id": "unpaid", "operation": "filter", "expression": "record => !record.paid" },
745
+ { "id": "classify", "operation": "map", "expression": "toInvoice" },
746
+ { "id": "node-3", "operation": "checkpoint", "name": "open-invoices" }
747
+ ],
748
+ "properties": { "lazy": true, "parallel": false, "resumable": true, "deterministic": true }
749
+ }
750
+ ],
751
+ "effects": [{ "name": "storeInvoice", "params": ["invoice"], "handled": true }],
752
+ "functions": [
753
+ { "name": "toInvoice", "line": 12, "purity": "pure", "requires": ["record.amount >= 0"], "ensures": [] }
754
+ ],
755
+ "types": [
756
+ { "name": "Invoice", "variants": [{ "name": "Open", "fields": ["id", "amount"] }, { "name": "Settled", "fields": ["id"] }] }
757
+ ]
758
+ }
759
+ ```
760
+
761
+ - `pipelines` uses the same names, effects, and properties as `jojo explain`,
762
+ and the same stage fields as `jojo plan`. A pipeline whose source is an
763
+ identifier lists it in `inputs`. A stage's `id` is its
764
+ [stage ID](#stage-ids), or the runtime node id (`node-3`) used by
765
+ `jojo graph` and `jojo profile`.
766
+ - `effects` lists declared effects and whether a handler is statically
767
+ visible in the file.
768
+ - `functions` lists functions that declare a purity or contracts.
769
+ - `types` lists tagged unions with their variants and fields.
770
+
771
+ `--pipeline <name>` limits `pipelines` to one pipeline. `describeSource(source)`
772
+ is exported from `@panagos/jojoscript`. See
773
+ [example/src/application/describe-example.jojo](example/src/application/describe-example.jojo).
774
+
775
+ ## Parsing programs
776
+
777
+ `jojo parse <file.jojo> --json` prints the syntax-level view of a file: every
778
+ JojoScript construct the compiler recognizes, with its line, ordered by line.
779
+ Where `jojo describe` summarizes what a program means, `jojo parse` lists what
780
+ it contains, so a tool or an LLM can locate and edit constructs precisely.
781
+
782
+ ```json
783
+ {
784
+ "language": "jojo",
785
+ "version": "1",
786
+ "nodes": [
787
+ { "kind": "type", "line": 6, "name": "Decision", "exported": true,
788
+ "variants": [{ "name": "Approved", "fields": ["id"] }, { "name": "Escalated", "fields": ["id", "team"] }] },
789
+ { "kind": "function", "line": 10, "name": "decide", "params": ["refund"], "exported": false,
790
+ "async": false, "purity": null, "body": "expression", "requires": [], "ensures": [] },
791
+ { "kind": "match", "line": 13, "patterns": ["Approved(id)", "Escalated(id, team)"] },
792
+ { "kind": "pipeline", "line": 20, "name": "routeRefunds", "declared": false, "source": "refunds",
793
+ "stages": [{ "type": "map", "args": ["decide"], "id": null }] }
794
+ ]
795
+ }
796
+ ```
797
+
798
+ | Kind | Fields |
799
+ |---|---|
800
+ | `import` | `from`, `names` |
801
+ | `effect` | `name`, `params` |
802
+ | `type` | `name`, `exported`, `variants` (`name`, `fields`) |
803
+ | `function` | `name`, `params` (as written, including defaults), `exported`, `async`, `purity`, `body` (`"block"` or `"expression"`), `requires`, `ensures` |
804
+ | `pipeline` | `name`, `declared` (a `pipeline` block), `source`, `stages` (`type`, `args`, `id`) |
805
+ | `handle` | `handlers` |
806
+ | `match` | `patterns` |
807
+
808
+ JojoScript has no AST by design: nodes come from the same finders the compiler
809
+ uses, so `parse` never disagrees with what compiles. Invalid syntax is
810
+ reported as a single `jojo:` error line. `parseSource(source)` is exported
811
+ from `@panagos/jojoscript`. See
812
+ [example/src/application/parse-example.jojo](example/src/application/parse-example.jojo).
813
+
814
+ ## Simulating pipelines
815
+
816
+ `jojo simulate` answers "what would this pipeline do with N records?" before
817
+ anything runs. No stage or effect is executed:
818
+
819
+ ```
820
+ $ jojo simulate prices.jojo --input 250000 --pipeline pricedProducts
821
+ SIMULATION pricedProducts
822
+
823
+ INPUT
824
+ 250,000 records
825
+
826
+ OPERATIONS
827
+ 2 filter 250,000 items in
828
+ 3 parallel ≤250,000 items in, 16 workers
829
+ 4 map ≤250,000 items in, ≤250,000 calls to fetchPrice
830
+ 5 checkpoint ≤250,000 items in, saved as "priced"
831
+
832
+ POTENTIAL PROBLEMS
833
+ ⚠ stage 4: up to 250,000 calls to effect "fetchPrice"
834
+ ⚠ stage 4: effect "fetchPrice" runs before checkpoint "priced": items processed before a failure are fetched again on retry (at-least-once)
835
+ ```
836
+
837
+ Stages are numbered as in `jojo explain`, where the source is stage 1.
838
+ Item counts flow through the stages as upper bounds (`≤` once a filtering
839
+ stage such as `filter`, `unique`, or `distinctBy` has run). `take`, `skip`,
840
+ `chunk`, `batch`, and `window` change them exactly; `flatMap` and `groupBy`
841
+ make them unknown; `count`, `first`, `find`, `some`, `every`, and `reduce`
842
+ produce one result. A stage that uses a declared effect or an `effectful fn`
843
+ makes one call per item it receives.
844
+
845
+ Potential problems reported:
846
+
847
+ - 1,000 or more calls to one effect;
848
+ - an effect that runs before a checkpoint, and so may run again for items
849
+ processed before a failure (see
850
+ [Effects and checkpoint/resume](#effects-and-checkpointresume));
851
+ - a pipeline with effects but no checkpoint at all;
852
+ - `sort`, `sortBy`, `groupBy`, or `partition` holding 10,000 or more items in
853
+ memory.
854
+
855
+ The input defaults to 1,000 records. `--format json` prints the simulation as
856
+ data, and `--pipeline <name>` selects one pipeline. `simulatePipelines(source,
857
+ { input })` and `renderSimulationText(simulation)` are exported from
858
+ `@panagos/jojoscript`. See
859
+ [example/src/application/simulate-example.jojo](example/src/application/simulate-example.jojo).
860
+
861
+ ## Asking why
862
+
863
+ `jojo why` turns the static analysis into plain-language answers, numbered
864
+ like `jojo explain` (the source is stage 1):
865
+
866
+ ```
867
+ $ jojo why onboarding.jojo --pipeline verifiedCustomers
868
+ WHY verifiedCustomers
869
+
870
+ Why is this pipeline not deterministic?
871
+ → Stage 3 (mapAsync) uses effect verifyEmail; stage 5 (mapAsync) uses effectful function sendWelcome.
872
+
873
+ Why is this pipeline lazy?
874
+ → Every stage passes items on one at a time, so no stage waits for the complete input.
875
+
876
+ Why is this pipeline resumable?
877
+ → Checkpoint "verified" (stage 4) saves its items once they all complete; a rerun restores them instead of repeating stages 2-3.
878
+
879
+ Why can't stage 3 be moved before stage 2?
880
+ → Stage 3 (mapAsync) uses effect verifyEmail and stage 2 (filter) decides which items reach it: moving it earlier would also run it for items that stage 2 drops.
881
+
882
+ Why does checkpoint "verified" not guarantee replay safety?
883
+ → Effect verifyEmail runs before it (stage 3): if a run fails before "verified" completes, it runs again on retry (at-least-once, not exactly-once).
884
+
885
+ Why can stage 5 repeat after a failure?
886
+ → Stage 5 (mapAsync) uses effectful function sendWelcome after the last checkpoint "verified": a rerun restores the checkpoint and runs it again for every restored item.
887
+ ```
888
+
889
+ Every pipeline gets answers about determinism, laziness, and resumability.
890
+ Additional questions appear when they apply: an effectful stage behind a
891
+ filtering stage (`filter`, `filterAsync`, `unique`, `distinctBy`, `take`,
892
+ `skip`) cannot be moved before it, a `parallel` stage explains what it
893
+ parallelizes, and checkpoints explain which effects can run again on retry.
894
+ Answers only reflect declared effects and `effectful fn`s. `--format json`
895
+ prints `[{ name, answers: [{ question, answer }] }]`, and
896
+ `whyPipelines(source)` / `renderWhyText(why)` are exported from
897
+ `@panagos/jojoscript`. See
898
+ [example/src/application/why-example.jojo](example/src/application/why-example.jojo).
899
+
328
900
  ## Effect Handlers
329
901
 
330
902
  > This is a deliberately simplified effect-handler model, not full
@@ -704,6 +1276,44 @@ interface (`record`/`replayNext`), mirroring `CheckpointStore` — an
704
1276
  extension point for a future backend, not a full production-grade
705
1277
  persistence system.
706
1278
 
1279
+ ### Deterministic execution mode
1280
+
1281
+ `now()` and `random()` are built-in capabilities, like `parallel()`. Outside
1282
+ any handler they behave like `Date.now()` and `Math.random()`, and a
1283
+ `handle { TIME: ..., RANDOM: ... }` block can replace them:
1284
+
1285
+ ```jojo
1286
+ import { now, random } from "@panagos/jojoscript/runtime"
1287
+
1288
+ order := { id: Math.round(random() * 1000), createdAt: new Date(now()) }
1289
+ ```
1290
+
1291
+ `deterministic(fn, { seed, startTime })` runs `fn` so that the same inputs
1292
+ always produce the same outputs:
1293
+
1294
+ - `random()` is a seeded generator (`seed` defaults to `1`);
1295
+ - `now()` starts at `startTime` (default `0`) and advances 1ms per read;
1296
+ - `parallel()` stages process one item at a time, in order;
1297
+ - every declared effect must resolve to a handler declared deterministic
1298
+ (`{ handle, deterministic: true }`, or `replayHandlers(...)`); any other
1299
+ handler throws a non-retryable `NondeterministicEffectError` before it runs;
1300
+ - checkpoints record `now()` as their `createdAt`, so reruns write identical
1301
+ checkpoints.
1302
+
1303
+ ```js
1304
+ import { deterministic } from "@panagos/jojoscript/runtime"
1305
+
1306
+ const first = await deterministic(() => generateOrders(3), { seed: 2026 })
1307
+ const second = await deterministic(() => generateOrders(3), { seed: 2026 })
1308
+ // first and second are deep-equal
1309
+ ```
1310
+
1311
+ `jojo run app.jojo --deterministic [--seed <n>]` runs a whole program, top-level
1312
+ code included, in this mode. Determinism covers JojoScript's capabilities:
1313
+ direct calls to `Date.now()`, `Math.random()`, or I/O outside declared effects
1314
+ are not intercepted. See
1315
+ [example/src/application/deterministic-example.jojo](example/src/application/deterministic-example.jojo).
1316
+
707
1317
  ### JavaScript interoperability
708
1318
 
709
1319
  A handler is just a normal JavaScript function — sync or async — and can
@@ -790,6 +1400,38 @@ never through `|>`) and also uses pipeline syntax with overlapping names,
790
1400
  the compiler merges the auto-imported names into the existing import
791
1401
  instead of emitting a second, colliding import declaration.
792
1402
 
1403
+ ### Deterministic serialization
1404
+
1405
+ `toJson(value, { indent })` and `fromJson(text)` give JojoScript values one
1406
+ canonical JSON form:
1407
+
1408
+ ```jojo
1409
+ import { toJson, fromJson } from "@panagos/jojoscript/runtime"
1410
+
1411
+ snapshot := toJson({ name: "John", id: 42, placedAt: new Date(0) })
1412
+ // {"id":42,"name":"John","placedAt":{"$type":"Date","value":"1970-01-01T00:00:00.000Z"}}
1413
+
1414
+ order := fromJson(snapshot) // order.placedAt is a Date again
1415
+ ```
1416
+
1417
+ - Object keys are sorted recursively, so equal values always serialize to
1418
+ identical text, whatever order their properties were created in.
1419
+ - Values plain JSON would lose or change are written as `{ "$type": ... }`
1420
+ objects: `Date`, `Map`, `Set`, `BigInt`, `undefined`, `NaN`, `Infinity`,
1421
+ `-Infinity`, `-0`, and `Error` (name, message, own fields such as a
1422
+ `ContractError`'s `kind`, and `cause`). `fromJson` restores each exactly.
1423
+ - An ordinary object that has its own `$type` key is wrapped, so tags are
1424
+ never ambiguous. Tagged union values are plain objects and serialize as
1425
+ they are. Objects with a `toJSON` method serialize its result.
1426
+ - Functions, symbols, and circular structures throw a `TypeError`.
1427
+
1428
+ `FileCheckpointStore` writes and reads checkpoints in this format, so
1429
+ checkpointed dates, maps, and sets resume as themselves. Checkpoint files
1430
+ written as plain JSON before this change still load. Pipelines and execution
1431
+ plans have their own canonical form: the [`jojo plan`](#machine-readable-execution-plans)
1432
+ JSON. See
1433
+ [example/src/application/serialization-example.jojo](example/src/application/serialization-example.jojo).
1434
+
793
1435
  ### MCP support
794
1436
 
795
1437
  JojoScript includes a small MCP-style JSON-RPC runtime for Node.js. It does
@@ -962,10 +1604,36 @@ const total = users
962
1604
  boundaries because they need the complete input. See
963
1605
  [ITERATORS.md](ITERATORS.md) for the complete proposal and memory model.
964
1606
 
1607
+ ## Formatting
1608
+
1609
+ `jojo format <file.jojo|directory>` reformats files in place and prints each
1610
+ file it changed. `--check` changes nothing: it lists the files that need
1611
+ formatting and exits with code 1 if there are any, which suits CI and
1612
+ generate-format-check loops.
1613
+
1614
+ The formatter is line-based, like the compiler: it normalizes indentation and
1615
+ whitespace but never rewrites expressions.
1616
+
1617
+ - A line that opens brackets indents their contents one level, however many
1618
+ it opens (`posts.reduce((groups, post) => {`).
1619
+ - `|>` stages, `requires` / `ensures` clauses, `| Variant` lines, ternary
1620
+ `?` / `:` lines, leading `.method()` chains, and the line after a trailing
1621
+ `:=` or `=>` are indented one extra level. Stages inside a
1622
+ `pipeline name { ... }` block line up with its `source` line.
1623
+ - Trailing whitespace is removed, Windows line endings become `\n`, runs of
1624
+ blank lines collapse to one, and the file ends with a single newline.
1625
+ - Block comments and template literal text are left untouched.
1626
+
1627
+ `formatSource(source, { indent })` is exported from `@panagos/jojoscript`, and
1628
+ the VS Code extension's `Format Document` uses the same formatter. Every file
1629
+ under `example/src` is kept formatted by a test.
1630
+
965
1631
  ## Editor support
966
1632
 
967
1633
  See [editors/vscode](editors/vscode) for a VS Code extension providing syntax
968
- highlighting and a `Format Document` provider for `.jojo` files. It is not
1634
+ highlighting for all JojoScript syntax (pipelines, stage IDs, named
1635
+ pipelines, `match` patterns, tagged unions, effects, purity, and contracts)
1636
+ and a `Format Document` provider for `.jojo` files. It is not
969
1637
  published to the marketplace; package it with `vsce package` and install the
970
1638
  resulting `.vsix` locally.
971
1639
 
@@ -973,8 +1641,6 @@ resulting `.vsix` locally.
973
1641
 
974
1642
  - **No source maps.** Stack traces from compiled `dist/*.js` point at the
975
1643
  generated JavaScript, not the original `.jojo` source.
976
- - **No `jojo fmt` CLI command.** Formatting is only available through the
977
- VS Code extension's `Format Document` command, not the `jojo` CLI.
978
1644
  - **No regex-literal awareness in the lexer.** A regex containing `{`, `}`,
979
1645
  `(`, or `)` can confuse brace/paren-depth-sensitive transforms (pipelines,
980
1646
  `match`, destructuring, type annotations). Prefer `new RegExp(...)` in code
@@ -1003,7 +1669,7 @@ resulting `.vsix` locally.
1003
1669
  actually consumed.** Constructing a lazy pipeline inside a `handle` block
1004
1670
  without consuming it there (no `toArray`/`toArrayAsync`/`reduce`/...) means
1005
1671
  effects pulled later, outside the block, won't see its handlers.
1006
- - **Only `PARALLEL` is a built-in effect today.** The other capabilities
1007
- mentioned as a long-term direction (`IO`, `HTTP`, `FILE`, `DATABASE`,
1008
- `SLEEP`, `TIME`, `RANDOM`, `CHECKPOINT`) are not implemented — user-defined
1672
+ - **`PARALLEL`, `TIME`, and `RANDOM` are the only built-in effects.** The
1673
+ other capabilities mentioned as a long-term direction (`IO`, `HTTP`, `FILE`,
1674
+ `DATABASE`, `SLEEP`, `CHECKPOINT`) are not implemented; user-defined
1009
1675
  `effect` declarations cover the same need today.