fusion-lang 0.0.1.alpha2 → 0.0.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -8
  3. data/docs/lang/design.md +240 -51
  4. data/docs/lang/implementation.md +238 -0
  5. data/docs/lang/roadmap.md +20 -36
  6. data/docs/user/explanation.md +5 -10
  7. data/docs/user/how-to-guides.md +60 -15
  8. data/docs/user/reference.md +356 -142
  9. data/docs/user/tutorial.md +21 -19
  10. data/examples/double.fsn +1 -1
  11. data/examples/factorial.fsn +2 -2
  12. data/examples/fizzbuzz.fsn +1 -4
  13. data/examples/json_test.fsn +4 -0
  14. data/examples/palindrome.fsn +1 -1
  15. data/exe/fusion +10 -10
  16. data/lib/fusion/ast.rb +2 -1
  17. data/lib/fusion/cli/decoder.rb +10 -5
  18. data/lib/fusion/cli/options.rb +130 -60
  19. data/lib/fusion/cli/parser.rb +3 -3
  20. data/lib/fusion/cli/repl.rb +30 -25
  21. data/lib/fusion/cli/serializer.rb +5 -4
  22. data/lib/fusion/cli.rb +119 -48
  23. data/lib/fusion/interpreter/builtins.rb +260 -151
  24. data/lib/fusion/interpreter/env.rb +42 -12
  25. data/lib/fusion/interpreter/error_val.rb +42 -20
  26. data/lib/fusion/interpreter/thunk.rb +53 -0
  27. data/lib/fusion/interpreter.rb +239 -82
  28. data/lib/fusion/lexer.rb +69 -3
  29. data/lib/fusion/parser.rb +189 -51
  30. data/lib/fusion/version.rb +1 -1
  31. data/stdlib/all.fsn +13 -0
  32. data/stdlib/any.fsn +12 -0
  33. data/stdlib/chars.fsn +5 -0
  34. data/stdlib/compact.fsn +6 -0
  35. data/stdlib/concat.fsn +5 -0
  36. data/stdlib/falsey.fsn +6 -0
  37. data/stdlib/filter.fsn +12 -0
  38. data/stdlib/flatten.fsn +7 -0
  39. data/stdlib/gt.fsn +9 -0
  40. data/stdlib/gte.fsn +9 -0
  41. data/stdlib/lt.fsn +9 -0
  42. data/stdlib/lte.fsn +9 -0
  43. data/stdlib/map.fsn +6 -4
  44. data/stdlib/range.fsn +2 -2
  45. data/stdlib/reduce.fsn +8 -0
  46. data/stdlib/sanitize.fsn +2 -2
  47. data/stdlib/truthy.fsn +7 -0
  48. metadata +18 -4
  49. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  50. data/stdlib/mapValues.fsn +0 -5
  51. 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: `??`. 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,57 @@ 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 == c → [a, b, c] | @OP.equal
198
+ a && b && c → [a, b, c] | @OP.and
199
+ a || b || c → [a, b, c] | @OP.or
200
+ xs |: f → {"c": xs, "f": f} | @map
201
+ xs |? f → {"c": xs, "f": f} | @filter
202
+ xs |+ f → {"c": xs, "f": f} | @reduce
203
+ ```
204
+
205
+ Folding and associativity:
206
+
207
+ - A maximal run of `+`/`-` folds into one `@OP.sum` over all terms; each `-` term is
208
+ negated (a numeric literal folds to a negative literal, otherwise via `@OP.negate`).
209
+ - A maximal run of `*`/`/` folds into one `@OP.product`; each `/` term is inverted via
210
+ `@OP.invert` (never a literal, so `1/x` stays a float and `/0` stays a runtime error).
211
+ - Runs of `==`, `&&`, `||` fold n-ary into `@OP.equal` / `@OP.and` / `@OP.or`.
212
+ - `%`, `//`, `??` are binary and left-associative; they sit at their level and break a
213
+ fold run: `a * b % c` is `(a * b) % c`; `a ?? b == 0` is `(a ?? b) == 0`.
214
+
215
+ Because pipe binds tighter than the value operators, `x|@f + 1` is `(x|@f) + 1`; to pipe a
216
+ computed value onward, parenthesize it: `(a + b)|@f`, `(a ?? b)|@gt`.
217
+
218
+ Comparisons: `a == b` is equality. `a < b` needs to be expressed as `(a ?? b) | @lt`
219
+ (likewise `@gt` / `@lte` / `@gte`), since `??` yields the `-1`/`0`/`1` ordering those
220
+ stdlib helpers interpret.
221
+
157
222
  ---
158
223
 
159
224
  ## 3. Functions and application
@@ -231,7 +296,7 @@ Rules:
231
296
  `p_core ? predicate` matches when `p_core` matches structurally **and** piping the
232
297
  matched value through `predicate` yields a **truthy** result. Truthiness is
233
298
  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.
299
+ truthy). The operators `@OP.and` / `@OP.or` / `@OP.not` apply the same test.
235
300
 
236
301
  The predicate is a `|` chain of functions, and the matched value flows in from the
237
302
  left: `a ? b | c` matches when `a` matches and `a | b | c` is truthy. A single-stage
@@ -313,7 +378,7 @@ particular built-ins:
313
378
  is per-call: it is not enough for the function to have *some* error clause;
314
379
  that clause must match the specific error received. An error of a shape no
315
380
  clause catches propagates unchanged.
316
- - **Built-in operations (`@add`, `@divide`, `@equals`, `@Integer`, …) all
381
+ - **Built-in and stdlib operations (`@math.divide`, `@OP.sum`, `@Integer`, …) all
317
382
  propagate** their input error without examining it. To inspect or compare an
318
383
  error's payload, you must catch it first and operate on the extracted payload:
319
384
  `!42 | (!a => a) | @Integer` returns `true` (the payload `42` *is* an integer);
@@ -323,7 +388,7 @@ particular built-ins:
323
388
  evaluating an element/member. `[1, !"bad", 2]` evaluates to `!"bad"`, not to
324
389
  an array of three things.
325
390
  - **Constructing an error from an erroring expression** propagates the inner
326
- error rather than wrapping it. `!([5,0] | @divide)` evaluates to the division
391
+ error rather than wrapping it. `!([5,0] | @math.divide)` evaluates to the division
327
392
  error itself (a `math_error`, §6.5), never to an error wrapping an error. (This
328
393
  preserves the rule that there is never more than one error simultaneously.)
329
394
  - **When the function value itself is an error** (e.g. `value | @undefined_name`
@@ -334,9 +399,10 @@ particular built-ins:
334
399
  If a `?` predicate evaluates to an error (the predicate function itself errored,
335
400
  or it was a non-function error value), that error becomes the function's return
336
401
  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.
402
+ treated as program failures, not as "no match." This is the key reason to keep a
403
+ `?` predicate from raising: a predicate that can crash will short-circuit your
404
+ whole function. A non-matching input is not a crash — it falls through to `null`
405
+ (falsey) — so a predicate needs no `_ => false` catch-all to be safe here.
340
406
 
341
407
  ### 6.5 The standardized error payload
342
408
 
@@ -354,46 +420,48 @@ There are two origins of error values, and they differ in payload:
354
420
  #### Payload shape
355
421
 
356
422
  ```json
357
- {"kind": "type_error", "location": "builtin add", "operation": "add", "input": [1, "x"], "message": "expected numbers"}
423
+ {"kind": "argument_error", "origin": "builtin", "operation": "@math.divide", "status": 0, "input": [1, "x"], "expected": ["[_ ? @Number, _ ? @Number]"]}
358
424
  ```
359
425
 
360
426
  | Field | Required | Meaning |
361
427
  | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
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"`. |
428
+ | `kind` | yes | The error category. Possible values are defined below. |
429
+ | `origin` | yes | Where the failing operation is *defined*. Possible values are defined below. |
430
+ | `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`). |
431
+ | `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"`. |
432
+ | `status` | yes | `0` or `1`. Whether the operation received an ordinary value (`0`) or an error value (`1`) |
433
+ | `input` | yes | The operation's input. A 0-argument operation (every `@`-reference except `@load`) carries `null`. |
434
+ | `expected` | no | The acceptable inputs as a list of Fusion **patterns**. The input matched none of them. |
435
+ | `message` | no | Extra human-readable detail, e.g. `"division by zero"`. Absent whenever `expected` is present. |
367
436
 
368
- #### `kind` — the closed set
437
+ #### Possible values for `kind`
369
438
 
370
439
  | `kind` | Raised when |
371
440
  | --------------------- | -------------------------------------------------------------------------------------------------------------- |
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 [_, _]`). |
441
+ | `syntax_error` | source code or the JSON input fails to parse. |
442
+ | `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). |
443
+ | `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
444
  | `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`). |
445
+ | `access_error` | a missing object key or an out-of-range array index. |
378
446
  | `math_error` | division or modulo by zero, or a non-finite number. |
379
447
  | `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. |
448
+ | `limit_error` | a runtime resource limit was exceeded. `"stack level too deep"`. |
449
+ | `internal_error` | an interpreter BUG. Please open an issue. |
450
+ | `serialization_error` | a result/error value has no JSON form. It contains functions or non-finite numbers. See §9. |
382
451
 
383
- #### `location` the closed set
452
+ #### Possible values for `origin`
384
453
 
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. |
454
+ | `origin` | Meaning |
455
+ | ------------- | ------------------------------------------------------------------------ |
456
+ | `builtin` | a built-in operation (named by its `@`-reference in `operation`). |
457
+ | `stdlib` | a standard-library function (named by its `@`-reference in `operation`). |
458
+ | `code` | user source core (a file or an inline expression (`-e`/REPL). |
459
+ | `input` | the input channel (stdin). Usually syntax errors. |
460
+ | `output` | the output channel. Usually serialization errors. |
461
+ | `interpreter` | the interpreter itself, e.g. a stack overflow. |
394
462
 
395
463
  `input` and `output` name the data channels; they **never** refer to the program
396
- source, which always reports as `code X`.
464
+ source, which always reports as `code`.
397
465
 
398
466
  User errors don't have to adhere to this standard.
399
467
 
@@ -402,60 +470,75 @@ User errors don't have to adhere to this standard.
402
470
  ## 7. Built-in functions
403
471
 
404
472
  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
473
+ (`@size`, `@Integer`, …); see §9.2 for how `@name` resolves. The names in the tables
406
474
  below are the built-in names; write `@` before them to use them. **Operations** return
407
475
  `!` on type-invalid or domain-invalid input. **Predicates** return `false` on any
408
476
  input that is not of the queried type (they never return `!`).
409
477
 
410
- ### 7.1 Arithmetic (operations)
411
-
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) |
421
-
422
- ### 7.2 Comparison (operations)
423
-
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 |
428
-
429
- Other comparisons (`lessEq`, `greaterThan`, `greaterEq`, `notEquals`) are specified
430
- for the standard library, derivable from `equals` and `lessThan`.
431
-
432
- ### 7.3 Boolean (operations)
433
-
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.
436
-
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`) |
478
+ The operators (`+ - * / == < …` and the boolean ops) live in the shadowable `@OP`
479
+ object (§7.6); a directory reskins them by placing an `OP.fsn`. Their **named** forms
480
+ below are stdlib functions built on `@OP.*`, so they follow a per-directory override.
481
+
482
+ ### 7.1 Arithmetic
483
+
484
+ In source you normally write the infix sugar of §2.7 — `a + b`, `-a`, `a * b`, `a % b`,
485
+ `a // b` which desugars to the members below. Addition, multiplication and negation are
486
+ `@OP.sum` / `@OP.product` / `@OP.negate` (§7.6); subtraction is `[a, b | @OP.negate] | @OP.sum`.
487
+ Integer division/remainder are `@OP.quotient` / `@OP.modulo`. Numerically correct
488
+ division and the other numeric functions live in `@math` (§7.6a): `@math.divide`,
489
+ `@math.floor`, `@math.round`, `@math.abs`, `@math.log`, `@math.sqrt`, etc.
490
+
491
+ ### 7.2 Comparison
492
+
493
+ Equality is `@OP.equal` (deep structural equality of a pair; §7.6) — used directly,
494
+ there is no `@eq` helper. Ordering is `@OP.compare`, which returns `-1`/`0`/`1`. The
495
+ named boolean forms **interpret that result** and are applied *after* it:
496
+
497
+ | Name | `-1` | `0` | `1` | `null` |
498
+ | ----- | ------ | ------ | ------ | ------ |
499
+ | `lt` | `true` | `false`| `false`| `null` |
500
+ | `gt` | `false`| `false`| `true` | `null` |
501
+ | `lte` | `true` | `true` | `false`| `null` |
502
+ | `gte` | `false`| `true` | `true` | `null` |
503
+
504
+ So `a < b` is written `[a, b] | @OP.compare | @lt` (with sugar: `(a ?? b) | @lt`; equality
505
+ is `a == b` — see §2.7). Because the caller invokes
506
+ `@OP.compare`, the ordering follows a per-directory `@OP` override while the helper
507
+ itself is fixed and shadow-independent. A partial order whose `compare` returns `null`
508
+ for incomparable operands passes that `null` straight through; any other input is an
509
+ `argument_error`. A type mismatch surfaces as `@OP.compare`'s own error, before the
510
+ helper runs.
511
+
512
+ ### 7.3 Boolean
513
+
514
+ The truthiness operators live in `@OP` (§7.6): `@OP.and`, `@OP.or`, `@OP.not`, written
515
+ with the sugar `a && b`, `a || b`, `~a` (§2.7). They
516
+ judge truthiness (every value is truthy except `false` and `null`), not strict
517
+ booleans, and always return a boolean. There are no top-level `@and`/`@or`/`@not`.
518
+ The stdlib helpers `@truthy` and `@falsey` reduce any single value to its truthiness
519
+ by **pattern matching** (independent of any `@OP` override): `@truthy` is `true` for
520
+ everything except `false`/`null`, and `@falsey` is its complement.
442
521
 
443
522
  ### 7.4 Strings and structure bridges (operations)
444
523
 
445
524
  | Name | Input | Result |
446
525
  | ------------- | --------------------------- | --------------------------------------- |
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 |
526
+ | `size` | string / array / object | element/character/key count (integer) |
527
+ | `join` | `[array-of-strings, separator]` | the elements joined by the separator string |
528
+ | `split` | `[string, separator]` | array split on the **literal** separator (an empty separator splits into characters), keeping empty fields |
451
529
  | `toString` | any | string form of the value |
452
530
  | `parseNumber` | string | integer or float; `!` if not numeric |
453
531
  | `keys` | object | array of key strings |
454
532
  | `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
533
  | `toObject` | `[[string-key, value], …]` | object built from entries; later duplicate keys win |
458
534
 
535
+ `concat` (`[string, string]` → concatenation) and `chars` (string → array of its
536
+ characters) are **standard-library** functions built on `join` / `split`, not
537
+ built-ins.
538
+
539
+ Indexed read (`x[k]`) and write (`x[k = v]`) are **core syntax**, not built-ins — there
540
+ is no `@get`/`@set`; see §8.
541
+
459
542
  ### 7.5 Type predicates (predicates)
460
543
 
461
544
  Each of these functions takes any input value and returns a boolean. This set of
@@ -478,7 +561,59 @@ Notes:
478
561
  - Booleans are separate from numbers. There's no automatic type conversion (`false` <-> `0`, `true` <-> `1`).
479
562
  - The set of values without JSON representation (§9.3) is exactly `Function` + `NonFinite`
480
563
 
481
- ### 7.6 Special built-ins: `ENV` and `load`
564
+ ### 7.6 The `@OP` object (the operators)
565
+
566
+ `@OP` is a built-in **object**, reached by member access (`@OP.sum`, `@OP.and`, …),
567
+ holding the arithmetic/comparison/boolean operators. Its members generalise to an
568
+ **array of any length** (`sum`/`product`/`and`/`or` fold; `equal` is deep over all
569
+ elements); `compare` reports an ordering. The infix operators (§2.7) desugar to these members.
570
+
571
+ `@OP` is **shadowable per directory**: place an `OP.fsn` sibling that overrides members
572
+ (spread the originals with `@@`) to reskin the operators — complex numbers, matrices,
573
+ ternary logic — for that directory only. The comparison helpers in §7.2 (`@lt`, `@gt`, …)
574
+ interpret an `@OP.compare` result, so ordering follows a local override the moment the
575
+ caller pipes through `@OP.compare` (see the how-to guide).
576
+
577
+ | Member | Input | Result |
578
+ | ------------- | ---------------------------------------- | -------------------------------------------------------- |
579
+ | `OP.sum` | array of numbers | sum (`0` for `[]`) |
580
+ | `OP.product` | array of numbers | product (`1` for `[]`) |
581
+ | `OP.negate` | number | negation |
582
+ | `OP.invert` | number | reciprocal `1/x`, always a float; `!` if `0` |
583
+ | `OP.quotient` | `[integer, integer]` | integer division; `!` on a non-integer or a `0` divisor |
584
+ | `OP.modulo` | `[integer, integer]` | integer remainder; `!` on a non-integer or a `0` divisor |
585
+ | `OP.equal` | array (any element types) | deep equality: `true` iff every element equals the first |
586
+ | `OP.compare` | `[number, number]` or `[string, string]` | `-1` / `0` / `1` (first smaller / equal / larger) |
587
+ | `OP.and` | array | `true` if every element is truthy (`true` for `[]`) |
588
+ | `OP.or` | array | `true` if any element is truthy (`false` for `[]`) |
589
+ | `OP.not` | `_` | `true` if the operand is falsey |
590
+
591
+ ### 7.6a The `@math` object (numeric functions and constants)
592
+
593
+ `@math` is a built-in object (shadowable like `@OP`) of numeric functions and two
594
+ constants. `pi`/`e` are plain values; the rest are one-argument functions. A
595
+ non-finite input to `round`/`floor`/`ceil`, a `log` of a non-positive number, and a
596
+ `pow` with a complex result are `math_error`s.
597
+
598
+ | Member | Input | Result |
599
+ | ------------- | -------------------- | ---------------------------------------------------------- |
600
+ | `math.pi` | — | `3.141592653589793` (a value, not a function) |
601
+ | `math.e` | — | `2.718281828459045` (a value) |
602
+ | `math.round` | number | nearest integer (half away from zero) |
603
+ | `math.floor` | number | floor (integer) |
604
+ | `math.ceil` | number | ceiling (integer) |
605
+ | `math.divide` | `[number, number]` | quotient, always a **float**; `!` if divisor is 0 |
606
+ | `math.sign` | number | `-1` / `0` / `1` |
607
+ | `math.abs` | number | absolute value (keeps int/float) |
608
+ | `math.rand` | `null` or a positive integer `n` | float in `[0, 1)`, or integer in `[0, n)` |
609
+ | `math.sin` | number | sine (radians), float |
610
+ | `math.cos` | number | cosine (radians), float |
611
+ | `math.exp` | number | `e^x`, float |
612
+ | `math.log` | positive number | natural log, float; `!` on a non-positive number |
613
+ | `math.pow` | `[base, exponent]` | `base^exp` (integer when base and non-negative integer exponent; else float); `!` on a complex result |
614
+ | `math.sqrt` | non-negative number | square root (float); `!` on a negative number |
615
+
616
+ ### 7.7 Special built-ins: `ENV` and `load`
482
617
 
483
618
  These resolve in the `@name` chain like other built-ins (so a sibling file of the
484
619
  same name shadows them), but they are not plain unary value functions:
@@ -495,12 +630,18 @@ same name shadows them), but they are not plain unary value functions:
495
630
 
496
631
  ## 8. Member and index access
497
632
 
633
+ Member access (`.`), index read (`[]`), and index write (`[=]`) are **core syntax**,
634
+ evaluated directly by the runtime. There are no `@get`/`@set` built-ins.
635
+
498
636
  - `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 `!`.
637
+ - `x[expr]` — **read**: if `x` is an array and `expr` is an integer in range, the element
638
+ (negative indices count from the end); if `x` is an object and `expr` is a string key
639
+ that exists, its value; otherwise `!`.
640
+ - `x[key = value]` — **write**: a **new** array/object with that one entry set; `x` itself
641
+ is unchanged. An array index must already exist (arrays are not extended; negative
642
+ indices count from the end); an object key may be new. `!` on a bad index/type.
502
643
 
503
- Both `.` and `[]` bind tighter than `|`.
644
+ `.`, `[]`, and `[=]` are postfix and bind tighter than every operator (including `|`).
504
645
 
505
646
  ---
506
647
 
@@ -515,7 +656,19 @@ A `.fsn` file contains **exactly one expression**, which is its value. A file is
515
656
 
516
657
  A `@` reference takes one of these forms:
517
658
 
518
- - **`@`** (nothing after it) — the **current file**'s value. Used for self-recursion.
659
+ - **`@`** (nothing after it) — the value of the **current top-level unit**: the
660
+ current file, or the inline (`-e`) / REPL entry being evaluated. Used for
661
+ self-recursion.
662
+ - **`@@`** (super) — the built-in or standard-library value the current file
663
+ **shadows**: the file's own name resolved by steps 2–3 below, skipping the
664
+ sibling step (which would be the file itself). Lets an override refer to the
665
+ original method, e.g. `add.fsn` containing `@@` refers to the stdlib `add`.
666
+ Outside a file (inline `-e` / REPL) there is no name to take super of, so it is a
667
+ `reference_error` (`no enclosing file`).
668
+ - **`@@name`, `@@dir/name`** — super with an explicit name: resolve `name` by
669
+ steps 2–3 below, skipping its sibling. The **stable** form of a reference — a
670
+ local shadow cannot intercept it (used inside an `OP.fsn` as `@@OP`, and by
671
+ error patterns that must stay canonical). `@@../…` is a syntax error.
519
672
  - **`@ENV`** — an object of all environment variables (string keys, string values;
520
673
  no parsing). Resolved in the `@name` chain below, so it is shadowable.
521
674
  - **`@name`** — a single bare identifier (no `/`, no `../`).
@@ -529,9 +682,9 @@ in order, first match winning:
529
682
  2. a **built-in** of that exact name (including `ENV` and `load`);
530
683
  3. a **standard-library file** at `<stdlib root>/<name>.fsn`.
531
684
 
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`.
685
+ If none match, the result is `!`. Downward paths participate fully: `@util/helper`
686
+ checks a sibling `util/helper.fsn`, then a built-in named `util/helper`, then a stdlib
687
+ `util/helper.fsn`.
535
688
 
536
689
  **Resolution of upward paths** (any reference containing `../`) is **file-only**: it
537
690
  resolves solely to a file relative to the referencing directory and never falls back
@@ -542,7 +695,20 @@ is relative to the **referencing file's** directory; built-ins and the standard
542
695
  library are global to the runtime but, per the order above, are shadowed by a sibling
543
696
  file of the same name. That shadowing is per-directory, not global.
544
697
 
545
- **Built-ins are reached through this same mechanism**: `@add`, `@Integer`, etc. are
698
+ **Confinement (the jail).** File-backed resolution is confined to a *jail*: a directory
699
+ and its subtree, set by `-j`/`--jail` and defaulting to the program's directory (the
700
+ working directory for `-e` and the REPL). All `@`-references and the builtin `@load`
701
+ respect the jail. Referencing a file outside the jail is a `reference_error`
702
+ (`outside the jail`). An existing sibling outside the jail fails this way too — it does
703
+ *not* fall back to a built-in or the stdlib, so a forbidden file fails loudly rather than
704
+ silently resolving elsewhere. References still resolve relative to the referencing file;
705
+ the jail only filters the result. The standard library is always reachable regardless of
706
+ the jail, and stdin is never affected — it is plain JSON, never an `@`-reference.
707
+ Confinement is lexical (it normalises `..`) and follows existing symlinks. It confines
708
+ references to a directory tree; it is not a security sandbox and needs none, since Fusion
709
+ cannot write files. Pass `--jail '*'` to disable confinement entirely.
710
+
711
+ **Built-ins are reached through this same mechanism**: `@size`, `@Integer`, etc. are
546
712
  `@name` references that resolve at step 2. A *bare* identifier (without `@`) is only
547
713
  a pattern hole; it never denotes a built-in.
548
714
 
@@ -572,12 +738,19 @@ above. Recursion through functions is not a data cycle.
572
738
 
573
739
  ### 9.3 Runtime contract
574
740
 
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).
741
+ The **pipe** use case (`--pipe`, and the default whenever any argument is given
742
+ see §9.7) reads standard input as JSON, converts it to a Fusion value `v`,
743
+ computes `v | programFunction`, and prints the result on standard output as JSON.
744
+ When standard input is empty, the program get evaluated and immediately becomes
745
+ the result instead.
746
+
747
+ - Input always arrives on standard input; there is no input argument.
748
+ - **Empty input means "no input": the program's own value is the result.** A
749
+ `.fsn` file therefore doubles as enriched JSON data — it can compute, read
750
+ `@ENV`, and pull in `@`-references, then print the value with no pipeline
751
+ input. (Under `-!` the input is an error value instead; empty input then has no
752
+ payload to mark, which is a usage error — see §9.4.)
753
+ - Non-JSON input yields a `syntax_error` at `origin: "input"` (§6.5).
581
754
  - **If the final result is an error**, the interpreter prints **nothing** to
582
755
  standard output, prints the error's **payload** (as JSON) to standard error, and
583
756
  exits with status `1`. Otherwise the result is printed to standard output and the
@@ -613,20 +786,25 @@ They are independent of each other and selected with the `--input` and `--output
613
786
  flags:
614
787
 
615
788
  - **`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).
789
+ the whole input as an error value instead (its JSON becomes the payload). `-!`
790
+ therefore requires input: with empty input there is no payload to mark, which
791
+ is a usage error (the program does not run). Output: a value goes to stdout
792
+ with exit code `0`; an error's payload goes to stderr with exit code `1`
793
+ (§9.3).
619
794
  - **`bang`** — a leading `!` marks an error value; the payload is the JSON after
620
795
  the `!`. A lone `!` is `!null`, like the language's bare `!`. Output is always
621
796
  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.
797
+ that is the price of the most lightweight marking, so `bang` is recommended only
798
+ between Fusion programs — for anything that must stay valid JSON, use `array` or
799
+ `object`.
623
800
  - **`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`.
801
+ `[1, payload]` for an error. Every line is valid JSON, which is why it is the
802
+ `--stream` default (§9.5). Output is always on stdout, exit code always `0`.
625
803
  - **`object`** — the envelope is `{"value": value}` for a value, `{"error": payload}`
626
804
  for an error. Output is always on stdout, exit code always `0`.
627
805
 
628
806
  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"`.
807
+ be exactly the integer `0` or `1`) is an `argument_error` at `origin: "input"`.
630
808
  Like any input failure it flows into the program as an error and is catchable.
631
809
 
632
810
  Mode support per use case (defaults in bold):
@@ -634,29 +812,45 @@ Mode support per use case (defaults in bold):
634
812
  | Use case | `unix` | `bang` | `array` | `object` |
635
813
  | ---------- | -------- | -------- | ------- | -------- |
636
814
  | pipe | **yes** | yes | yes | yes |
637
- | `--stream` | no | **yes** | yes | yes |
815
+ | `--stream` | no | yes | **yes** | yes |
638
816
  | `--repl` | — | — | — | — |
639
817
 
640
818
  The unix mode spends the process's only exit code and both standard streams on a
641
819
  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.
820
+ case therefore excludes it. Stream defaults to `array` rather than `bang` so each
821
+ record stays valid JSON (NDJSON, §9.5); `bang` remains available as the cheapest
822
+ encoding for Fusion-to-Fusion pipelines. The REPL is interactive and has no modes
823
+ at all.
643
824
 
644
825
  ### 9.5 Streaming (`--stream`)
645
826
 
646
827
  `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.
828
+ as [NDJSON](https://github.com/ndjson/ndjson-spec) streams: each input line is
829
+ decoded per the input mode, piped through the program, and printed as one output
830
+ line encoded per the output mode. Input and output default to the **array** mode
831
+ (not `bang`) so every line is valid JSON. The media type is
832
+ `application/x-ndjson` and the file extension for storing such a stream should
833
+ be `.ndjson`.
834
+
835
+ NDJSON conformance:
836
+ - Every output record is a single JSON text in UTF-8, terminated by `\n`, and
837
+ never contains an embedded newline or carriage return.
838
+ - Both `\n` and `\r\n` are accepted as input line delimiters.
839
+ - A blank input line (empty or whitespace-only) carries no record, so the program
840
+ never runs on it. By default it is echoed as a blank output line, keeping input
841
+ and output aligned line-for-line. Pass `--skip-blank-lines` to drop blank lines
842
+ instead. Every non-blank line produces exactly one output line.
649
843
 
650
- - Blank lines are skipped; every other input line produces exactly one output line.
651
844
  - Errors stay in-band, so a failing record — including a stack overflow — becomes
652
845
  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.
846
+ - A program that fails to load will return the same load error for every record.
654
847
 
655
848
  ### 9.6 The REPL (`--repl`)
656
849
 
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:
850
+ `fusion --repl` starts an interactive session as does a bare `fusion` with no
851
+ arguments at all (§9.7). It loads no program, takes no pipeline input, has no
852
+ input/output mode, and always exits `0`. Each entry is read, evaluated, and its
853
+ result printed. An entry is one of:
660
854
 
661
855
  - an **expression** — evaluated and printed; or
662
856
  - a **statement** — an assignment that also binds a name:
@@ -677,43 +871,63 @@ relative to the working directory.
677
871
  statement is an assignment, not a pattern match.)
678
872
  - Rebinding a name is allowed; later entries see the new value.
679
873
  - A bound function can call itself through its own name
680
- (`fact = (0 => 1, n => [n, [n,1] | @subtract | fact] | @multiply)`), because
874
+ (`fact = (0 => 1, n => [n, [n,-1] | @OP.sum | fact] | @OP.product)`), because
681
875
  the name is looked up at application time.
682
- - Entries report errors at `location: "code <inline>"`, like `-e` programs.
876
+ - Entries report errors at `origin: "code"` with `file: "<inline>"`, like `-e` programs.
683
877
 
684
878
  **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.
879
+ statement or expression; until then the session opens a new line so the entry
880
+ can be finished or corrected. An entry may therefore span multiple lines
881
+ (continuation lines show `...> `); on an empty continuation line, backspace
882
+ returns to the previous line. The prompt and the echoed input render on **stderr**
883
+ (like a shell prompt), so stdout carries only the stream of results.
884
+ The prompt is shown in light blue, and each result is preceded **on stderr**
885
+ by a green `✔` (a value) or a red `✗` (an error); these
886
+ are decorations only — the result itself stays unstyled on stdout. End the
887
+ session with Ctrl-D; Ctrl-C discards the entry being typed.
692
888
 
693
889
  ### 9.7 Command-line interface
694
890
 
695
891
  ```
696
- usage: fusion [options] <file.fsn> [json-input]
697
- fusion [options] -e '<source>' [json-input]
892
+ usage: fusion [options] <file.fsn>
893
+ fusion [options] -e '<source>'
698
894
  fusion --repl
699
895
 
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`
896
+ use cases (default: --repl with no arguments, otherwise --pipe):
897
+ -p, --pipe apply the program to stdin; with no input, the
898
+ program's own value is the result
899
+ -s, --stream apply the program to each line of an NDJSON stream
900
+ -r, --repl interactive expressions and `identifier = expression`
704
901
 
705
902
  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)
903
+ -e, --execute '<source>'
904
+ inline program instead of a file
905
+ -i, --input MODE
906
+ how the input marks an error value (§9.4)
907
+ -o, --output MODE
908
+ how the output marks an error value (§9.4)
909
+ -j, --jail DIR confine @-references to DIR and its subtree
910
+ (default: the program's directory; '*' disables it; §9.2)
709
911
  -! treat the input as an error value (unix input mode only)
912
+ -b, --skip-blank-lines
913
+ drop blank input lines instead of echoing them (--stream, §9.5)
710
914
  ```
711
915
 
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.
916
+ **Selecting a use case.** At most one of `--pipe`, `--stream`, `--repl` may be
917
+ given; passing two is a command-line misuse. With none, a bare `fusion` (no
918
+ arguments at all) starts the REPL, while any other invocation is a pipe run. So
919
+ `--pipe` is needed only to be explicit, `fusion file.fsn` already implicitly
920
+ use `--pipe`.
921
+
922
+ In the pipe use case, input comes from standard input; when standard input is
923
+ empty, the program's own value is the result (§9.3). The stream use case also
924
+ reads standard input. Neither accepts an input argument.
925
+
926
+ Every flag has a short and a long form (`-p`/`--pipe`, `-i`/`--input`, …), except
927
+ `-!`, which has only the short form. Each of `--input`/`--output` may only be used
928
+ once. Multiple different modes for one direction is a misuse.
715
929
 
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.
930
+ A command-line misuse (an unknown flag, more than one use case, two different
931
+ modes for one direction, an unsupported mode combination, a missing program) is
932
+ reported as plain usage text on stderr with exit code `1`. It happens before the
933
+ input/output contract begins, so it is not a payloaded error.