fusion-lang 0.0.1.alpha2 → 0.0.2

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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +24 -0
  3. data/.simplecov +11 -0
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +11 -9
  6. data/Rakefile +8 -0
  7. data/docs/lang/design.md +289 -51
  8. data/docs/lang/implementation.md +279 -0
  9. data/docs/lang/roadmap.md +20 -36
  10. data/docs/user/explanation.md +5 -10
  11. data/docs/user/how-to-guides.md +145 -15
  12. data/docs/user/reference.md +365 -140
  13. data/docs/user/tutorial.md +22 -19
  14. data/examples/double.fsn +4 -1
  15. data/examples/factorial.fsn +6 -3
  16. data/examples/fizzbuzz.fsn +1 -4
  17. data/examples/gcd.fsn +9 -0
  18. data/examples/json_test.fsn +4 -0
  19. data/examples/matrix/OP.fsn +2 -0
  20. data/examples/matrix/average.fsn +2 -0
  21. data/examples/matrix/solve.fsn +2 -0
  22. data/examples/palindrome.fsn +1 -1
  23. data/exe/fusion +12 -12
  24. data/lib/fusion/ast.rb +76 -27
  25. data/lib/fusion/atom.rb +1 -1
  26. data/lib/fusion/cli/decoder.rb +13 -8
  27. data/lib/fusion/cli/encoder.rb +2 -2
  28. data/lib/fusion/cli/options.rb +134 -61
  29. data/lib/fusion/cli/parser.rb +4 -4
  30. data/lib/fusion/cli/repl.rb +32 -27
  31. data/lib/fusion/cli/serializer.rb +11 -11
  32. data/lib/fusion/cli.rb +120 -49
  33. data/lib/fusion/interpreter/builtins.rb +298 -160
  34. data/lib/fusion/interpreter/env.rb +42 -12
  35. data/lib/fusion/interpreter/error_val.rb +42 -20
  36. data/lib/fusion/interpreter/thunk.rb +53 -0
  37. data/lib/fusion/interpreter.rb +263 -98
  38. data/lib/fusion/lexer.rb +125 -37
  39. data/lib/fusion/parser.rb +245 -70
  40. data/lib/fusion/version.rb +3 -1
  41. data/lib/fusion.rb +0 -1
  42. data/stdlib/all.fsn +13 -0
  43. data/stdlib/any.fsn +12 -0
  44. data/stdlib/chars.fsn +5 -0
  45. data/stdlib/compact.fsn +5 -0
  46. data/stdlib/concat.fsn +6 -0
  47. data/stdlib/entries.fsn +6 -0
  48. data/stdlib/falsey.fsn +6 -0
  49. data/stdlib/filter.fsn +12 -0
  50. data/stdlib/flatten.fsn +7 -0
  51. data/stdlib/map.fsn +7 -4
  52. data/stdlib/matrix/Matrix.fsn +8 -0
  53. data/stdlib/matrix/OP.fsn +18 -0
  54. data/stdlib/matrix/add.fsn +7 -0
  55. data/stdlib/matrix/column.fsn +6 -0
  56. data/stdlib/matrix/determinant.fsn +16 -0
  57. data/stdlib/matrix/dimensions.fsn +5 -0
  58. data/stdlib/matrix/identity.fsn +6 -0
  59. data/stdlib/matrix/invert.fsn +26 -0
  60. data/stdlib/matrix/minor.fsn +15 -0
  61. data/stdlib/matrix/multiply.fsn +10 -0
  62. data/stdlib/matrix/negate.fsn +5 -0
  63. data/stdlib/matrix/product.fsn +11 -0
  64. data/stdlib/matrix/rotate.fsn +10 -0
  65. data/stdlib/matrix/row.fsn +6 -0
  66. data/stdlib/matrix/scale.fsn +6 -0
  67. data/stdlib/matrix/subtract.fsn +7 -0
  68. data/stdlib/matrix/sum.fsn +14 -0
  69. data/stdlib/matrix/transpose.fsn +5 -0
  70. data/stdlib/range.fsn +2 -2
  71. data/stdlib/reduce.fsn +8 -0
  72. data/stdlib/safe.fsn +7 -0
  73. data/stdlib/sanitize.fsn +2 -3
  74. data/stdlib/toObject.fsn +7 -0
  75. data/stdlib/truthy.fsn +7 -0
  76. data/stdlib/vector/Vector.fsn +7 -0
  77. data/stdlib/vector/add.fsn +6 -0
  78. data/stdlib/vector/cross.fsn +6 -0
  79. data/stdlib/vector/dot.fsn +6 -0
  80. data/stdlib/vector/norm.fsn +6 -0
  81. data/stdlib/vector/scale.fsn +5 -0
  82. data/stdlib/vector/subtract.fsn +6 -0
  83. data/stdlib/zip.fsn +6 -0
  84. metadata +50 -4
  85. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  86. data/stdlib/mapValues.fsn +0 -5
  87. data/stdlib/math/square.fsn +0 -4
@@ -41,13 +41,19 @@ Precedence, tightest to loosest:
41
41
 
42
42
  1. Primary: literals, `[...]`, `{...}`, `(...)` grouping, function literals,
43
43
  identifiers, `@`-references.
44
- 2. Postfix member/index access: `x.key`, `x[expr]`.
45
- 3. Error prefix: `!expr` (construct an error). Bare `!` (with no operand) is the
46
- same as `!null`.
47
- 4. Pipe (application): `value | function`. Left-associative
48
- (`a | f | g` ≡ `(a | f) | g`).
49
- 5. Clause arrow: `=>` (loosest, so the entire right-hand side of a clause is one
50
- expression).
44
+ 2. Postfix: member access `x.key`, index read `x[expr]`, index write `x[key = value]`.
45
+ 3. Unary prefix: `!x` (construct an error; bare `!` is `!null`), `-x` (negate),
46
+ `/x` (invert), `~x` (logical not).
47
+ 4. Pipe (application) and map-pipes: `value | function`, `|:` (map), `|?` (filter),
48
+ `|+` (reduce). Left-associative (`a | f | g` ≡ `(a | f) | g`).
49
+ 5. Multiplicative: `*`, `/`, `%`, `//`. Left-associative.
50
+ 6. Additive: `+`, `-`. Left-associative.
51
+ 7. Ordering: `??` and the comparisons `<`, `<=`, `>`, `>=`. Binary, left-associative.
52
+ 8. Equality: `==`.
53
+ 9. Logical and: `&&`.
54
+ 10. Logical or: `||`.
55
+ 11. Clause arrow: `=>` (loosest, so the entire right-hand side of a clause is one
56
+ expression).
51
57
 
52
58
  ### 2.2 Function literals
53
59
 
@@ -95,17 +101,25 @@ by the same rule.
95
101
  ```ebnf
96
102
  file = expr ;
97
103
 
98
- expr = pipe ;
99
- pipe = prefix { "|" prefix } ;
100
- prefix = "!" [ prefix ] | postfix ; (* bare "!" -> !null *)
101
- postfix = primary { "." identifier | "[" expr "]" } ;
104
+ expr = logical_or ;
105
+ logical_or = logical_and { "||" logical_and } ;
106
+ logical_and = equality { "&&" equality } ;
107
+ equality = ordering { "==" ordering } ;
108
+ ordering = additive { ( "??" | "<" | "<=" | ">" | ">=" ) additive } ;
109
+ additive = multiplicative { ( "+" | "-" ) multiplicative } ;
110
+ multiplicative = pipe { ( "*" | "/" | "%" | "//" ) pipe } ;
111
+ pipe = unary { ( "|" | "|:" | "|?" | "|+" ) unary } ;
112
+ unary = "!" [ unary ] (* bare "!" -> !null; "!x" builds an error *)
113
+ | ( "-" | "/" | "~" ) unary (* negate / invert / not; operand required *)
114
+ | postfix ;
115
+ postfix = primary { "." identifier | "[" expr [ "=" expr ] "]" } ; (* "[e]" reads, "[e = e]" writes *)
102
116
  primary = atom | array | object | function | identifier | fileref | "(" expr ")" ;
103
117
 
104
118
  identifier = letter { letter | digit | "_" } ;
105
119
 
106
120
  atom = "null" | "true" | "false" | number | string ;
107
- number = int_lit | float_lit ;
108
- int_lit = [ "-" ] digit { digit } ;
121
+ number = int_lit | float_lit ; (* unsigned; a negative is unary "-" *)
122
+ int_lit = digit { digit } ;
109
123
  float_lit = int_lit "." digit { digit } [ exp ] | int_lit exp ;
110
124
  exp = ("e" | "E") [ "+" | "-" ] digit { digit } ;
111
125
  string = '"' { char | escape } '"' ; (* char excludes raw newline; use \n *)
@@ -119,8 +133,8 @@ spread = "..." expr ;
119
133
  function = "(" [ clause { "," clause } [ "," ] ] ")" ; (* "()" is the empty function *)
120
134
  clause = pattern "=>" expr ;
121
135
 
122
- fileref = "@" [ refpath ] ; (* bare "@" = current file *)
123
- refpath = { "../" } segment { "/" segment } ; (* ".fsn" implied *)
136
+ fileref = ( "@" | "@@" ) [ path ] ; (* bare "@"/"@@" = current unit / its super *)
137
+ path = { ".." "/" } segment { "/" segment } ; (* one tight lexer token; ".fsn" implied *)
124
138
  segment = identifier ;
125
139
 
126
140
  pattern = p_error | p_guarded ;
@@ -128,7 +142,7 @@ p_error = "!" | "!" p_guarded ; (* bare "!" matches any e
128
142
  p_guarded = p_core [ "?" predicate ] ;
129
143
  predicate = pipe ; (* a `|` chain of functions; the matched value flows in *)
130
144
  p_core = p_literal | p_bind | p_wildcard | p_array | p_object ;
131
- p_literal = atom ;
145
+ p_literal = atom | "-" number ; (* "-" number is a negative literal *)
132
146
  p_wildcard = "_" ;
133
147
  p_bind = identifier ;
134
148
 
@@ -154,6 +168,60 @@ Object literals and object patterns may not repeat a fixed key. `{"a": …, "a":
154
168
  is a `syntax_error`. Keys arriving through `...spread` / `...rest` are dynamic and
155
169
  not checked.
156
170
 
171
+ A file-reference **path** is a single token, lexed only immediately after `@` or `@@`
172
+ with no intervening whitespace: tight `/`-separated `segment`s (identifiers) with an
173
+ optional leading `../` chain. So a `/` that is not part of such a path is division/invert:
174
+ `@a/b` is the path `a/b`, but `@a / b` is `@a` divided by `b`. `//` is always the
175
+ integer-quotient operator, never a path separator.
176
+
177
+ There are no negative-number tokens: `-` is always the negation/subtraction operator.
178
+ A negative literal is written `-` directly before a number — the parser folds it into a
179
+ literal in expressions, and `p_literal` admits it directly in patterns.
180
+
181
+ ### 2.7 Operators (syntactic sugar)
182
+
183
+ Every operator here is **pure syntactic sugar**: it desugars to a pipe into an `@OP.*`
184
+ member (§7.6), or, for the map-pipes, into a stdlib call.
185
+
186
+ ```
187
+ -a → negative literal if a is a number, else a | @OP.negate
188
+ /a → a | @OP.invert
189
+ ~a → a | @OP.not
190
+ a + b + c → [a, b, c] | @OP.sum
191
+ a - b → [a, b | @OP.negate] | @OP.sum (numeric b folds: a - 42 → [a, -42] | @OP.sum)
192
+ a * b * c → [a, b, c] | @OP.product
193
+ a / b → [a, b | @OP.invert] | @OP.product
194
+ a % b → [a, b] | @OP.modulo
195
+ a // b → [a, b] | @OP.quotient
196
+ a ?? b → [a, b] | @OP.compare
197
+ a < b → [a, b] | @OP.compare | @OP.lt (likewise <= / > / >= via @OP.lte / @OP.gt / @OP.gte)
198
+ a == b == c → [a, b, c] | @OP.equal
199
+ a && b && c → [a, b, c] | @OP.and
200
+ a || b || c → [a, b, c] | @OP.or
201
+ xs |: f → {"c": xs, "f": f} | @map
202
+ xs |? f → {"c": xs, "f": f} | @filter
203
+ xs |+ f → {"c": xs, "f": f} | @reduce
204
+ ```
205
+
206
+ Folding and associativity:
207
+
208
+ - A maximal run of `+`/`-` folds into one `@OP.sum` over all terms; each `-` term is
209
+ negated (a numeric literal folds to a negative literal, otherwise via `@OP.negate`).
210
+ - A maximal run of `*`/`/` folds into one `@OP.product`; each `/` term is inverted via
211
+ `@OP.invert` (never a literal, so `1/x` stays a float and `/0` stays a runtime error).
212
+ - Runs of `==`, `&&`, `||` fold n-ary into `@OP.equal` / `@OP.and` / `@OP.or`.
213
+ - `%`, `//`, `??`, `<`, `<=`, `>`, `>=` are binary and left-associative; they sit at
214
+ their level and break a fold run: `a * b % c` is `(a * b) % c`; `a ?? b == 0` is
215
+ `(a ?? b) == 0`. Comparisons do not chain: `a < b < c` is `(a < b) < c`, which
216
+ errors at runtime (a boolean is not comparable).
217
+
218
+ Because pipe binds tighter than the value operators, `x|@f + 1` is `(x|@f) + 1`; to pipe a
219
+ computed value onward, parenthesize it: `(a + b)|@f`, `(a < b)|@f`.
220
+
221
+ Comparisons: `a == b` is equality; `a < b` (and `<=` / `>` / `>=`) orders two numbers
222
+ or two strings. `??` exposes the underlying `-1`/`0`/`1` ordering that the comparison
223
+ desugaring reads through `@OP.lt` / `@OP.lte` / `@OP.gt` / `@OP.gte` (§7.2).
224
+
157
225
  ---
158
226
 
159
227
  ## 3. Functions and application
@@ -231,7 +299,7 @@ Rules:
231
299
  `p_core ? predicate` matches when `p_core` matches structurally **and** piping the
232
300
  matched value through `predicate` yields a **truthy** result. Truthiness is
233
301
  Ruby-style: every value is truthy except `false` and `null` (so `0` and `""` are
234
- truthy). The built-ins `@and` / `@or` / `@not` apply the same test.
302
+ truthy). The operators `@OP.and` / `@OP.or` / `@OP.not` apply the same test.
235
303
 
236
304
  The predicate is a `|` chain of functions, and the matched value flows in from the
237
305
  left: `a ? b | c` matches when `a` matches and `a | b | c` is truthy. A single-stage
@@ -313,8 +381,11 @@ particular built-ins:
313
381
  is per-call: it is not enough for the function to have *some* error clause;
314
382
  that clause must match the specific error received. An error of a shape no
315
383
  clause catches propagates unchanged.
316
- - **Built-in operations (`@add`, `@divide`, `@equals`, `@Integer`, …) all
317
- propagate** their input error without examining it. To inspect or compare an
384
+ - **Built-in and stdlib operations (`@math.divide`, `@OP.sum`, `@Integer`, …) all
385
+ propagate** their input error without examining it. The one deliberate
386
+ exception is the stdlib catcher `@safe` — an ordinary two-clause function
387
+ `(! => false, v => v)` that catches any error into `false` and passes regular
388
+ values through. To inspect or compare an
318
389
  error's payload, you must catch it first and operate on the extracted payload:
319
390
  `!42 | (!a => a) | @Integer` returns `true` (the payload `42` *is* an integer);
320
391
  `!42 | @Integer` returns `!42` (the predicate doesn't handle the error,
@@ -323,7 +394,7 @@ particular built-ins:
323
394
  evaluating an element/member. `[1, !"bad", 2]` evaluates to `!"bad"`, not to
324
395
  an array of three things.
325
396
  - **Constructing an error from an erroring expression** propagates the inner
326
- error rather than wrapping it. `!([5,0] | @divide)` evaluates to the division
397
+ error rather than wrapping it. `!([5,0] | @math.divide)` evaluates to the division
327
398
  error itself (a `math_error`, §6.5), never to an error wrapping an error. (This
328
399
  preserves the rule that there is never more than one error simultaneously.)
329
400
  - **When the function value itself is an error** (e.g. `value | @undefined_name`
@@ -334,9 +405,10 @@ particular built-ins:
334
405
  If a `?` predicate evaluates to an error (the predicate function itself errored,
335
406
  or it was a non-function error value), that error becomes the function's return
336
407
  value immediately. Subsequent clauses are **not** tried — predicate-errors are
337
- treated as program failures, not as "no match." This is the key reason to make
338
- your predicates *total* (end with `_ => false`): a predicate that can crash will
339
- short-circuit your whole function.
408
+ treated as program failures, not as "no match." This is the key reason to keep a
409
+ `?` predicate from raising: a predicate that can crash will short-circuit your
410
+ whole function. A non-matching input is not a crash — it falls through to `null`
411
+ (falsey) — so a predicate needs no `_ => false` catch-all to be safe here.
340
412
 
341
413
  ### 6.5 The standardized error payload
342
414
 
@@ -354,46 +426,48 @@ There are two origins of error values, and they differ in payload:
354
426
  #### Payload shape
355
427
 
356
428
  ```json
357
- {"kind": "type_error", "location": "builtin add", "operation": "add", "input": [1, "x"], "message": "expected numbers"}
429
+ {"kind": "argument_error", "origin": "builtin", "operation": "@math.divide", "status": 0, "input": [1, "x"], "expected": ["[_ ? @Number, _ ? @Number]"]}
358
430
  ```
359
431
 
360
432
  | Field | Required | Meaning |
361
433
  | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
362
- | `kind` | yes | The error category, from the closed set below. |
363
- | `location` | yes | Where the failing operation lives, from the closed set below. |
364
- | `operation` | yes | A short description of the operation that failed, e.g. `"\|"`, `".name"`, `"[2]"`, `"add"`, `"reading file"`, `"parsing"`. |
365
- | `input` | yes | The operand(s) the operation received often the offending value; for member/index access it is `[object, key]`. |
366
- | `message` | no | Extra human-readable detail, e.g. `"expected an object"`. |
434
+ | `kind` | yes | The error category. Possible values are defined below. |
435
+ | `origin` | yes | Where the failing operation is *defined*. Possible values are defined below. |
436
+ | `file` | no | The **innermost user-code file** on the call chain. `builtin`/`stdlib` frames are skipped. The path is **relative to** `Dir.pwd`. Contains `"<inline>"` for errors in the CLI `-e` option or the REPL. Contains `"<fusion>"` for an error above all user code (e.g. `stdin` present, but `code` is not a function). Present for `builtin`/`stdlib`/`code` origins; absent for a channel/runtime origin (`input`/`output`/`interpreter`). |
437
+ | `operation` | yes | The operation that failed. All `@`-references are named by their **source text**. A syntactic operation uses its form (`"\|"`, `".name"`, `"[]"`, `"parsing code"`, `"parsing JSON"`). Loading the top-level program file uses `"loading code"`. |
438
+ | `status` | yes | `0` or `1`. Whether the operation received an ordinary value (`0`) or an error value (`1`) |
439
+ | `input` | yes | The operation's input. A 0-argument operation (every `@`-reference except `@load`) carries `null`. |
440
+ | `expected` | no | The acceptable inputs as a list of Fusion **patterns**. The input matched none of them. |
441
+ | `message` | no | Extra human-readable detail, e.g. `"division by zero"`. Absent whenever `expected` is present. |
367
442
 
368
- #### `kind` — the closed set
443
+ #### Possible values for `kind`
369
444
 
370
445
  | `kind` | Raised when |
371
446
  | --------------------- | -------------------------------------------------------------------------------------------------------------- |
372
- | `syntax_error` | source code, or the JSON input, fails to parse. |
373
- | `reference_error` | an `@`-reference cannot be resolved: unknown name, file not found, a file-system failure, a non-productive data cycle, or a `@`-self-reference with no current file. |
374
- | `type_error` | a value has the wrong type for an operation (expected X / a type mismatch); also applying a non-function, spreading a non-array/object, member access on a non-object, or a wrong-typed index. |
375
- | `argument_error` | a built-in receives the wrong number/shape of arguments (e.g. not a pair), or an `array`/`object`-mode input envelope has the wrong shape (§9.4). Its `message` states the expected shape as a Fusion pattern where possible (the pair-built-ins report `expected [_, _]`). |
447
+ | `syntax_error` | source code or the JSON input fails to parse. |
448
+ | `reference_error` | an `@`-reference cannot be resolved: unknown name, file not found, a file-system failure, a non-productive data cycle, a target outside the jail (§9.2), no enclosing file for `@@` (§9.2). |
449
+ | `argument_error` | a value has the wrong shape or type for an operation: a built-in given the wrong number/shape of arguments (e.g. not a pair) or a wrong-typed value, applying a non-function, spreading a non-array/object, member access on a non-object, a wrong-typed index, or an `array`/`object`-mode input envelope of the wrong shape (§9.4). Its `expected` lists the acceptable inputs as patterns. |
376
450
  | `binding_error` | reading an unbound identifier, or binding the same name twice in one clause. |
377
- | `access_error` | a missing object key or an out-of-range array index — and nothing else (a non-object member access or a wrong-typed index is a `type_error`). |
451
+ | `access_error` | a missing object key or an out-of-range array index. |
378
452
  | `math_error` | division or modulo by zero, or a non-finite number. |
379
453
  | `conversion_error` | a value cannot be converted (`@toString` of an unconvertible type, `@parseNumber` of a non-numeric string). |
380
- | `stack_error` | recursion too deep (a stack overflow). |
381
- | `serialization_error` | a result, or a user error's payload, has no JSON form — see §9.3. |
454
+ | `limit_error` | a runtime resource limit was exceeded. `"stack level too deep"`. |
455
+ | `internal_error` | an interpreter BUG. Please open an issue. |
456
+ | `serialization_error` | a result/error value has no JSON form. It contains functions or non-finite numbers. See §9. |
382
457
 
383
- #### `location` the closed set
458
+ #### Possible values for `origin`
384
459
 
385
- | `location` | Meaning |
386
- | --------------- | ----------------------------------------------------------------- |
387
- | `builtin X` | the built-in named X, e.g. `builtin divide`. |
388
- | `stdlib X` | the standard-library file X. |
389
- | `code X` | the user source file X (basename). |
390
- | `code <inline>` | an inline `-e` program or a REPL statement. |
391
- | `input` | the input channel (stdin or the CLI-argument). |
392
- | `output` | the output channel (the serialized result). |
393
- | `interpreter` | the interpreter itself, e.g. a stack overflow. |
460
+ | `origin` | Meaning |
461
+ | ------------- | ------------------------------------------------------------------------ |
462
+ | `builtin` | a built-in operation (named by its `@`-reference in `operation`). |
463
+ | `stdlib` | a standard-library function (named by its `@`-reference in `operation`). |
464
+ | `code` | user source core (a file or an inline expression (`-e`/REPL). |
465
+ | `input` | the input channel (stdin). Usually syntax errors. |
466
+ | `output` | the output channel. Usually serialization errors. |
467
+ | `interpreter` | the interpreter itself, e.g. a stack overflow. |
394
468
 
395
469
  `input` and `output` name the data channels; they **never** refer to the program
396
- source, which always reports as `code X`.
470
+ source, which always reports as `code`.
397
471
 
398
472
  User errors don't have to adhere to this standard.
399
473
 
@@ -402,59 +476,74 @@ User errors don't have to adhere to this standard.
402
476
  ## 7. Built-in functions
403
477
 
404
478
  All built-ins are ordinary one-argument functions, **reached with an `@` prefix**
405
- (`@add`, `@Integer`, …); see §9.2 for how `@name` resolves. The names in the tables
479
+ (`@size`, `@Integer`, …); see §9.2 for how `@name` resolves. The names in the tables
406
480
  below are the built-in names; write `@` before them to use them. **Operations** return
407
481
  `!` on type-invalid or domain-invalid input. **Predicates** return `false` on any
408
482
  input that is not of the queried type (they never return `!`).
409
483
 
410
- ### 7.1 Arithmetic (operations)
484
+ The operators (`+ - * / == < …` and the boolean ops) live in the shadowable `@OP`
485
+ object (§7.6); a directory reskins them by placing an `OP.fsn`.
486
+
487
+ ### 7.1 Arithmetic
411
488
 
412
- | Name | Input | Result |
413
- | ---------- | ----------------- | ---------------------------------------------------------------------- |
414
- | `add` | `[number, number]`| sum |
415
- | `subtract` | `[number, number]`| difference |
416
- | `multiply` | `[number, number]`| product |
417
- | `divide` | `[number, number]`| quotient; integer if evenly divisible, else float; `!` if divisor is 0 |
418
- | `mod` | `[number, number]`| remainder; `!` if divisor is 0 |
419
- | `negate` | `number` | negation |
420
- | `floor` | `number` | floor (integer) |
489
+ In source you normally write the infix sugar of §2.7 — `a + b`, `-a`, `a * b`, `a % b`,
490
+ `a // b` which desugars to the members below. Addition, multiplication and negation are
491
+ `@OP.sum` / `@OP.product` / `@OP.negate` (§7.6); subtraction is `[a, b | @OP.negate] | @OP.sum`.
492
+ Integer division/remainder are `@OP.quotient` / `@OP.modulo`. Numerically correct
493
+ division and the other numeric functions live in `@math` (§7.6a): `@math.divide`,
494
+ `@math.floor`, `@math.round`, `@math.abs`, `@math.log`, `@math.sqrt`, etc.
421
495
 
422
- ### 7.2 Comparison (operations)
496
+ ### 7.2 Comparison
423
497
 
424
- | Name | Input | Result |
425
- | ----------- | --------------------------------------- | ---------------------------------------- |
426
- | `equals` | `[any, any]` | deep structural equality (boolean) |
427
- | `lessThan` | `[number, number]` or `[string, string]`| boolean; `!` on mismatched/invalid types |
498
+ Equality is `@OP.equal` (deep structural equality of a pair; §7.6) — used directly,
499
+ there is no `@eq` helper. Ordering is `@OP.compare`, which returns `-1`/`0`/`1`. The
500
+ comparison members **interpret that result** and are applied *after* it:
428
501
 
429
- Other comparisons (`lessEq`, `greaterThan`, `greaterEq`, `notEquals`) are specified
430
- for the standard library, derivable from `equals` and `lessThan`.
502
+ | Member | `-1` | `0` | `1` | `null` |
503
+ | -------- | ------ | ------ | ------ | ------ |
504
+ | `OP.lt` | `true` | `false`| `false`| `null` |
505
+ | `OP.gt` | `false`| `false`| `true` | `null` |
506
+ | `OP.lte` | `true` | `true` | `false`| `null` |
507
+ | `OP.gte` | `false`| `true` | `true` | `null` |
431
508
 
432
- ### 7.3 Boolean (operations)
509
+ `a < b` desugars to exactly this pipeline: `[a, b] | @OP.compare | @OP.lt` (likewise
510
+ `<=` / `>` / `>=`; equality is `a == b` — see §2.7). Both steps resolve `@OP` per
511
+ directory, so an override reskins the ordering and its reading together. A partial
512
+ order whose `compare` returns `null` for incomparable operands passes that `null`
513
+ straight through; any other input is an `argument_error`. A type mismatch surfaces
514
+ as `@OP.compare`'s own error, before the reader runs.
433
515
 
434
- These judge **truthiness** (the same Ruby-style test as `?` predicates: every value
435
- is truthy except `false` and `null`), not strict booleans, and always return a boolean.
516
+ ### 7.3 Boolean
436
517
 
437
- | Name | Input | Result |
438
- | ----- | -------- | --------------------------------------------------- |
439
- | `and` | `[_, _]` | `true` if both operands are truthy |
440
- | `or` | `[_, _]` | `true` if either operand is truthy |
441
- | `not` | `_` | `true` if the operand is falsey (`false` or `null`) |
518
+ The truthiness operators live in `@OP` (§7.6): `@OP.and`, `@OP.or`, `@OP.not`, written
519
+ with the sugar `a && b`, `a || b`, `~a` (§2.7). They
520
+ judge truthiness (every value is truthy except `false` and `null`), not strict
521
+ booleans, and always return a boolean. There are no top-level `@and`/`@or`/`@not`.
522
+ The stdlib helpers `@truthy` and `@falsey` reduce any single value to its truthiness
523
+ by **pattern matching** (independent of any `@OP` override): `@truthy` is `true` for
524
+ everything except `false`/`null`, and `@falsey` is its complement.
442
525
 
443
526
  ### 7.4 Strings and structure bridges (operations)
444
527
 
445
528
  | Name | Input | Result |
446
529
  | ------------- | --------------------------- | --------------------------------------- |
447
- | `length` | string / array / object | element/character/key count (integer) |
448
- | `concat` | `[string, string]` | concatenation |
449
- | `chars` | string | array of single-character strings |
450
- | `join` | `[array-of-strings, string]`| joined string |
530
+ | `size` | string / array / object | element/character/key count (integer) |
531
+ | `join` | `[array-of-strings, separator]` | the elements joined by the separator string |
532
+ | `split` | `[string, separator]` | array split on the **literal** separator (an empty separator splits into characters), keeping empty fields |
451
533
  | `toString` | any | string form of the value |
452
534
  | `parseNumber` | string | integer or float; `!` if not numeric |
453
535
  | `keys` | object | array of key strings |
454
536
  | `values` | object | array of values |
455
- | `get` | `[array, int]` or `[object, string-key]` | element at that index/key (like `[]`, §8); `!` if out of range / missing |
456
- | `set` | `[array, int, value]` or `[object, string-key, value]` | a **new** array/object with that entry set; an array index must already exist, an object key may be new |
457
- | `toObject` | `[[string-key, value], …]` | object built from entries; later duplicate keys win |
537
+
538
+ `concat` (array of strings, any length concatenation) and `chars` (string
539
+ array of its characters) are **standard-library** functions built on `join` /
540
+ `split`, not built-ins. So are `entries` (object → array of its `[key, value]` entries, in
541
+ insertion order; built on `keys`) and `toObject` (`[[string-key, value], …]` →
542
+ object, later duplicate keys win; built on the `[=]` setter). They are inverses:
543
+ `obj | @entries | @toObject` is `obj`.
544
+
545
+ Indexed read (`x[k]`) and write (`x[k = v]`) are **core syntax**, not built-ins — there
546
+ is no `@get`/`@set`; see §8.
458
547
 
459
548
  ### 7.5 Type predicates (predicates)
460
549
 
@@ -471,6 +560,7 @@ functions provides a runtime type system.
471
560
  | `String` | strings | |
472
561
  | `Array` | arrays | `[_]` |
473
562
  | `Object` | objects | `{_}` |
563
+ | `Collection`| arrays and objects | |
474
564
  | `Function` | any function (builtin, stdlib, user) | |
475
565
  | `NonFinite` | "Infinity", "-Infinity", "NaN" | |
476
566
 
@@ -478,7 +568,64 @@ Notes:
478
568
  - Booleans are separate from numbers. There's no automatic type conversion (`false` <-> `0`, `true` <-> `1`).
479
569
  - The set of values without JSON representation (§9.3) is exactly `Function` + `NonFinite`
480
570
 
481
- ### 7.6 Special built-ins: `ENV` and `load`
571
+ ### 7.6 The `@OP` object (the operators)
572
+
573
+ `@OP` is a built-in **object**, reached by member access (`@OP.sum`, `@OP.and`, …),
574
+ holding the arithmetic/comparison/boolean operators. Its members generalise to an
575
+ **array of any length** (`sum`/`product`/`and`/`or` fold; `equal` is deep over all
576
+ elements); `compare` reports an ordering. The infix operators (§2.7) desugar to these members.
577
+
578
+ `@OP` is **shadowable per directory**: place an `OP.fsn` sibling that overrides members
579
+ (spread the originals with `@@`) to reskin the operators — complex numbers, matrices,
580
+ ternary logic — for that directory only. The comparison members (§7.2: `lt`, `gt`,
581
+ `lte`, `gte`) interpret an `@OP.compare` *result* rather than comparing values
582
+ themselves, so an override of `compare` alone already reskins `a < b` (see the
583
+ how-to guide).
584
+
585
+ | Member | Input | Result |
586
+ | ------------- | ---------------------------------------- | -------------------------------------------------------- |
587
+ | `OP.sum` | array of numbers | sum (`0` for `[]`) |
588
+ | `OP.product` | array of numbers | product (`1` for `[]`) |
589
+ | `OP.negate` | number | negation |
590
+ | `OP.invert` | number | reciprocal `1/x`, always a float; `!` if `0` |
591
+ | `OP.quotient` | `[integer, integer]` | integer division; `!` on a non-integer or a `0` divisor |
592
+ | `OP.modulo` | `[integer, integer]` | integer remainder; `!` on a non-integer or a `0` divisor |
593
+ | `OP.equal` | array (any element types) | deep equality: `true` iff every element equals the first |
594
+ | `OP.compare` | `[number, number]` or `[string, string]` | `-1` / `0` / `1` (first smaller / equal / larger) |
595
+ | `OP.lt` | a compare result (`-1`/`0`/`1`/`null`) | `true` for `-1`, else `false`; `null` passes through |
596
+ | `OP.gt` | a compare result | `true` for `1` |
597
+ | `OP.lte` | a compare result | `true` for `-1` / `0` |
598
+ | `OP.gte` | a compare result | `true` for `0` / `1` |
599
+ | `OP.and` | array | `true` if every element is truthy (`true` for `[]`) |
600
+ | `OP.or` | array | `true` if any element is truthy (`false` for `[]`) |
601
+ | `OP.not` | `_` | `true` if the operand is falsey |
602
+
603
+ ### 7.6a The `@math` object (numeric functions and constants)
604
+
605
+ `@math` is a built-in object (shadowable like `@OP`) of numeric functions and two
606
+ constants. `pi`/`e` are plain values; the rest are one-argument functions. A
607
+ non-finite input to `round`/`floor`/`ceil`, a `log` of a non-positive number, and a
608
+ `pow` with a complex result are `math_error`s.
609
+
610
+ | Member | Input | Result |
611
+ | ------------- | -------------------- | ---------------------------------------------------------- |
612
+ | `math.pi` | — | `3.141592653589793` (a value, not a function) |
613
+ | `math.e` | — | `2.718281828459045` (a value) |
614
+ | `math.round` | number | nearest integer (half away from zero) |
615
+ | `math.floor` | number | floor (integer) |
616
+ | `math.ceil` | number | ceiling (integer) |
617
+ | `math.divide` | `[number, number]` | quotient, always a **float**; `!` if divisor is 0 |
618
+ | `math.sign` | number | `-1` / `0` / `1` |
619
+ | `math.abs` | number | absolute value (keeps int/float) |
620
+ | `math.rand` | `null` or a positive integer `n` | float in `[0, 1)`, or integer in `[0, n)` |
621
+ | `math.sin` | number | sine (radians), float |
622
+ | `math.cos` | number | cosine (radians), float |
623
+ | `math.exp` | number | `e^x`, float |
624
+ | `math.log` | positive number | natural log, float; `!` on a non-positive number |
625
+ | `math.pow` | `[base, exponent]` | `base^exp` (integer when base and non-negative integer exponent; else float); `!` on a complex result |
626
+ | `math.sqrt` | non-negative number | square root (float); `!` on a negative number |
627
+
628
+ ### 7.7 Special built-ins: `ENV` and `load`
482
629
 
483
630
  These resolve in the `@name` chain like other built-ins (so a sibling file of the
484
631
  same name shadows them), but they are not plain unary value functions:
@@ -495,12 +642,18 @@ same name shadows them), but they are not plain unary value functions:
495
642
 
496
643
  ## 8. Member and index access
497
644
 
645
+ Member access (`.`), index read (`[]`), and index write (`[=]`) are **core syntax**,
646
+ evaluated directly by the runtime. There are no `@get`/`@set` built-ins.
647
+
498
648
  - `x.key` — if `x` is an object containing `key`, its value; otherwise `!`.
499
- - `x[expr]` — if `x` is an array and `expr` is an integer in range, the element
500
- (negative indices count from the end); if `x` is an object and `expr` is a string
501
- key that exists, its value; otherwise `!`.
649
+ - `x[expr]` — **read**: if `x` is an array and `expr` is an integer in range, the element
650
+ (negative indices count from the end); if `x` is an object and `expr` is a string key
651
+ that exists, its value; otherwise `!`.
652
+ - `x[key = value]` — **write**: a **new** array/object with that one entry set; `x` itself
653
+ is unchanged. An array index must already exist (arrays are not extended; negative
654
+ indices count from the end); an object key may be new. `!` on a bad index/type.
502
655
 
503
- Both `.` and `[]` bind tighter than `|`.
656
+ `.`, `[]`, and `[=]` are postfix and bind tighter than every operator (including `|`).
504
657
 
505
658
  ---
506
659
 
@@ -515,7 +668,19 @@ A `.fsn` file contains **exactly one expression**, which is its value. A file is
515
668
 
516
669
  A `@` reference takes one of these forms:
517
670
 
518
- - **`@`** (nothing after it) — the **current file**'s value. Used for self-recursion.
671
+ - **`@`** (nothing after it) — the value of the **current top-level unit**: the
672
+ current file, or the inline (`-e`) / REPL entry being evaluated. Used for
673
+ self-recursion.
674
+ - **`@@`** (super) — the built-in or standard-library value the current file
675
+ **shadows**: the file's own name resolved by steps 2–3 below, skipping the
676
+ sibling step (which would be the file itself). Lets an override refer to the
677
+ original method, e.g. `add.fsn` containing `@@` refers to the stdlib `add`.
678
+ Outside a file (inline `-e` / REPL) there is no name to take super of, so it is a
679
+ `reference_error` (`no enclosing file`).
680
+ - **`@@name`, `@@dir/name`** — super with an explicit name: resolve `name` by
681
+ steps 2–3 below, skipping its sibling. The **stable** form of a reference — a
682
+ local shadow cannot intercept it (used inside an `OP.fsn` as `@@OP`, and by
683
+ error patterns that must stay canonical). `@@../…` is a syntax error.
519
684
  - **`@ENV`** — an object of all environment variables (string keys, string values;
520
685
  no parsing). Resolved in the `@name` chain below, so it is shadowable.
521
686
  - **`@name`** — a single bare identifier (no `/`, no `../`).
@@ -529,9 +694,9 @@ in order, first match winning:
529
694
  2. a **built-in** of that exact name (including `ENV` and `load`);
530
695
  3. a **standard-library file** at `<stdlib root>/<name>.fsn`.
531
696
 
532
- If none match, the result is `!`. Downward paths participate fully: `@math/sqrt`
533
- checks a sibling `math/sqrt.fsn`, then a built-in named `math/sqrt`, then a stdlib
534
- `math/sqrt.fsn`.
697
+ If none match, the result is `!`. Downward paths participate fully: `@util/helper`
698
+ checks a sibling `util/helper.fsn`, then a built-in named `util/helper`, then a stdlib
699
+ `util/helper.fsn`.
535
700
 
536
701
  **Resolution of upward paths** (any reference containing `../`) is **file-only**: it
537
702
  resolves solely to a file relative to the referencing directory and never falls back
@@ -542,7 +707,20 @@ is relative to the **referencing file's** directory; built-ins and the standard
542
707
  library are global to the runtime but, per the order above, are shadowed by a sibling
543
708
  file of the same name. That shadowing is per-directory, not global.
544
709
 
545
- **Built-ins are reached through this same mechanism**: `@add`, `@Integer`, etc. are
710
+ **Confinement (the jail).** File-backed resolution is confined to a *jail*: a directory
711
+ and its subtree, set by `-j`/`--jail` and defaulting to the program's directory (the
712
+ working directory for `-e` and the REPL). All `@`-references and the builtin `@load`
713
+ respect the jail. Referencing a file outside the jail is a `reference_error`
714
+ (`outside the jail`). An existing sibling outside the jail fails this way too — it does
715
+ *not* fall back to a built-in or the stdlib, so a forbidden file fails loudly rather than
716
+ silently resolving elsewhere. References still resolve relative to the referencing file;
717
+ the jail only filters the result. The standard library is always reachable regardless of
718
+ the jail, and stdin is never affected — it is plain JSON, never an `@`-reference.
719
+ Confinement is lexical (it normalises `..`) and follows existing symlinks. It confines
720
+ references to a directory tree; it is not a security sandbox and needs none, since Fusion
721
+ cannot write files. Pass `--jail '*'` to disable confinement entirely.
722
+
723
+ **Built-ins are reached through this same mechanism**: `@size`, `@Integer`, etc. are
546
724
  `@name` references that resolve at step 2. A *bare* identifier (without `@`) is only
547
725
  a pattern hole; it never denotes a built-in.
548
726
 
@@ -551,9 +729,8 @@ Two built-ins are special in how they resolve:
551
729
  - **`@ENV`** resolves (at step 2) to a fresh object of environment variables.
552
730
  - **`@load`** resolves (at step 2) to a function that loads a file by a **verbatim**
553
731
  filename string — no `.fsn` is appended — relative to the referencing directory,
554
- returning that file's value. If the argument is not a string, or the file does not
555
- exist, the result is an error (`{"kind":"load_bad_arg",...}` or
556
- `{"kind":"file_not_found","path":...}` respectively). This is the only way to load
732
+ returning that file's value. A non-string argument is an `argument_error`; a
733
+ missing file is a `reference_error` (`file not found`). This is the only way to load
557
734
  a file whose name is computed at runtime or is not a plain identifier (e.g.
558
735
  `"data.config.fsn"`).
559
736
 
@@ -572,12 +749,19 @@ above. Recursion through functions is not a data cycle.
572
749
 
573
750
  ### 9.3 Runtime contract
574
751
 
575
- The default use case (**pipe**) reads standard input as JSON, converts it to a
576
- Fusion value `v`, computes `v | programFunction`, and prints the result on
577
- standard output as JSON.
578
-
579
- - Empty input is treated as `null` (in every input mode).
580
- - Non-JSON input yields a `syntax_error` at `location: "input"` (§6.5).
752
+ The **pipe** use case (`--pipe`, and the default whenever any argument is given
753
+ see §9.7) reads standard input as JSON, converts it to a Fusion value `v`,
754
+ computes `v | programFunction`, and prints the result on standard output as JSON.
755
+ When standard input is empty, the program get evaluated and immediately becomes
756
+ the result instead.
757
+
758
+ - Input always arrives on standard input; there is no input argument.
759
+ - **Empty input means "no input": the program's own value is the result.** A
760
+ `.fsn` file therefore doubles as enriched JSON data — it can compute, read
761
+ `@ENV`, and pull in `@`-references, then print the value with no pipeline
762
+ input. (Under `-!` the input is an error value instead; empty input then has no
763
+ payload to mark, which is a usage error — see §9.4.)
764
+ - Non-JSON input yields a `syntax_error` at `origin: "input"` (§6.5).
581
765
  - **If the final result is an error**, the interpreter prints **nothing** to
582
766
  standard output, prints the error's **payload** (as JSON) to standard error, and
583
767
  exits with status `1`. Otherwise the result is printed to standard output and the
@@ -613,20 +797,25 @@ They are independent of each other and selected with the `--input` and `--output
613
797
  flags:
614
798
 
615
799
  - **`unix`** — the input is plain JSON and always a value; the `-!` flag marks
616
- the whole input as an error value instead (its JSON becomes the payload).
617
- Output: a value goes to stdout with exit code `0`; an error's payload goes to
618
- stderr with exit code `1` (§9.3).
800
+ the whole input as an error value instead (its JSON becomes the payload). `-!`
801
+ therefore requires input: with empty input there is no payload to mark, which
802
+ is a usage error (the program does not run). Output: a value goes to stdout
803
+ with exit code `0`; an error's payload goes to stderr with exit code `1`
804
+ (§9.3).
619
805
  - **`bang`** — a leading `!` marks an error value; the payload is the JSON after
620
806
  the `!`. A lone `!` is `!null`, like the language's bare `!`. Output is always
621
807
  on stdout and the exit code is always `0`. A `!`-marked line is not valid JSON;
622
- that is the price of the most lightweight marking.
808
+ that is the price of the most lightweight marking, so `bang` is recommended only
809
+ between Fusion programs — for anything that must stay valid JSON, use `array` or
810
+ `object`.
623
811
  - **`array`** — everything is wrapped in an envelope: `[0, value]` for a value,
624
- `[1, payload]` for an error. Output is always on stdout, exit code always `0`.
812
+ `[1, payload]` for an error. Every line is valid JSON, which is why it is the
813
+ `--stream` default (§9.5). Output is always on stdout, exit code always `0`.
625
814
  - **`object`** — the envelope is `{"value": value}` for a value, `{"error": payload}`
626
815
  for an error. Output is always on stdout, exit code always `0`.
627
816
 
628
817
  A malformed `array`/`object` input envelope (any other shape; the array tag must
629
- be exactly the integer `0` or `1`) is an `argument_error` at `location: "input"`.
818
+ be exactly the integer `0` or `1`) is an `argument_error` at `origin: "input"`.
630
819
  Like any input failure it flows into the program as an error and is catchable.
631
820
 
632
821
  Mode support per use case (defaults in bold):
@@ -634,29 +823,45 @@ Mode support per use case (defaults in bold):
634
823
  | Use case | `unix` | `bang` | `array` | `object` |
635
824
  | ---------- | -------- | -------- | ------- | -------- |
636
825
  | pipe | **yes** | yes | yes | yes |
637
- | `--stream` | no | **yes** | yes | yes |
826
+ | `--stream` | no | yes | **yes** | yes |
638
827
  | `--repl` | — | — | — | — |
639
828
 
640
829
  The unix mode spends the process's only exit code and both standard streams on a
641
830
  single result, so it cannot mark errors per record in a stream; the stream use
642
- case therefore excludes it. The REPL is interactive and has no modes at all.
831
+ case therefore excludes it. Stream defaults to `array` rather than `bang` so each
832
+ record stays valid JSON (NDJSON, §9.5); `bang` remains available as the cheapest
833
+ encoding for Fusion-to-Fusion pipelines. The REPL is interactive and has no modes
834
+ at all.
643
835
 
644
836
  ### 9.5 Streaming (`--stream`)
645
837
 
646
838
  `fusion --stream` loads the program once, then treats standard input and output
647
- as NDJSON streams: each input line is decoded per the input mode, piped through
648
- the program, and printed as one output line encoded per the output mode.
839
+ as [NDJSON](https://github.com/ndjson/ndjson-spec) streams: each input line is
840
+ decoded per the input mode, piped through the program, and printed as one output
841
+ line encoded per the output mode. Input and output default to the **array** mode
842
+ (not `bang`) so every line is valid JSON. The media type is
843
+ `application/x-ndjson` and the file extension for storing such a stream should
844
+ be `.ndjson`.
845
+
846
+ NDJSON conformance:
847
+ - Every output record is a single JSON text in UTF-8, terminated by `\n`, and
848
+ never contains an embedded newline or carriage return.
849
+ - Both `\n` and `\r\n` are accepted as input line delimiters.
850
+ - A blank input line (empty or whitespace-only) carries no record, so the program
851
+ never runs on it. By default it is echoed as a blank output line, keeping input
852
+ and output aligned line-for-line. Pass `--skip-blank-lines` to drop blank lines
853
+ instead. Every non-blank line produces exactly one output line.
649
854
 
650
- - Blank lines are skipped; every other input line produces exactly one output line.
651
855
  - Errors stay in-band, so a failing record — including a stack overflow — becomes
652
856
  that record's output line and the stream continues. The exit code is always `0`.
653
- - A program that fails to load answers every record with that same load error.
857
+ - A program that fails to load will return the same load error for every record.
654
858
 
655
859
  ### 9.6 The REPL (`--repl`)
656
860
 
657
- `fusion --repl` starts an interactive session. It loads no program, takes no
658
- pipeline input, has no input/output mode, and always exits `0`. Each entry is
659
- read, evaluated, and its result printed. An entry is one of:
861
+ `fusion --repl` starts an interactive session as does a bare `fusion` with no
862
+ arguments at all (§9.7). It loads no program, takes no pipeline input, has no
863
+ input/output mode, and always exits `0`. Each entry is read, evaluated, and its
864
+ result printed. An entry is one of:
660
865
 
661
866
  - an **expression** — evaluated and printed; or
662
867
  - a **statement** — an assignment that also binds a name:
@@ -677,43 +882,63 @@ relative to the working directory.
677
882
  statement is an assignment, not a pattern match.)
678
883
  - Rebinding a name is allowed; later entries see the new value.
679
884
  - A bound function can call itself through its own name
680
- (`fact = (0 => 1, n => [n, [n,1] | @subtract | fact] | @multiply)`), because
885
+ (`fact = (0 => 1, n => [n, [n,-1] | @OP.sum | fact] | @OP.product)`), because
681
886
  the name is looked up at application time.
682
- - Entries report errors at `location: "code <inline>"`, like `-e` programs.
887
+ - Entries report errors at `origin: "code"` with `file: "<inline>"`, like `-e` programs.
683
888
 
684
889
  **Input editing.** An entry is submitted only once it parses as a complete
685
- statement or expression; until then whether still unfinished or not yet valid
686
- the session opens a new line so the entry can be finished or corrected. An entry
687
- may therefore span multiple lines (continuation lines show `...> `); on an empty
688
- continuation line, backspace returns to the previous line. The prompt and the
689
- echoed input render on **stderr** (like a shell prompt), so stdout carries only
690
- the stream of results. End the session with Ctrl-D; Ctrl-C discards the entry
691
- being typed.
890
+ statement or expression; until then the session opens a new line so the entry
891
+ can be finished or corrected. An entry may therefore span multiple lines
892
+ (continuation lines show `...> `); on an empty continuation line, backspace
893
+ returns to the previous line. The prompt and the echoed input render on **stderr**
894
+ (like a shell prompt), so stdout carries only the stream of results.
895
+ The prompt is shown in light blue, and each result is preceded **on stderr**
896
+ by a green `✔` (a value) or a red `✗` (an error); these
897
+ are decorations only — the result itself stays unstyled on stdout. End the
898
+ session with Ctrl-D; Ctrl-C discards the entry being typed.
692
899
 
693
900
  ### 9.7 Command-line interface
694
901
 
695
902
  ```
696
- usage: fusion [options] <file.fsn> [json-input]
697
- fusion [options] -e '<source>' [json-input]
903
+ usage: fusion [options] <file.fsn>
904
+ fusion [options] -e '<source>'
698
905
  fusion --repl
699
906
 
700
- use cases:
701
- (default) pipe: apply the program to one input
702
- --stream apply the program to each line of an NDJSON stream
703
- --repl interactive expressions and `identifier = expression`
907
+ use cases (default: --repl with no arguments, otherwise --pipe):
908
+ -p, --pipe apply the program to stdin; with no input, the
909
+ program's own value is the result
910
+ -s, --stream apply the program to each line of an NDJSON stream
911
+ -r, --repl interactive expressions and `identifier = expression`
704
912
 
705
913
  options:
706
- -e '<source>' inline program instead of a file
707
- --input MODE how the input marks an error value (§9.4)
708
- --output MODE how the output marks an error value (§9.4)
914
+ -e, --execute '<source>'
915
+ inline program instead of a file
916
+ -i, --input MODE
917
+ how the input marks an error value (§9.4)
918
+ -o, --output MODE
919
+ how the output marks an error value (§9.4)
920
+ -j, --jail DIR confine @-references to DIR and its subtree
921
+ (default: the program's directory; '*' disables it; §9.2)
709
922
  -! treat the input as an error value (unix input mode only)
923
+ -b, --skip-blank-lines
924
+ drop blank input lines instead of echoing them (--stream, §9.5)
710
925
  ```
711
926
 
712
- In the pipe use case, input comes from the `[json-input]` argument if present,
713
- otherwise from standard input. The stream use case always reads standard input
714
- and accepts no input argument.
927
+ **Selecting a use case.** At most one of `--pipe`, `--stream`, `--repl` may be
928
+ given; passing two is a command-line misuse. With none, a bare `fusion` (no
929
+ arguments at all) starts the REPL, while any other invocation is a pipe run. So
930
+ `--pipe` is needed only to be explicit, `fusion file.fsn` already implicitly
931
+ use `--pipe`.
932
+
933
+ In the pipe use case, input comes from standard input; when standard input is
934
+ empty, the program's own value is the result (§9.3). The stream use case also
935
+ reads standard input. Neither accepts an input argument.
936
+
937
+ Every flag has a short and a long form (`-p`/`--pipe`, `-i`/`--input`, …), except
938
+ `-!`, which has only the short form. Each of `--input`/`--output` may only be used
939
+ once. Multiple different modes for one direction is a misuse.
715
940
 
716
- A command-line misuse (an unknown flag, an unsupported mode combination, a
717
- missing program) is reported as plain usage text on stderr with exit code `1`.
718
- It happens before the input/output contract begins, so it is not a payloaded
719
- error.
941
+ A command-line misuse (an unknown flag, more than one use case, two different
942
+ modes for one direction, an unsupported mode combination, a missing program) is
943
+ reported as plain usage text on stderr with exit code `1`. It happens before the
944
+ input/output contract begins, so it is not a payloaded error.