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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4300463df5e82850cc75d27151c01a6b83e3cd66b90a5c820d40f27d54787250
4
- data.tar.gz: 044f7a4883952b56aa4336067c994ab78a7dd84e8116f68501c229414c91512e
3
+ metadata.gz: 6571414d6f0a1f977f517652742c7b2766d7a17577f0276163856ff992bc7bc0
4
+ data.tar.gz: 1f4f3a71cbfe0a18e18e91276b0ade75ea5c49a5b9a531097b739e8f04929b24
5
5
  SHA512:
6
- metadata.gz: 0d0cd98c87c04c9cc88694ad3d4e926103334fd03d5bfadcf147050c41f63bdb17b4b1bfaad9a5cd9bc456d5ea3ec17ded24db02e4d83d4035d1f403d4d669d6
7
- data.tar.gz: 38d2637d43d8cbc5c17423a329e64605cde2cb2d154207174ec482611df43b1bc1f5d8435bd1bda751528bdc4a7d029b751b0e5f3fa11d10731afe82044b745a
6
+ metadata.gz: e4f9a32c372aea8633c1e66ab883015674d052ca83cccfab10c693f65ac805d90b4d768e1f78a4fb1a77745c158e6a39d56fa25ff9cae16cc29c765c22480ca7
7
+ data.tar.gz: 7feb41d65c7be52ee4a92c820edd714b8ef28b6e0c5e9a6726d1a7c41963e9f3ff4d36b7cb89afacba1256cdc675c8a4a0c5c34d6ca5865e8106f4efe70fc4c7
data/README.md CHANGED
@@ -55,16 +55,17 @@ gem "fusion-lang", require: "fusion"
55
55
  ## How to run your code
56
56
 
57
57
  ```sh
58
- echo '5' | fusion examples/factorial.fsn # => 120
59
- echo '15' | fusion examples/fizzbuzz.fsn # => "FizzBuzz"
60
- fusion examples/factorial.fsn 5 # => 120 (input as an argument)
61
- fusion -e '(n => [n,2] | @multiply)' 21 # => 42 (inline program)
62
- printf '[1, 2]\n[3, 4]\n' | fusion --stream examples/double.fsn # => [2,4] [6,8] (NDJSON, one value per line)
63
- fusion --repl # interactive expressions and `name = expression`
58
+ echo '5' | fusion examples/factorial.fsn # => 120
59
+ echo '15' | fusion examples/fizzbuzz.fsn # => "FizzBuzz"
60
+ echo '21' | fusion -e '(n => [n,2] | @OP.product)' # => 42 (inline program)
61
+ fusion -e '[1, [2, 3] | @OP.sum]' # => [1,5] (no input: the program's value is the result)
62
+ printf '[0,3]\n[0,4]\n' | fusion --stream examples/double.fsn # => [0,6] [0,8] (NDJSON, array mode)
63
+ fusion --repl # interactive REPL (also started by a bare `fusion`)
64
64
  ```
65
65
 
66
- - Input is read from stdin (or the 2nd CLI arg) as JSON and parsed into a Fusion value.
67
- - The file's function gets applied to this value: `value | function`
66
+ - Input is read from stdin as JSON and parsed into a Fusion value.
67
+ - The file's function gets applied to this value: `value | function`.
68
+ - With no input, the file's own value is the result β€” so a `.fsn` file doubles as enriched JSON data.
68
69
  - The result gets printed as JSON to stdout.
69
70
  - Errors get printed to stderr instead and set exit code `1`.
70
71
  - How errors cross the boundary is configurable per side (`--input` / `--output`);
data/docs/lang/design.md CHANGED
@@ -79,9 +79,9 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
79
79
  ### Decisions
80
80
 
81
81
  - πŸ§‘ βœ… Integers and floats are distinct kinds.
82
- - πŸ€– βœ… `divide` returns an integer when evenly divisible and a float otherwise.
83
- - πŸ€– βœ… `floor` returns an integer.
84
- - πŸ€– βœ… `equals` is exact.
82
+ - πŸ€– βͺ `@OP.divide` returned an integer when evenly divisible, a float otherwise. Reverted in Β§5.5. It is now always a float, with integer division split out to `@OP.quotient` (Β§5.5).
83
+ - πŸ€– βœ… `@math.floor` returns an integer.
84
+ - πŸ€– βœ… `@OP.equal` is exact.
85
85
 
86
86
  ### Alternatives
87
87
 
@@ -101,11 +101,11 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
101
101
 
102
102
  ---
103
103
 
104
- ## 1.4 Member/index access failures yield `!`
104
+ ## 1.4 Member/index access failures yield an error
105
105
 
106
106
  ### Decisions
107
107
 
108
- - πŸ€– βœ… `x.key` on a missing key or non-object, and `x[i]` out of range or on a wrong type, yield `!` (not `null`).
108
+ - πŸ€– βœ… `x.key` on a missing key or non-object, and `x[i]` out of range or on a wrong type, yield an error (instead of `null`).
109
109
 
110
110
  ### Why the implementer decided this
111
111
 
@@ -162,7 +162,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
162
162
 
163
163
  ### Decisions
164
164
 
165
- - πŸ§‘ βœ… Patterns and results are mirror images using the same names β€” values captured by a pattern are re-inserted in the result.
165
+ - πŸ§‘ βœ… Patterns and results are mirror images using the same names. Values captured by a pattern are re-inserted in the result.
166
166
  - πŸ€– βœ… A bare (unquoted) identifier is the binder/hole: it binds in a pattern and reads in an expression (Claude's choice to use JSON's one unused syntactic slot, rather than a sigil).
167
167
 
168
168
  ### Alternatives
@@ -232,7 +232,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
232
232
  - Unifies three things (structural matching, type checks, value guards) into one mechanism.
233
233
  - The "type system" is user-extensible with ordinary functions.
234
234
  - Nothing new to learn beyond `?`.
235
- - Predicates compose directly (`a ? @ends | @equals`) instead of requiring a wrapping function (`a ? (x => x | @ends | @equals)`).
235
+ - Predicates compose directly (`a ? @ends | @OP.equal`) instead of requiring a wrapping function (`a ? (x => x | @ends | @OP.equal)`).
236
236
 
237
237
  ### Cons
238
238
 
@@ -274,7 +274,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
274
274
  - πŸ§‘ βœ… Introduce a distinct error mode `!`, separate from `null`.
275
275
  - πŸ§‘ βœ… `null` = legitimate absence; `!` = failure.
276
276
  - πŸ§‘ βœ… A function is made *strict* by ending with `_ => !` (error on no match) and is otherwise *lenient* (returns `null` on no match).
277
- - πŸ§‘ βœ… Total predicates end with `_ => false`.
277
+ - πŸ§‘ βͺ Total predicates end with `_ => false`. Obsoleted by Β§2.12. Predicates no longer need a `_ => false` clause, because `null` became equivalent to `false`.
278
278
  - πŸ€– βœ… Built-in operations return `!` on bad input; built-in predicates return `false`.
279
279
  - πŸ§‘ βœ… The form of `!` (always carrying a payload) is fixed by 2.8.
280
280
  - πŸ€– βœ… `!` matches **only** error patterns (not `_`, not a binder).
@@ -336,7 +336,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
336
336
  - πŸ§‘ βœ… In expression position, `!expr` is a prefix operator that wraps a value as an error; in pattern position, `!pat` matches an error and destructures its payload (bare `!` matches any error without binding; `!_` does the same but admits a `?` predicate).
337
337
  - πŸ§‘ βœ… Propagation preserves the *same* error (payload intact), so by the time you reach a catch site you still know what happened.
338
338
  - πŸ§‘ βœ… The CLI prints the payload (as JSON) to stderr on failure and exits `1`, leaving stdout empty.
339
- - πŸ§‘ βœ… **Errors propagate uniformly β€” they are not values.** At any moment of execution there is either a value or an error in motion, never both. Built-ins (including `@equals` and the type predicates), array and object literals, `?` predicates, and the function-value position of a pipe all propagate an encountered error. The only way to do anything with an error besides letting it propagate is to catch it in an `!pat` clause, which yields a normal value (the payload).
339
+ - πŸ§‘ βœ… **Errors propagate uniformly β€” they are not values.** At any moment of execution there is either a value or an error in motion, never both. Built-ins (including `@OP.equal` and the type predicates), array and object literals, `?` predicates, and the function-value position of a pipe all propagate an encountered error. The only way to do anything with an error besides letting it propagate is to catch it in an `!pat` clause, which yields a normal value (the payload).
340
340
  - πŸ§‘ βœ… **No nested errors.** When `!expr` is evaluated and `expr` itself produces an error, that inner error propagates and the outer `!` is a no-op. This preserves the "never more than one error simultaneously" invariant β€” there is no `!!` value, ever.
341
341
  - πŸ§‘ βœ… **Partial matching propagates the unmatched error.** A function with error clauses that match *some* error shapes (e.g. `!42 => ...`) but not the one it receives (e.g. `!"oops"`) propagates the unmatched error rather than turning it into `null`. The "no match β†’ null" lenient default from 2.7 applies only to non-error inputs.
342
342
  - πŸ§‘ βœ… **`!pat` is a top-level prefix in the clause grammar.** `!` is a prefix on the *clause pattern*, not on any sub-pattern: array elements, object members, and the payload of another `!` all recurse into the non-`!` pattern production. This grammar shape simultaneously enforces two things with no special-case parsing flag: (i) nested error patterns (`[!a, b]`, `{"err": !x}`, `!!42`, `!{"k": !v}`) are syntax errors, matching the runtime invariant that errors never sit inside other values; and (ii) `!pat ? pred` parses as `!(pat ? pred)`, so the `?` binds *inside* the `!`. The runtime payoff is that the predicate of `!a ? pred` naturally sees the payload, with no special case needed in `PGuard`.
@@ -347,7 +347,7 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
347
347
  - πŸ§‘ βͺ Keep `!` opaque with no payload (the original error model, superseded because it was too hard to debug).
348
348
  - πŸ€– πŸ’­ Use a `Result`-style two-variant `Ok | Err` (hypothetical: needs new machinery; payloaded errors with propagation give the same ergonomics with two rules).
349
349
  - πŸ€– ❌ Make payloads always strings (Claude's first payload sketch, e.g. `!"divide by zero"`; rejected because built-in mechanics like missing keys benefit from structured payloads).
350
- - πŸ€– βͺ Make errors first-class values that can be stored in collections, compared with `@equals`, and inspected by predicates (Claude implemented this reading; the designer corrected it because it creates carve-outs in propagation that contradict the "never more than one error" invariant).
350
+ - πŸ€– βͺ Make errors first-class values that can be stored in collections, compared with `@OP.equal`, and inspected by predicates (Claude implemented this reading; the designer corrected it because it creates carve-outs in propagation that contradict the "never more than one error" invariant).
351
351
  - πŸ€– βͺ Parse `!pat ? pred` as `(!pat) ? pred` so the predicate refines the whole error rather than the payload (Claude parsed it this way; the designer corrected it to `!(pat ? pred)` so the predicate sees the payload, matching its sibling binders).
352
352
 
353
353
  ### Pros
@@ -373,12 +373,10 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
373
373
 
374
374
  ### Decisions
375
375
 
376
- - πŸ§‘ βœ… Every error payload produced by "the runtime" (the interpreter or a built-in function) has the **same shape**. This shape is enforced by constructing "internal errors" via `ErrorVal.internal`. The full schema is documented in [reference Β§6.5](../user/reference.md#65-the-standardized-error-payload).
377
- - πŸ€– βœ… The stdlib is "ordinary unpriviledged Fusion code" and doesn't produce internal errors.
378
- - πŸ§‘ βœ… However, all stdlib functions mirror the built-in error shape.
379
- - πŸ”’ βœ… During function application we differentiate between:
380
- - `argument_error`: bad *input shape* (e.g. the wrong number of inputs). Constraints that could be expressed as a pattern without `?`.
381
- - `type_error`: unsupported *input type* (e.g. string instead of number) or constraints between multiple inputs. Usually would require a `?` to express as a pattern.
376
+ - πŸ§‘ βœ… Every error payload produced by "the runtime" (the interpreter or a built-in function) has the **same shape**. This shape is enforced by constructing "runtime errors" via `ErrorVal.from_runtime`. The full schema is documented in [reference Β§6.5](../user/reference.md#65-the-standardized-error-payload).
377
+ - πŸ€– βͺ The stdlib is "ordinary unpriviledged Fusion code". It didn't produce runtime errors. Reverted by Β§2.13. The stdlib now constructs regular `!expr` user errors, but they get marked as runtime errors afterwards.
378
+ - πŸ§‘ βœ… All stdlib functions mirror the built-in error shape.
379
+ - πŸ”’ βͺ During function application we differentiated `argument_error` (bad input *shape*, expressible as a pattern without `?`) from `type_error` (bad input *type*). Reverted in Β§2.13. Both errors got unified into a single `argument_error`.
382
380
  - πŸ§‘ βœ… Member/index access reserves `access_error` for exactly `missing key` and `index out of range`:
383
381
  - Accessing a member of a non-object or indexing with a wrong-typed key is a `type_error` instead.
384
382
  - File-system access failures ("missing file", "directory instead of file", "permission denied") are a `reference_error` instead.
@@ -467,6 +465,53 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
467
465
 
468
466
  ---
469
467
 
468
+ ## 2.13 Refining the error payload
469
+
470
+ Refines Β§2.9: same general shape, more orthogonal fields, field values easier to match on, field values contain smarter contents
471
+
472
+ ### Decisions
473
+
474
+ - πŸ§‘ βœ… The error payload fields are now: `kind`, `origin`, `file` (opt), `operation`, `status`, `input`, `expected` (opt), `message` (opt).
475
+ - πŸ§‘ βœ… Split `location` into `origin` (where the operation is *defined*) and an optional `file` (the **innermost user-code file** on the call chain).
476
+ - πŸ§‘ βœ… `file` is `Dir.pwd`-relative, so it reads as the route from the location where `fusion` was called to the offending source code.
477
+ - πŸ§‘ βœ… Split `status` out from `input`. `status` is `0` (a value) or `1` (an error). On `1`, `input` carries the error's bare payload, so `input` is always valid JSON.
478
+ - πŸ§‘ βœ… `operation` now contains the failing operation's own **`@`-reference** (`@`, `@@`, `@lt`, `@math.round`, `@../mod`, `@load`) or for Built-in *syntax* its own form (`|`, `.key`, `[]`, `parsing code`). Loading the top-level program file is `loading code` (not an `@`-reference).
479
+ - πŸ§‘ βœ… An `@`-reference takes no argument, so its `input` is `null` and its `status` is always `0`. `@load` is the exception: it's a function taking a filename.
480
+ - πŸ§‘ βœ… For *access errors* the "key" appears only once:
481
+ - `.name` carries the static key in `operation` and the object alone in `input`
482
+ - `[]` is generic in `operation` and echoes the key in the `input` = `[collection, key]`
483
+ - `[=]` is generic in `operation` and echoes the key in the `input` = `[collection, key, newValue]`
484
+ - πŸ§‘ βœ… A failure to read a file (missing file, directory given, access denied) is reported as `"operation": <the literal @-reference>`, `"input": null`, `"file": <the referring call site>`.
485
+ - πŸ§‘ βœ… `type_error` is merged into `argument_error`. The distinction between *wrong shape* and *wrong type* didn't fit Fusion's runtime type system.
486
+ - πŸ§‘ βœ… `expected` lists the acceptable inputs as Fusion patterns (the input matched none); an error with `expected` never also carries a `message`.
487
+ - πŸ§‘ βœ… `internal_error` is the new catch-all for an unexpected host/interpreter failure. It's a Ruby error the engine caught rather than letting it crash the process (`origin` `interpreter` or `builtin`). It's an interpreter bug.
488
+ - πŸ§‘ βœ… A runtime resource limit being exceeded is a separate `limit_error` (currently a stack overflow, `"stack level too deep"`): the runtime gave up because a space/time budget ran out β€” not an engine defect. The general name (vs `stack_error`) lets future runtime resource limits share the kind.
489
+ - πŸ§‘ βœ… stdlib functions preemptively handle all *argument* errors. They appear atomic. No input should be able to trigger e.g. an error in a `|` operation. *Argument* errors refer to the stdlib function itself (`origin: "stdlib"`, `operation` = its `@`-reference).
490
+ - πŸ§‘ βœ… stdlib functions are *transparent* for *inner errors*. They can't catch every possible error from inner operations, so an inner error bubbles through unchanged. The purest example is `@map`, which knows nothing about the given `f`: an error from `f` originates from `f` and simply bubbles through `map`.
491
+ - πŸ§‘ βœ… stdlib higher-order functions (`@all`/`@map`) guard `f ? @Function` in every clause β€” a non-function `f` errors even on an empty collection β€” and `expected` shows the guard.
492
+ - πŸ§‘ βœ… `@all` short-circuits: the first falsey item yields `false`, the rest go untested.
493
+
494
+ ### Alternatives
495
+
496
+ - πŸ”’ βͺ A variable `location` string embedding the file/builtin name, with the error marker living inside `input` β€” split into the fixed `origin` + `file`, and the `status` field.
497
+ - πŸ”’ ❌ Over-approximating `expected` patterns for `@join`/`@toObject` (e.g. `[_ ? @Array, _ ? @String]`) β€” they would match inputs that still fail, breaking "matches β‡’ acceptable"; `@all` keeps them exact.
498
+ - πŸ”’ ❌ `operation` = the *literal source text* of the `|`'s right-hand side. Not implementable: the text isn't available where the error is born; stamping it at `apply` would relabel inner errors bubbling *through* a function (violating Β§2.9 transparency); and an indirect RHS like `f` is uninformative. The producer's own `@`-reference gives the same result for a direct call and stays correct otherwise.
499
+ - πŸ§‘ ❌ Drop `conversion_error` and have a failed conversion (e.g. `@parseNumber` of `"abc"`) return `null` (a "Maybe", as Ruby's `to_i` does). Rejected: the error payload carries more information, `| (! => null)` recovers the lenient form in one token, and forcing a catch keeps errors local β€” which matters with no backtraces. (A `null` would slip downstream and surface far from its cause.)
500
+ - πŸ§‘ ❌ Base the payload path on the jail / program directory instead of `Dir.pwd`. Rejected: `Dir.pwd` gives a path usable straight from your shell; for an installed/shebang tool invoked from elsewhere, a jail-relative path would describe the program's internal layout, which you'd then have to rebase onto your own location.
501
+
502
+ ### Pros
503
+
504
+ - `input` is always valid JSON as the `status` now lives in its own field
505
+ - `expected` documents the acceptable inputs as patterns a caller can reuse.
506
+ - `origin` is directly dispatchable as the variable filename now lives in its own `file` field.
507
+ - The roles of `file` and `operation` have been clarified and are much more helpful now.
508
+
509
+ ### Cons
510
+
511
+ - A few `expected` patterns must reference the `@all` stdlib helper, so they aren't purely structural.
512
+
513
+ ---
514
+
470
515
  # 3. @ references
471
516
 
472
517
  ## 3.1 A file contains exactly one value
@@ -605,10 +650,10 @@ Future work and open questions are tracked separately in our [Roadmap](./roadmap
605
650
 
606
651
  All access goes through `@`:
607
652
 
608
- - πŸ§‘ βœ… **Built-ins require `@`.** `@add`, `@Integer`, etc. A bare identifier is *only* a pattern hole; it never denotes a built-in. Built-ins cannot be shadowed by a clause's bindings β€” they live in a different namespace entirely.
653
+ - πŸ§‘ βœ… **Built-ins require `@`.** `@OP.sum`, `@Integer`, etc. A bare identifier is *only* a pattern hole; it never denotes a built-in. Built-ins cannot be shadowed by a clause's bindings β€” they live in a different namespace entirely.
609
654
  - πŸ§‘ βœ… **The standard library has no prefix.** `@map` reaches it directly.
610
655
  - πŸ§‘ βœ… **An `@name` reference (without leading `../`) resolves: sibling file β†’ built-in β†’ stdlib file β†’ error.** First match wins. Consequently siblings can shadow a built-in or a stdlib function, but only for files in that directory (never globally).
611
- - πŸ§‘ βœ… **Built-in/stdlib fallback is gated on `../`, not on `/`.** Downward paths (`@dir/a`, `@math/sqrt`) remain eligible for the built-in/stdlib fallback; only upward paths (`@../a`) are file-only and never fall back.
656
+ - πŸ§‘ βœ… **Built-in/stdlib fallback is gated on `../`, not on `/`.** Downward paths (`@dir/a`, `@util/helper`) remain eligible for the built-in/stdlib fallback; only upward paths (`@../a`) are file-only and never fall back.
612
657
  - πŸ§‘ βœ… **A bare `@`** (nothing after it) means the current file β€” recursion is written this way rather than by repeating the file's own name.
613
658
  - πŸ§‘ βœ… **`@ENV`** is a built-in evaluating to an object of environment variables (all string values, no parsing); read with member access (`@ENV.CI`).
614
659
  - πŸ§‘ βœ… **`@load`** is a built-in taking a filename **verbatim** (no `.fsn` appended), resolved relative to the referencing file, for runtime/non-identifier filenames. Both `@ENV` and `@load` resolve in the `@name` chain, so both are shadowable by a sibling file of that name.
@@ -617,7 +662,7 @@ All access goes through `@`:
617
662
 
618
663
  - Every bare `@name` follows one precedence order (sibling β†’ builtin β†’ stdlib), so there are no reserved names to remember and no parser carve-outs.
619
664
  - `ENV` and `load` live in the builtin tier like everything else.
620
- - Gating fallback on `../` rather than on the presence of any `/` keeps downward paths (`@math/sqrt`) eligible for the stdlib, which is what stdlib subpackaging needs.
665
+ - Gating fallback on `../` rather than on the presence of any `/` keeps downward paths (`@util/helper`) eligible for the stdlib, which is what stdlib subpackaging needs.
621
666
 
622
667
  ### Alternatives
623
668
 
@@ -635,12 +680,86 @@ All access goes through `@`:
635
680
 
636
681
  ### Cons
637
682
 
638
- - Built-ins are verbose (`@add` everywhere).
683
+ - Built-ins are verbose (`@OP.sum` everywhere).
639
684
  - Shadowing is invisible at the call site (whether `@map` is yours or the stdlib's depends on directory contents).
640
685
  - A bare word that *looks* like a function reference is silently just a hole (reading an unbound one yields an error).
641
686
 
642
687
  ---
643
688
 
689
+ ## 3.7 Bare `@` also works for inline code, not only for files
690
+
691
+ ### Decisions
692
+
693
+ - πŸ§‘ βœ… A bare `@` is the value of the current top-level **unit**: a file (previously the only case), an inline (`-e`) program, or a REPL entry.Self-recursion works in all three cases.
694
+ - πŸ§‘ βœ… Interpreter context is not part of the identifier namespace. `:dir`/`:file`/`:self` are hidden values and not exposed as `__dir__`/`__file__`/`__self__`.
695
+
696
+ ### Alternatives
697
+
698
+ - πŸ€– ❌ Model inline/REPL code as a synthetic "fake file" so the existing file machinery applies β€” needs temp-file lifecycle or a special-cased reader, and leaves the REPL with no coherent "which file" answer.
699
+ - πŸ§‘ βͺ "Bare `@` = the current file" (3.2/3.6); the "no current file for self-reference" error is gone, as it can no longer occur.
700
+ - πŸ€– 🩹 Claude's first cut kept the self-value as an ordinary binding, so reading `__self__` returned an internal thunk and crashed serialization with a raw Ruby error; the designer caught it. Interpreter context now lives in its own channel, off the binding namespace.
701
+
702
+ ### Pros
703
+
704
+ - One self-reference rule across files, inline source, and the REPL. The file path is incidental.
705
+ - No identifier-namespace pollution. Internals stay internal.
706
+
707
+ ### Cons
708
+
709
+ - `__dir__` is no longer exposed.
710
+
711
+ ---
712
+
713
+ ## 3.8 The jail: confining `@`-resolution to a directory
714
+
715
+ ### Decisions
716
+
717
+ - πŸ§‘ βœ… `-j/--jail DIR` confines `@`-resolution to `DIR` and its subtree. It defaults to the program's directory (or cwd for `-e` and the REPL). Available in every use case, the REPL included.
718
+ - πŸ§‘ βœ… A relative `--jail` resolves against the default jail, so `-j ..` widens to the parent; `--jail '*'` disables confinement entirely.
719
+ - πŸ§‘ βœ… An out-of-jail target is a `reference_error` (`outside the jail`).
720
+ - πŸ§‘ βœ… The stdlib is unaffected by the jail. However, an existing file outside the jail raises an error and prevents falling through to a built-in or the stdlib.
721
+ - πŸ§‘ βœ… `@`-references still resolve relative to the **referencing file**; the jail only filters the resolved target, it does not move the resolution base.
722
+ - πŸ”’ βœ… Containment is lexical (`expand_path` normalises `..`) and confines references to a directory tree. It is **not** a security sandbox and follows existing symlinks. Fusion cannot write files, so no symlink can be planted to escape. Any encountered symlink is part of the legitimate project layout.
723
+
724
+ ### Alternatives
725
+
726
+ - πŸ§‘ πŸ’­ Resolve `@`-references relative to the **jail root** instead of the referencing file (a `--relative-to-jail` mode). Rejected: it would make `@name` mean `<jail>/name` everywhere and turn per-directory sibling-shadowing (3.6) into jail-global shadowing (project-rooted imports).
727
+ - πŸ€– ❌ Resolve symlinks (`realpath`) to make the jail a hard boundary. Declined: it buys nothing here (a program that cannot write files cannot plant an escaping symlink) and a real security sandbox is tricky to build.
728
+
729
+ ### Pros
730
+
731
+ - A `.fsn` program is sandboxed to its own directory by default; reaching out is explicit (`-j ..`) or an opt-out (`-j '*'`).
732
+ - The stdlib and stdin are untouched, so confinement never breaks an ordinary program.
733
+
734
+ ### Cons
735
+
736
+ - Safe symlink-following rests on Fusion being unable to write files; adding a file-writing capability would mean revisiting it.
737
+
738
+ ---
739
+
740
+ ## 3.9 Super-reference `@@`
741
+
742
+ ### Decisions
743
+
744
+ - πŸ§‘ βœ… `@@name` (and downward `@@dir/name`) resolves `@name` while *skipping its sibling files*. It always references a builtin/stdlib and is immune to local shadowing. Bare `@@` is the special case `@@<current-file>`.
745
+ - πŸ§‘ βœ… Inline/REPL `@@` is a `reference_error` (`no enclosing file`). There is no filename to take "super" of.
746
+
747
+ ### Alternatives
748
+
749
+ - πŸ€– ❌ Make `@name` never refer to its own file (so `@range` inside `range.fsn` means the stdlib function). Rejected: the override would name itself (breaking relocatability) and it adds a per-file carve-out to the resolution chain.
750
+
751
+ ### Pros
752
+
753
+ - An override delegates to the original without a separate handle, and `@name` semantics are untouched.
754
+ - Relocatable: no file names itself.
755
+ - `@@name` provides a stable mechanism for error-payload patterns that must stay canonical regardless of a caller's shadows.
756
+
757
+ ### Cons
758
+
759
+ - Reaches only what you shadow under your *own* name, not an arbitrary shadowed built-in.
760
+
761
+ ---
762
+
644
763
  # 4. Runtime and CLI
645
764
 
646
765
  ## 4.1 Runtime I/O contract
@@ -649,7 +768,7 @@ All access goes through `@`:
649
768
 
650
769
  - πŸ§‘ βœ… Read stdin as JSON β†’ value `v`; compute `v | program`; print the result as JSON.
651
770
  - πŸ§‘ βœ… A final `!` produces a nonzero exit code.
652
- - πŸ€– βœ… Empty stdin is treated as `null`.
771
+ - πŸ€– βͺ Empty stdin was treated as `null`. Superseded in 4.4: empty stdin means *no input*.
653
772
  - πŸ€– βœ… Non-JSON stdin yields `!`.
654
773
 
655
774
  ### Alternatives
@@ -665,7 +784,7 @@ All access goes through `@`:
665
784
  ### Cons
666
785
 
667
786
  - No streaming; whole input must be buffered and parsed.
668
- - 🩹 A bare `!` with nonzero exit gives little diagnostic detail. Mitigated for internal errors by more detailed error payloads in 2.9.
787
+ - 🩹 A bare `!` with nonzero exit gives little diagnostic detail. Mitigated for runtime errors by more detailed error payloads in 2.9.
669
788
 
670
789
  ---
671
790
 
@@ -695,21 +814,21 @@ All access goes through `@`:
695
814
 
696
815
  ---
697
816
 
698
- ## 4.3 Internal errors and lenient JSON serialization
817
+ ## 4.3 Runtime errors and lenient JSON serialization
699
818
 
700
819
  ### Decisions
701
820
 
702
821
  - πŸ”’ βœ… To serialize values without a valid JSON representation, we introduce "lenient serialization". Values without JSON representation get turned into string representations (`"<function>"` and `"<Infinity>"`/`"<-Infinity>"`/`"<NaN>"`).
703
822
  - πŸ§‘ βœ… The remaining data structure gets preserved.
704
- - πŸ”’ βœ… Internal errors (`ErrorVal.internal_error?`) get serialized leniently by default, so their info isn't obscured by a `serialization_error`.
705
- - πŸ€– βœ… The stdlib doesn't produce internal errors.
706
- - πŸ§‘ βœ… However, all stdlib functions use `@sanitize` to mimick the lenient JSON serialization and preserve as much info as possible.
707
- - πŸ§‘ βœ… Ordinary values and user errors are serialized strictly to avoid surprising type conversions. If they fail to serialize, they get turned into an internal `serialization_error` and will subsequently get serialized leniently.
823
+ - πŸ”’ βœ… Runtime errors (`ErrorVal#runtime?`) get serialized leniently by default, so their info isn't obscured by a `serialization_error`.
824
+ - πŸ€– βͺ The stdlib doesn't produce runtime errors. Reverted in Β§2.13. stdlib errors now get marked as runtime errors.
825
+ - πŸ§‘ βͺ All stdlib functions use `@sanitize` to mimick the lenient JSON serialization and preserve as much info as possible. Obsoleted by Β§2.13. However, `@sanitize` is kept as a utility.
826
+ - πŸ§‘ βœ… Ordinary values and user errors are serialized strictly to avoid surprising type conversions. If they fail to serialize, they get turned into a runtime `serialization_error` and will subsequently get serialized leniently.
708
827
 
709
828
  ### Alternatives
710
829
 
711
- - πŸ€– ❌ The standard library could create real "internal errors" via a dedicated `@raise` primitive. Declined, because it offers little over the `!` prefix and would let any code (not just the stdlib) create internal errors. The main reason for internal errors (apart from enforcing a consistent shape) is lenient serialization.
712
- - πŸ§‘ ❌ Internal errors also serialize strictly by default. Rejected, because this would turn too many errors into a `serialization_error` and would lose too much information.
830
+ - πŸ€– ❌ The standard library could create real "runtime errors" via a dedicated `@raise` primitive. Declined: it offers little over the `!` prefix and would let *any* code create runtime errors. Instead a stdlib `!{…}` is marked runtime by its construction *location* β€” the same benefit (lenient serialization, a consistent shape, call-site `file`), confined to stdlib source.
831
+ - πŸ§‘ ❌ Runtime errors also serialize strictly by default. Rejected, because this would turn too many errors into a `serialization_error` and would lose too much information.
713
832
  - πŸ§‘ πŸ’­ All errors serialize leniently by default. Rejected, because this hides real errors (e.g. `NaN` or a function as a result) behind an automatic type conversion.
714
833
 
715
834
  ---
@@ -723,29 +842,43 @@ error value can cross the boundary.
723
842
 
724
843
  **Use cases:**
725
844
 
726
- - πŸ§‘ βœ… The CLI supports three use cases: **pipe** (apply the program to one input β€” the 4.1 model, the default), **stream** (apply it to each line of an NDJSON stream), **repl** (interactive).
727
- - πŸ§‘ βœ… **stream** keeps errors in-band and always exits `0`; a failing record (including a stack overflow) becomes that record's output line and the stream continues. Blank input lines are skipped.
845
+ - πŸ§‘ βœ… The CLI supports three use cases: **pipe** (apply the program to one input, Β§4.1), **stream** (apply it to each line of an NDJSON stream), **repl** (interactive).
846
+ - πŸ§‘ βœ… **pipe**: Compute `stdin | program`. Empty or whitespace-only stdin means *no input*: return `program` (evaluated on load). So a `.fsn` file doubles as enriched JSON (computations, `@ENV`, `@`-references).
847
+ - πŸ§‘ βœ… **stream**: Conforms to NDJSON. Keeps errors in-band and continues the stream. Always exits `0`.
848
+ - πŸ§‘ βœ… A blank **stream** input line is echoed as a blank output line (no computation); `--skip-blank-lines` drops it instead.
849
+ - πŸ§‘ βœ… **repl**: Can evaluate expressions. Also allows an assignment statement: `identifier = expression`.
850
+ - πŸ€– βœ… A command-line misuse (unknown flag, more than one use case, conflicting input/output modes, unsupported mode combination, missing program, `-!` with empty stdin) is plain usage text on stderr with exit `1`, never a payloaded error. Most are caught during option parsing, `-!` with empty-stdin while reading input.
728
851
 
729
852
  **I/O modes** β€” how an error is marked crossing the boundary; `--input` and `--output` are independent:
730
853
 
731
- - πŸ§‘ βœ… Four modes: **unix** (plain JSON; value β†’ stdout/exit 0, error β†’ stderr/exit 1, as in 4.1), **bang** (a leading `!` marks an error), **array** (`[0, value]` / `[1, payload]`), **object** (`{"value": _}` / `{"error": _}`).
732
- - πŸ§‘ βœ… The `-!` flag (unix input only) makes the whole input an error value.
733
- - πŸ§‘ βœ… pipe supports all four modes; stream all but unix; repl has none.
734
- - πŸ€– βœ… Defaults: pipe = unix/unix, stream = bang/bang. The non-unix modes always print to stdout and exit `0`.
735
- - πŸ€– βœ… Empty input is `null` in every input mode; `-!` on empty input is `!null`.
854
+ TODO: Under `-!` the input is the error payload, so empty stdin is a usage error (nothing to mark).
855
+
856
+ - πŸ§‘ βœ… Four modes: **unix** (asymmetric, Unix filter) and **bang** / **array** (`[0, value]` / `[1, payload]`), **object** (`{"value": _}` / `{"error": _}`).
857
+ - πŸ§‘ βœ… **unix**: input is `stdin` + `-!` flag, output is `value β†’ stdout` + `exit 0` OR `error β†’ stderr` + `exit 1`. stdin/stdout/stderr are always pure JSON.
858
+ - πŸ§‘ βœ… **bang**: shortest encoding, errors are simply a `!` prefix and thus not valid JSON.
859
+ - πŸ§‘ βœ… **array**: always valid JSON, error encoded via envelope: `[0, value]` / `[1, payload]`.
860
+ - πŸ§‘ βœ… **object**: always valid JSON, error encoded via envelope: `{"value": _}` / `{"error": _}`.
861
+ - πŸ§‘ βœ… `-!` requires stdin. With absent stdin, it's a usage error.
862
+ - πŸ§‘ βœ… pipe supports all four modes; stream all but unix (so errors stay "in-band"); repl has none.
863
+ - πŸ§‘ βœ… Defaults: pipe = unix/unix, stream = array/array.
736
864
  - πŸ€– βœ… A malformed `array`/`object` input envelope is a catchable `argument_error` at `location: "input"` (the array tag must be exactly the integer `0`/`1`), flowing into the program like any input error.
737
865
 
738
866
  **REPL:**
739
867
 
740
- - πŸ§‘ βœ… An entry is an **expression** (evaluated and printed) or a **statement** `identifier = expression` (evaluated, printed, and bound for later entries). The statement is the only construct that is not an expression.
741
- - πŸ§‘ βœ… An entry is evaluated only once it parses as a whole statement/expression; an incomplete or invalid buffer keeps the entry open to finish or correct.
742
- - πŸ§‘ βœ… A statement binds its identifier to the result **including an error result**; reading it back later propagates the error, exactly like reading an `@`-reference that resolved to one.
868
+ - πŸ§‘ βœ… An entry is an **expression** (evaluated and printed) or an **assignment statement** `identifier = expression` (evaluated, printed, and bound for later entries).
869
+ - πŸ§‘ βœ… An entry is evaluated only once it parses as a whole statement/expression. An incomplete or invalid buffer keeps the entry open to finish or correct.
870
+ - πŸ§‘ βœ… Error results can also get bound to an identifier via the **assignment statement**. When accessing them, they'll propagate regularly.
743
871
  - πŸ€– βœ… Results print leniently (a function as `"<function>"`, etc.); entries report errors at `location: "code <inline>"`, like `-e`.
744
872
  - πŸ€– βœ… Results go to stdout; the prompt and echoed input go to stderr (like a shell prompt), so stdout is a clean stream of results.
745
- - πŸ€– βœ… A command-line misuse (unknown flag, unsupported mode combination, missing program) is plain usage text on stderr with exit `1` β€” it precedes the I/O contract, so it is not a payloaded error.
873
+ - πŸ§‘ βœ… `stderr` decorations are styled. Styling never touches `stdout`.
746
874
 
747
875
  ### Alternatives
748
876
 
877
+ - πŸ§‘ βͺ pipe was the unconditional default β€” now repl on a bare `fusion`, otherwise pipe.
878
+ - πŸ€– βͺ stream defaulted to `bang`/`bang` β€” now `array`/`array`, since `bang` lines aren't valid JSON.
879
+ - πŸ€– βͺ Empty input was the value `null`, and input could also come from an inline `[json-input]` argument β€” now empty means *no input* and input is stdin-only.
880
+ - πŸ§‘ βͺ Empty stdin under `-!` supplied `!null` β€” now a usage error, since there is no payload to mark.
881
+ - πŸ€– βͺ Blank `stream` lines were silently skipped β€” now echoed by default, dropped with `--skip-blank-lines`.
749
882
  - πŸ§‘ βͺ REPL accepts only statements (the original "introduces a single statement" sketch); widened so a bare expression is also an entry.
750
883
  - πŸ§‘ βͺ Statements terminated by `;` (so several could share a line); dropped β€” completeness is decided by parsing, so a line that parses is submitted and no terminator is needed.
751
884
  - πŸ€– βͺ A statement does **not** bind an error result (mirroring pattern binders, which never capture an error). Flipped: a statement is an assignment, not a pattern match; binding an error is harmless and needs no special case.
@@ -757,11 +890,13 @@ error value can cross the boundary.
757
890
  - One small flag surface spans first-class Unix filters (pipe), bulk processing (stream), and interactive exploration (repl).
758
891
  - Errors cross the boundary in whatever shape the surrounding tool wants β€” exit code, `!` sentinel, or envelope β€” with input and output chosen independently.
759
892
  - The REPL reuses the whole language: an entry is just an expression, with assignment the one addition, and a bound error propagates like any other value-or-error (no carve-out).
893
+ - A program with no stdin doubles as enriched JSON (computation, `@ENV`, `@`-references), and `--stream` emits valid NDJSON.
760
894
 
761
895
  ### Cons
762
896
 
763
897
  - Four error-marking modes are more surface than the original single unix contract.
764
898
  - Completeness-by-parsing submits a complete-but-maybe-unintended line (e.g. `x = 5` when more was meant) as-is.
899
+ - Empty stdin no longer means the value `null`; to feed `null` you pipe the literal `null`.
765
900
 
766
901
  ---
767
902
 
@@ -771,9 +906,8 @@ error value can cross the boundary.
771
906
 
772
907
  ### Decisions
773
908
 
774
- - πŸ§‘ βœ… No infix `+ - * / == < && …`.
775
- - πŸ§‘ βœ… Arithmetic, comparison, and boolean operations are built-in functions applied to a pair, e.g. `[a, b] | @add`.
776
- - πŸ§‘ βœ… Sugar is explicitly deferred, not rejected.
909
+ - πŸ§‘ βͺ No infix `+ - * / == && …` until the core semantics are settled. Superseded by Β§5.6. Syntax sugar is now implemented.
910
+ - πŸ§‘ βœ… Arithmetic, comparison, and boolean operations are built-in functions reached via an `@-reference`, e.g. `[a, b] | @OP.sum`.
777
911
 
778
912
  ### Alternatives
779
913
 
@@ -786,7 +920,7 @@ error value can cross the boundary.
786
920
 
787
921
  ### Cons
788
922
 
789
- - Arithmetic-heavy code is verbose and harder to read (`[n, [n, 1] | @subtract | @fact] | @multiply` vs. `n * fact(n-1)`).
923
+ - Arithmetic-heavy code is verbose and harder to read (`[n, [n, -1] | @OP.sum | @fact] | @OP.product` vs. `n * fact(n-1)`).
790
924
 
791
925
  ---
792
926
 
@@ -819,17 +953,10 @@ error value can cross the boundary.
819
953
  ### Decisions
820
954
 
821
955
  - πŸ€– βœ… Only things that can't be built in Fusion itself become a builtin. Other frequently used functions become part of the standard library.
822
- - πŸ€– βœ… The interpreter provides these builtins:
823
- - arithmetic (`add`, `subtract`, `multiply`, `divide`, `mod`, `negate`, `floor`);
824
- - comparison (`equals`, `lessThan`);
825
- - boolean (`and`, `or`, `not`);
826
- - bridges (`length`, `concat`, `chars`, `join`, `toString`, `parseNumber`, `keys`, `values`);
827
- - predicates (`Integer`, `Float`, `Number`, `String`, `Boolean`, `Array`, `Object`, `Null`).
828
956
  - πŸ€– βœ… `keys` must be a builtin: pattern matching can pull *known* object keys but cannot enumerate *unknown* ones, so iterating an object of unknown shape is impossible without it.
829
957
 
830
958
  ### Alternatives
831
959
 
832
- - πŸ€– ❌ Derive `lessThan`'s siblings as built-ins too (chose to leave `lessEq`/`greaterThan`/etc. to the library).
833
960
  - πŸ€– ❌ Omit `values` (derivable from `keys`).
834
961
 
835
962
  ### Pros
@@ -873,3 +1000,65 @@ error value can cross the boundary.
873
1000
 
874
1001
  - No way to annotate a single token mid-line; an explanatory comment must occupy its own line above the code.
875
1002
  - A breaking change from the earlier `//` / `/* */` syntax (acceptable at this Alpha stage).
1003
+
1004
+ ---
1005
+
1006
+ ## 5.5 Reworking the builtins and standard library
1007
+
1008
+ ### Decisions
1009
+
1010
+ - πŸ§‘ βœ… Bundle the most important arithmetic and logic operations together into a single `@OP` reference.
1011
+ - πŸ§‘ βœ… Make the `stdlib` as orthogonal as possible to `@OP`.
1012
+ - πŸ§‘ βœ… Higher-order helpers (`map`, `filter`, `reduce`, `compact`, `flatten`, `any`, `all`) are implemented via recursion in Fusion, not hidden in Ruby. They work for both arrays and objects where possible.
1013
+ - πŸ§‘ βœ… Provide access to more advanced mathematical operations in `@math`.
1014
+ - πŸ§‘ βœ… Where possible, builtins and stdlib functions are n-ary instead of binary.
1015
+
1016
+ ### Alternatives
1017
+
1018
+ - πŸ€– βͺ Keep every operator as its own builtin (the Β§5.3 set). Superseded: bundling into `@OP` gives a directory one place to shadow them all.
1019
+ - πŸ€– ❌ Resolve `@OP` **dynamically** (at the call site) so stdlib helpers follow a foreign override. Rejected: it breaks per-directory confinement.
1020
+
1021
+ ### Pros
1022
+
1023
+ - By bundling operators into `@OP`, the presence of an `OP.fsn` file will immediately become a warning sign that core operations might behave differently in a given directory.
1024
+ - By making the `stdlib` orthogonal to `@OP`, a shadowed `@OP` will not create footguns where `stdlib` functions still refer to the original implementation.
1025
+
1026
+ ### Cons
1027
+
1028
+ - Not all operators with syntax sugar have been grouped into `@OP`. Exceptions are the structural operators `@map`, `@filter`, `@reduce`.
1029
+ - The few helpers that still reference `@OP` (currently only `@range`) will ignore `@OP` overrides. To make them aware of `@OP` overrides, create a copy of their `stdlib` source code next to your `OP.fsn`.
1030
+ - The native way of writing division `[a, b | @OP.negate] | @OP.product` is numerically incorrect and might produce a double rounding error. For the numerically correct division you have to use `@math.divide`.
1031
+
1032
+ ---
1033
+
1034
+ ## 5.6 Syntax sugar
1035
+
1036
+ Implements the infix-operator sugar deferred in Β§5.1. Additionally adds `map` / `filter` / `reduce`
1037
+ pipe operators. Promotes `[]`/`[=]` to core syntax. Desugaring is purely syntactic. The only new
1038
+ runtime node is the `[=]` setter.
1039
+
1040
+ ### Decisions
1041
+
1042
+ - πŸ”’ βœ… Precedence, tightestβ†’loosest: postfix (`.` `[]` `[=]`) β†’ unary prefix (`! - / ~`) β†’ pipe (`| |: |? |+`) β†’ multiplicative (`* / % //`) β†’ additive (`+ -`) β†’ `??` β†’ `==` β†’ `&&` β†’ `||`. Pipe binds just under unary, tighter than every value operator.
1043
+ - πŸ§‘ βœ… The array/object setter gets promoted to core syntax `container[key = value]`. The `@set` builtin is removed.
1044
+ - πŸ§‘ βœ… The getter `container[key]` stays core syntax instead of becoming syntax sugar for `@get`. The `@get` builtin is removed.
1045
+ - πŸ§‘ βœ… A maximal run of `+`/`-` folds to one `[terms] | @OP.sum`; a run of `*`/`/` to one `[terms] | @OP.product`. `-` operands are negated, `/` operands inverted.
1046
+ - πŸ§‘ βœ… `-` is always an operator; the lexer emits no negative-number literals.
1047
+ - πŸ§‘ βœ… A `-` before a bare numeric literal folds into a negative literal (in expressions and patterns alike); any other operand becomes `operand | @OP.negate`. In a pattern, `-` may only precede a numeric literal.
1048
+ - πŸ§‘ βœ… `/` never folds to a literal: `a / 2` β†’ `[a, 2|@OP.invert] | @OP.product`, so `/0` stays a runtime `math_error`.
1049
+ - πŸ§‘ βœ… A single `/` is a path separator only inside a file-reference path; elsewhere it is division/invert. `@a/b` is the path `a/b`; `@a / b` (any space around the slash) is `@a` divided by `b`.
1050
+ - πŸ§‘ βœ… The lexer emits a `path` token only immediately after `@`/`@@`, tight: no space after `@`/`@@`, interior slashes abutting their segments. A path starts with an identifier or `..`, never a lone `.` (so `@.map` is `.map` access on bare `@`). Bare `@`/`@@` when no tight path follows.
1051
+ - πŸ§‘ βœ… Whitespace between `@`/`@@` and its path is no longer allowed (`@ name` is a syntax error).
1052
+ - πŸ§‘ βœ… Runs of `==`/`&&`/`||` fold n-ary to `@OP.equal`/`@OP.and`/`@OP.or`.
1053
+ - πŸ§‘ βœ… `??` is its own precedence level, tighter than `==` (compare produces an ordinal that `==` then tests; a boolean can't be compared).
1054
+ - πŸ§‘ βœ… `xs |: f`, `xs |? f`, `xs |+ f` desugar to `{"c": xs, "f": f}` piped into `@map`/`@filter`/`@reduce`.
1055
+
1056
+ ### Alternatives
1057
+
1058
+ - πŸ€– βͺ Lex negative-number literals (JSON-style). Rewound: `a-3` would be ambiguous between the literal `-3` and subtraction.
1059
+ - πŸ€– ❌ Desugar every `-x` to `x | @OP.negate`, dropping literal negatives. Rejected: `-5` should stay a plain literal, not an `@OP`-routed computation.
1060
+ - πŸ€– ❌ Assemble the path in the parser so `@a / b` is also the path `a/b` (divide via `(@a) / b`). Rejected: a spaced `@a / b` should read as division like every other operator.
1061
+
1062
+ ### Pros
1063
+
1064
+ - Giving `pipe` the tightest precedence keeps the "useful reading" paren-free: a pipe's RHS has to always be a function and arithmetic never yields one, so `x|@f + 1` can only sensibly mean `(x|@f) + 1`.