fusion-lang 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +24 -0
  3. data/.simplecov +11 -0
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +2 -1
  6. data/Rakefile +8 -0
  7. data/docs/lang/design.md +54 -5
  8. data/docs/lang/implementation.md +41 -0
  9. data/docs/user/how-to-guides.md +90 -5
  10. data/docs/user/reference.md +48 -37
  11. data/docs/user/tutorial.md +6 -5
  12. data/examples/double.fsn +4 -1
  13. data/examples/factorial.fsn +6 -3
  14. data/examples/gcd.fsn +9 -0
  15. data/examples/matrix/OP.fsn +2 -0
  16. data/examples/matrix/average.fsn +2 -0
  17. data/examples/matrix/solve.fsn +2 -0
  18. data/exe/fusion +2 -2
  19. data/lib/fusion/ast.rb +76 -28
  20. data/lib/fusion/atom.rb +1 -1
  21. data/lib/fusion/cli/decoder.rb +6 -6
  22. data/lib/fusion/cli/encoder.rb +2 -2
  23. data/lib/fusion/cli/options.rb +11 -8
  24. data/lib/fusion/cli/parser.rb +1 -1
  25. data/lib/fusion/cli/repl.rb +6 -6
  26. data/lib/fusion/cli/serializer.rb +6 -7
  27. data/lib/fusion/cli.rb +1 -1
  28. data/lib/fusion/interpreter/builtins.rb +51 -22
  29. data/lib/fusion/interpreter/thunk.rb +2 -2
  30. data/lib/fusion/interpreter.rb +31 -23
  31. data/lib/fusion/lexer.rb +62 -40
  32. data/lib/fusion/parser.rb +72 -35
  33. data/lib/fusion/version.rb +3 -1
  34. data/lib/fusion.rb +0 -1
  35. data/stdlib/all.fsn +2 -2
  36. data/stdlib/any.fsn +3 -3
  37. data/stdlib/compact.fsn +2 -3
  38. data/stdlib/concat.fsn +4 -3
  39. data/stdlib/entries.fsn +6 -0
  40. data/stdlib/filter.fsn +3 -3
  41. data/stdlib/map.fsn +3 -2
  42. data/stdlib/matrix/Matrix.fsn +8 -0
  43. data/stdlib/matrix/OP.fsn +18 -0
  44. data/stdlib/matrix/add.fsn +7 -0
  45. data/stdlib/matrix/column.fsn +6 -0
  46. data/stdlib/matrix/determinant.fsn +16 -0
  47. data/stdlib/matrix/dimensions.fsn +5 -0
  48. data/stdlib/matrix/identity.fsn +6 -0
  49. data/stdlib/matrix/invert.fsn +26 -0
  50. data/stdlib/matrix/minor.fsn +15 -0
  51. data/stdlib/matrix/multiply.fsn +10 -0
  52. data/stdlib/matrix/negate.fsn +5 -0
  53. data/stdlib/matrix/product.fsn +11 -0
  54. data/stdlib/matrix/rotate.fsn +10 -0
  55. data/stdlib/matrix/row.fsn +6 -0
  56. data/stdlib/matrix/scale.fsn +6 -0
  57. data/stdlib/matrix/subtract.fsn +7 -0
  58. data/stdlib/matrix/sum.fsn +14 -0
  59. data/stdlib/matrix/transpose.fsn +5 -0
  60. data/stdlib/range.fsn +2 -2
  61. data/stdlib/safe.fsn +7 -0
  62. data/stdlib/sanitize.fsn +2 -3
  63. data/stdlib/toObject.fsn +7 -0
  64. data/stdlib/vector/Vector.fsn +7 -0
  65. data/stdlib/vector/add.fsn +6 -0
  66. data/stdlib/vector/cross.fsn +6 -0
  67. data/stdlib/vector/dot.fsn +6 -0
  68. data/stdlib/vector/norm.fsn +6 -0
  69. data/stdlib/vector/scale.fsn +5 -0
  70. data/stdlib/vector/subtract.fsn +6 -0
  71. data/stdlib/zip.fsn +6 -0
  72. metadata +37 -5
  73. data/stdlib/gt.fsn +0 -9
  74. data/stdlib/gte.fsn +0 -9
  75. data/stdlib/lt.fsn +0 -9
  76. data/stdlib/lte.fsn +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6571414d6f0a1f977f517652742c7b2766d7a17577f0276163856ff992bc7bc0
4
- data.tar.gz: 1f4f3a71cbfe0a18e18e91276b0ade75ea5c49a5b9a531097b739e8f04929b24
3
+ metadata.gz: be9bb991ddd9049b4dfbf56cd1b71395941e6836acb71272be3a72588cf80aea
4
+ data.tar.gz: e47c996224e5ffa767095f8be75c9c0728df566ba7463d351e065925e36cd5d7
5
5
  SHA512:
6
- metadata.gz: e4f9a32c372aea8633c1e66ab883015674d052ca83cccfab10c693f65ac805d90b4d768e1f78a4fb1a77745c158e6a39d56fa25ff9cae16cc29c765c22480ca7
7
- data.tar.gz: 7feb41d65c7be52ee4a92c820edd714b8ef28b6e0c5e9a6726d1a7c41963e9f3ff4d36b7cb89afacba1256cdc675c8a4a0c5c34d6ca5865e8106f4efe70fc4c7
6
+ metadata.gz: fa9b652f9f19d74561245a6948726ecef900ff4e40ba2fd3d8fda66f36cf68a0fc54fc7369ee78094794a083a6519c78751c4e0c8b5b3cd7cf1a99cfca55ff00
7
+ data.tar.gz: 66cc3557d141785c1075f3483c894140bb7d94c9fa8634a069713b84b00c72ae3852e395637d750e4da0486079fad96edd378677e8070b811f2500e39220763a
data/.mutant.yml ADDED
@@ -0,0 +1,24 @@
1
+ ---
2
+ requires:
3
+ - ./lib/fusion
4
+
5
+ integration:
6
+ name: rspec
7
+ # Optional per integration arguments. Proxies through to the integrations native CLI processor.
8
+ # Currently for minitest integration arguments are ignored.
9
+ # Note that when set this *OVERWRITES* the hardcoded defaults mutant provides. So should you
10
+ # provide your own arguments, for rspec you *need* to specify the `spec` directory at a minimum.
11
+ # Mutant is not compatible with many rspec options, take care you do not violate mutants invariants.
12
+ # During CLI executions you can manually add new integration arguments via `--integration-argument`,
13
+ # you have to specify it once per argument, so: `--integration-argument seed --integration-argument 0`
14
+ # would execute rspec with the `--seed 0` flag.
15
+ # Below shows an example configuring rspec to use a static seed from the config file.
16
+ arguments:
17
+ - --fail-fast # rspec integration default, keep this when specifying manual options!
18
+ - --options
19
+ - .rspec-ci
20
+ - spec # spec directory
21
+
22
+ matcher:
23
+ subjects:
24
+ - Fusion*
data/.simplecov ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Central SimpleCov configuration, shared by
4
+ # - the in-process suite (spec/spec_helper.rb)
5
+ # - the subprocesses spawned to drive the real binary (spec/simplecov_spawn.rb)
6
+ SimpleCov.configure do
7
+ enable_coverage :branch
8
+
9
+ add_group "Fusion", "lib/"
10
+ add_group "Tests", "spec/"
11
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ This project adheres to [Break Versioning](https://www.taoensso.com/break-versioning).
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Breaking
8
+
9
+ - The stdlib functions `@lt`, `@lte`, `@gt`, `@gte` have been turned into builtins
10
+ and moved into the `@OP` object. Migration:
11
+ - `@lt` -> `@OP.lt`
12
+ - `@lte` -> `@OP.lte`
13
+ - `@gt` -> `@OP.gt`
14
+ - `@gte` -> `@OP.gte`
15
+ Their errors now report `"origin": "builtin"`.
16
+ - The builtin `@toObject` has been moved to the stdlib. Its regular behavior stayed
17
+ the same, but its errors now report `"origin": "stdlib"`.
18
+ - The `expected` strings of many error payloads have been improved. Consumers that
19
+ matched the exact `expected` text need to update.
20
+ - The stdlib function `@concat` has been extended to be n-ary. Consumers that relied
21
+ on an error if the number of given strings wasn't equal to 2 need to update.
22
+
23
+ ### Non-breaking
24
+
25
+ - New syntax sugar has been introduced:
26
+ - `a < b` is `[a, b] | @OP.compare | @OP.lt`
27
+ - `a <= b` is `[a, b] | @OP.compare | @OP.lte`
28
+ - `a > b` is `[a, b] | @OP.compare | @OP.gt`
29
+ - `a >= b` is `[a, b] | @OP.compare | @OP.gte`
30
+ - New builtin predicates:
31
+ - `@Collection`: true for arrays and objects.
32
+ - New stdlib functions:
33
+ - `@entries`: an object's `[key, value]` pairs, the inverse of `@toObject`.
34
+ - `@zip`: turns a pair of equal-length arrays into an array of `[left, right]` pairs.
35
+ - `@safe`: the error catcher `(! => false, v => v)`
36
+ - New stdlib modules:
37
+ - `matrix/` (including the matrix operator set `@matrix/OP`)
38
+ - `vector/`
39
+
40
+ ## [0.0.1] - 2026-07-04
41
+
42
+ Release the first non-alpha version of Fusion.
data/README.md CHANGED
@@ -77,7 +77,8 @@ Refer to the [Documentation](docs/index.md) for further information.
77
77
 
78
78
  ## Development
79
79
 
80
- After checking out the repo, run `bin/setup` to install dependencies. Run `bin/console` for an interactive prompt that will allow you to experiment.
80
+ - Run `bundle install` to install dependencies.
81
+ - Run `bin/console` for an interactive prompt that will allow you to experiment.
81
82
 
82
83
  ### Tests
83
84
 
data/Rakefile CHANGED
@@ -12,6 +12,14 @@ namespace :spec do
12
12
  RSpec::Core::RakeTask.new(:all) do |task|
13
13
  task.rspec_opts = "--options .rspec-ci"
14
14
  end
15
+
16
+ desc "Run mutation testing"
17
+ task :mutant do
18
+ command = "bundle exec mutant run --usage opensource"
19
+ # On CI, let mutant use all available cores.
20
+ command += " --jobs 8" unless ENV["CI"]
21
+ sh command
22
+ end
15
23
  end
16
24
 
17
25
  task default: :spec
data/docs/lang/design.md CHANGED
@@ -475,7 +475,7 @@ Refines §2.9: same general shape, more orthogonal fields, field values easier t
475
475
  - 🧑 ✅ Split `location` into `origin` (where the operation is *defined*) and an optional `file` (the **innermost user-code file** on the call chain).
476
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
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).
478
+ - 🧑 ✅ `operation` now contains the failing operation's own **`@`-reference** (`@`, `@@`, `@range`, `@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
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
480
  - 🧑 ✅ For *access errors* the "key" appears only once:
481
481
  - `.name` carries the static key in `operation` and the object alone in `input`
@@ -851,8 +851,6 @@ error value can cross the boundary.
851
851
 
852
852
  **I/O modes** — how an error is marked crossing the boundary; `--input` and `--output` are independent:
853
853
 
854
- TODO: Under `-!` the input is the error payload, so empty stdin is a usage error (nothing to mark).
855
-
856
854
  - 🧑 ✅ Four modes: **unix** (asymmetric, Unix filter) and **bang** / **array** (`[0, value]` / `[1, payload]`), **object** (`{"value": _}` / `{"error": _}`).
857
855
  - 🧑 ✅ **unix**: input is `stdin` + `-!` flag, output is `value → stdout` + `exit 0` OR `error → stderr` + `exit 1`. stdin/stdout/stderr are always pure JSON.
858
856
  - 🧑 ✅ **bang**: shortest encoding, errors are simply a `!` prefix and thus not valid JSON.
@@ -1008,7 +1006,7 @@ TODO: Under `-!` the input is the error payload, so empty stdin is a usage error
1008
1006
  ### Decisions
1009
1007
 
1010
1008
  - 🧑 ✅ Bundle the most important arithmetic and logic operations together into a single `@OP` reference.
1011
- - 🧑 ✅ Make the `stdlib` as orthogonal as possible to `@OP`.
1009
+ - 🧑 ✅ Make the `stdlib` as orthogonal as possible to `@OP`. Addendum: this only applies to top-level stdlib functions. *Modules* like `matrix` are exempt.
1012
1010
  - 🧑 ✅ 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
1011
  - 🧑 ✅ Provide access to more advanced mathematical operations in `@math`.
1014
1012
  - 🧑 ✅ Where possible, builtins and stdlib functions are n-ary instead of binary.
@@ -1026,7 +1024,7 @@ TODO: Under `-!` the input is the error payload, so empty stdin is a usage error
1026
1024
  ### Cons
1027
1025
 
1028
1026
  - 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`.
1027
+ - The few top-level stdlib functions 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
1028
  - 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
1029
 
1032
1030
  ---
@@ -1058,7 +1056,58 @@ runtime node is the `[=]` setter.
1058
1056
  - 🤖 ⏪ Lex negative-number literals (JSON-style). Rewound: `a-3` would be ambiguous between the literal `-3` and subtraction.
1059
1057
  - 🤖 ❌ Desugar every `-x` to `x | @OP.negate`, dropping literal negatives. Rejected: `-5` should stay a plain literal, not an `@OP`-routed computation.
1060
1058
  - 🤖 ❌ 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.
1059
+ - 🤖 ❌ Make `&&` / `||` short-circuiting by desugaring to clause dispatch. Rejected: the boolean operators would no longer be reskinnable.
1061
1060
 
1062
1061
  ### Pros
1063
1062
 
1064
1063
  - 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`.
1064
+
1065
+ ### Cons
1066
+
1067
+ - By desugaring `==`/`&&`/`||` into an n-ary `@OP.equal`/`@OP.and`/`@OP.or`, the logic operators can never be short-circuiting. All inputs will be evaluated up front before getting passed to the `@OP` operator as an array. The first erroring input will propagate. The only way to create real short-circuiting is by using **clause bodies**: `a | (true => b, _ => false)`.
1068
+
1069
+ ---
1070
+
1071
+ ## 5.7 Comparison operators `<` `<=` `>=` `>`
1072
+
1073
+ Extends the §5.6 syntax sugar with comparisons.
1074
+
1075
+ ### Decisions
1076
+
1077
+ - 🧑 ✅ The builtins `@OP.lt` / `@OP.gt` / `@OP.lte` / `@OP.gte` map an `@OP.compare` result (`-1`/`0`/`1`) to a boolean. They pass `null` through to make partial orders possible.
1078
+ - 🧑 ✅ The inequality operator `a < b` is syntax sugar for the function chain `[a, b] | @OP.compare | @OP.lt`. Likewise `<=` / `>` / `>=` via `@OP.lte` / `@OP.gt` / `@OP.gte`.
1079
+ - 🤖 ✅ The comparisons sit at the ordering level with `??`: binary, left-associative, no folding.
1080
+
1081
+ ### Alternatives
1082
+
1083
+ - 🧑 ⏪ The comparisons previously were stdlib functions (`@lt`, `@gt`, `@lte`, `@gte`), because they can be built in Fusion natively. Superseded: as they are now target of syntax sugar, they have been moved into `@OP`.
1084
+ - 🧑 ❌ Four direct pair-comparing members (`[a, b] | @OP.lt`). Rejected: they are semantically coupled to `@OP.compare`. Keeping them consistent would have required to always override all of them together with `@OP.compare`.
1085
+
1086
+ ### Pros
1087
+
1088
+ - `@OP.compare` stays the single ordering primitive. The four operators are thin readers of its result. Shadowing `@OP.compare` keeps the semantics of all 5 syntax sugar operators `??`, `<`, `<=`, `>` and `>=` in sync.
1089
+
1090
+ ### Cons
1091
+
1092
+ - Comparisons don't chain: `a < b < c` is not `a < b && b < c`.
1093
+
1094
+ ---
1095
+
1096
+ ## 5.8 Stdlib modules
1097
+
1098
+ ### Decisions
1099
+
1100
+ - 🧑 ✅ Stdlib modules are directories (`@matrix/...`, `@vector/...`).
1101
+ - 🧑 ✅ Stdlib modules may ship shadowed default operators (`@matrix/OP`).
1102
+
1103
+ ### Alternatives
1104
+
1105
+ - 🧑 💭 Don't ship a preconstructed `@matrix/OP` and require users to manually assemble it themselves.
1106
+
1107
+ ### Pros
1108
+
1109
+ - Pointing a directory's `OP.fsn` at `@matrix/OP` switches the arithmetic operators `+`, `-`, `*`, `/` to matrix operations. A linear-system solver can be expressed by `([a, b] => /a * b)`.
1110
+
1111
+ ### Cons
1112
+
1113
+ - `@matrix/...` methods make frequent use of `@@`. This makes the `@matrix/...` module unable to compose with other definitions of the basic arithmetic operations (like complex numbers).
@@ -236,3 +236,44 @@ Marking the flag at construction (a constructor argument, not a later mutation)
236
236
  they serialize **leniently** (functions → `"<function>"`, non-finite → `"<Infinity>"`),
237
237
  which made the old `| @sanitize` in stdlib error payloads redundant; it was dropped,
238
238
  and `sanitize.fsn` remains as a standalone utility.
239
+
240
+ ## `Unreachable` is relative to the `exe/fusion` entry point
241
+
242
+ `raise Unreachable` asserts "no input can steer execution here **from `exe/fusion`**":
243
+ the lexer/parser only emit the known token/node classes and `Options.parse` only
244
+ produces the known use cases and modes, so from the binary the guarded arms are dead
245
+ — reaching one is by definition an interpreter bug, the one deliberate exception to
246
+ "no raw Ruby errors on stderr" (design §2.9). The claim says nothing about the more
247
+ granular Ruby seams: calling `Fusion::CLI` or `Fusion::Interpreter` directly *can*
248
+ reach the guards by handing over a bogus node or use case, which is exactly how the
249
+ specs prove they exist (`cli_spec.rb`, `error_kinds_spec.rb` "internal invariant
250
+ guards"). For the same reason the guards carry no `:nocov:` markers: the four with
251
+ such specs count as covered, the rest as honest misses.
252
+
253
+ ## Mutant test selection: the `mutant_expression` tags
254
+
255
+ Mutation testing (`.mutant.yml`, subjects `Fusion*`) kills a mutation by re-running
256
+ specs **inside the mutant worker's process** — the mutated method exists only in that
257
+ process's memory, never on disk. Two consequences shape how the specs are wired up:
258
+
259
+ - The subprocess-driven specs (`cli_subprocess_spec.rb`, `repl_pty_spec.rb`) can never
260
+ kill a mutation: they spawn `exe/fusion`, which loads the pristine code from disk.
261
+ Their prose describe titles keep them out of selection, deliberately.
262
+ - Mutant does not select tests by coverage. mutant-rspec parses the first word of each
263
+ example's description into an expression (`RSpec.describe Fusion::CLI` →
264
+ `Fusion::CLI`); a prose title is unaddressable. The `mutant_expression` metadata is
265
+ the explicit override, which is why every `expect_pipe` spec file is tagged
266
+ `mutant_expression: "Fusion::CLI*"` — the entry point the spec harness drives.
267
+
268
+ For each subject, mutant walks the subject's match expressions most-specific-first —
269
+ `Fusion::Interpreter#load_file`, then `Fusion::Interpreter*`, then `Fusion*` — and
270
+ **stops at the first level that matches any tests**. The tagged battery matches at the
271
+ `Fusion::CLI*` level (CLI subjects, alongside `cli_spec.rb`) and at the `Fusion*`
272
+ fallback (all other subjects).
273
+
274
+ **Caveat — preemption.** The fallback reaches the tagged specs only because no spec
275
+ file describes an inner scope directly. Adding a `RSpec.describe Fusion::Interpreter`
276
+ file would match at the `Fusion::Interpreter*` level and silently preempt the whole
277
+ tagged battery for every interpreter mutation — most kills would vanish. When adding
278
+ such a file, extend the language specs' tags to cover that scope too
279
+ (`mutant_expression` accepts an array).
@@ -26,7 +26,7 @@ Compute a boolean, then pipe it into a two-clause function:
26
26
 
27
27
  ```fusion
28
28
  (n =>
29
- [n, 0] | @OP.compare | @lt | (
29
+ (n < 0) | (
30
30
  true => "negative",
31
31
  false => "non-negative"
32
32
  )
@@ -126,6 +126,84 @@ array again to compare its two ends.
126
126
 
127
127
  ---
128
128
 
129
+ ## Short-circuit a chain of checks
130
+
131
+ `&&` and `||` are **eager**. `a && b` desugars to `[a, b] | @OP.and` — an array
132
+ piped into a function — so both operands are computed before the operator sees
133
+ them. The same goes for a hand-written conditions array `[a, b, c] | @OP.and`:
134
+ an array literal evaluates all of its elements, and if one of them is an error,
135
+ the error propagates out of the literal immediately. No piped form can keep its
136
+ input from being computed.
137
+
138
+ The one place where Fusion is lazy is a **clause body**: it evaluates only when
139
+ its clause's pattern matches. To run check B only if check A passed, put B in a
140
+ body that A's success selects. For boolean conditions:
141
+
142
+ ```fusion
143
+ a | (true => b, _ => false) # short-circuiting a && b
144
+ a | (true => true, _ => b) # short-circuiting a || b
145
+ ```
146
+
147
+ For example, `x | @Array | (true => x | @size > 0, _ => false)` safely tests
148
+ "a non-empty array": fed `5`, it yields `false` without ever piping the number
149
+ into `@size`.
150
+
151
+ In a guard, the same sequencing falls out of the stages pattern → predicate →
152
+ body, each of which runs only after the previous one succeeded. `@zip`
153
+ validates "a pair of equal-length arrays" like this:
154
+
155
+ ```fusion
156
+ [xs, ys] ? ([a ? @Array, b ? @Array] => a | @size == b | @size)
157
+ ```
158
+
159
+ The size comparison lives in the predicate's body, so it is computed only after
160
+ the pattern has established that both elements are arrays — `@size` can never
161
+ see a non-array. Sequencing matters because a predicate that *errors* does not
162
+ fall through to the next clause — the error propagates.
163
+
164
+ For a longer chain of checks you have two options. You can keep sequencing by
165
+ nesting another predicate between pattern and body — the checks stay lazy, and
166
+ a broken predicate still fails loudly. Or you can accept that every condition
167
+ is computed and route any error into "no match" by piping a conditions array
168
+ through `@OP.and | @safe`, one condition per line. `@matrix/sum` checks "an
169
+ array, non-empty, all matrices, all of equal dimensions" like this:
170
+
171
+ ```fusion
172
+ matrices ? (ms => [
173
+ ms | @Array,
174
+ ms | @size > 0,
175
+ {"c": ms, "f": @Matrix} | @all,
176
+ ms |: @dimensions | @OP.equal,
177
+ ] | @OP.and | @safe)
178
+ ```
179
+
180
+ Fed `[5]`, the last condition errors (`@dimensions` of the number `5`), the
181
+ error collapses the conditions array — an array literal propagates an error
182
+ element, and `@OP.and` passes an error through untouched — and `@safe` turns
183
+ it into `false`: the clause simply doesn't match, and the input falls through
184
+ to the function's own error clause. Two things to keep in mind:
185
+
186
+ - `@safe` is just the two-clause function `(! => false, v => v)`, and it needs
187
+ both clauses: the error clause alone would turn every *successful* condition
188
+ into `null` — a value that matches no clause yields `null` — and the guard
189
+ would go falsey for valid inputs too.
190
+ - Catching trades loudness for flatness. Errors-as-false means a genuinely
191
+ broken condition (a typo'd reference, a wrong shape) reads as "the guard is
192
+ false for every input" instead of crashing. Prefer sequenced clauses where
193
+ the checks order themselves structurally anyway.
194
+
195
+ Eager conditions are fine when they are safe on the already-matched input.
196
+ After `[x ? @Matrix, row ? @Integer, col ? @Integer]` has matched, bounds
197
+ checks like `row >= 0 && row < x | @size` cannot error, and the flat `&&`
198
+ chain reads best — the structural pattern is what bought that safety.
199
+
200
+ (`@all` and `@any` do short-circuit, but at the level of applying their
201
+ predicate to already-computed items: the first falsey/truthy item stops the
202
+ testing. That protects against a predicate erroring on a later item, not
203
+ against computing the items themselves.)
204
+
205
+ ---
206
+
129
207
  ## Shadow a built-in or stdlib function locally
130
208
 
131
209
  Because a sibling file wins over a built-in or the standard library, you can override
@@ -152,14 +230,21 @@ Only files in *that* directory are affected; everything else keeps the defaults.
152
230
  check whether a directory changed the operators, look for an `OP.fsn` — there is no
153
231
  other way to change them.
154
232
 
233
+ The standard library ships one ready-made reskin: an `OP.fsn` containing just
234
+ `@matrix/OP` gives the directory matrix arithmetic — `a + b` elementwise, `a * b`
235
+ the matrix product, `a / b` multiplication by the inverse, `%`/`//` always an
236
+ error — built on the helpers `@matrix/multiply`, `@matrix/determinant`,
237
+ `@matrix/scale`, and `@matrix/rotate`.
238
+
155
239
  ### Making a named derived helper follow your override
156
240
 
157
241
  Most stdlib helpers are deliberately **immune** to your override, so a reskin can't
158
242
  break them by accident: `@truthy`/`@falsey` decide truthiness by pattern matching,
159
- `@compact` drops nulls by pattern matching too, and the comparison helpers
160
- `@lt`/`@gt`/`@lte`/`@gte` interpret an `@OP.compare` *result* (you write
161
- `[a, b] | @OP.compare | @lt`, so the compare step already follows your override while
162
- the interpretation stays fixed).
243
+ and `@compact` drops nulls by pattern matching too. The comparison readers
244
+ `@OP.lt`/`@OP.gt`/`@OP.lte`/`@OP.gte` interpret an `@OP.compare` *result*, and both
245
+ steps of `a < b` (= `[a, b] | @OP.compare | @OP.lt`) resolve through `@OP` so
246
+ overriding `compare` alone already reskins the comparisons, and an `OP.fsn` that
247
+ spreads `...@@` keeps the original readers.
163
248
 
164
249
  One helper still calls `@OP` internally — `@range` uses `@OP.sum` and `@OP.compare` —
165
250
  and, like `@`-names everywhere, it resolves `@OP` in *its own* directory (the stdlib),
@@ -48,7 +48,7 @@ Precedence, tightest to loosest:
48
48
  `|+` (reduce). Left-associative (`a | f | g` ≡ `(a | f) | g`).
49
49
  5. Multiplicative: `*`, `/`, `%`, `//`. Left-associative.
50
50
  6. Additive: `+`, `-`. Left-associative.
51
- 7. Ordering: `??`. Left-associative.
51
+ 7. Ordering: `??` and the comparisons `<`, `<=`, `>`, `>=`. Binary, left-associative.
52
52
  8. Equality: `==`.
53
53
  9. Logical and: `&&`.
54
54
  10. Logical or: `||`.
@@ -105,7 +105,7 @@ expr = logical_or ;
105
105
  logical_or = logical_and { "||" logical_and } ;
106
106
  logical_and = equality { "&&" equality } ;
107
107
  equality = ordering { "==" ordering } ;
108
- ordering = additive { "??" additive } ;
108
+ ordering = additive { ( "??" | "<" | "<=" | ">" | ">=" ) additive } ;
109
109
  additive = multiplicative { ( "+" | "-" ) multiplicative } ;
110
110
  multiplicative = pipe { ( "*" | "/" | "%" | "//" ) pipe } ;
111
111
  pipe = unary { ( "|" | "|:" | "|?" | "|+" ) unary } ;
@@ -194,6 +194,7 @@ member (§7.6), or, for the map-pipes, into a stdlib call.
194
194
  a % b → [a, b] | @OP.modulo
195
195
  a // b → [a, b] | @OP.quotient
196
196
  a ?? b → [a, b] | @OP.compare
197
+ a < b → [a, b] | @OP.compare | @OP.lt (likewise <= / > / >= via @OP.lte / @OP.gt / @OP.gte)
197
198
  a == b == c → [a, b, c] | @OP.equal
198
199
  a && b && c → [a, b, c] | @OP.and
199
200
  a || b || c → [a, b, c] | @OP.or
@@ -209,15 +210,17 @@ Folding and associativity:
209
210
  - A maximal run of `*`/`/` folds into one `@OP.product`; each `/` term is inverted via
210
211
  `@OP.invert` (never a literal, so `1/x` stays a float and `/0` stays a runtime error).
211
212
  - 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`.
213
+ - `%`, `//`, `??`, `<`, `<=`, `>`, `>=` are binary and left-associative; they sit at
214
+ their level and break a fold run: `a * b % c` is `(a * b) % c`; `a ?? b == 0` is
215
+ `(a ?? b) == 0`. Comparisons do not chain: `a < b < c` is `(a < b) < c`, which
216
+ errors at runtime (a boolean is not comparable).
214
217
 
215
218
  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`.
219
+ computed value onward, parenthesize it: `(a + b)|@f`, `(a < b)|@f`.
217
220
 
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
+ Comparisons: `a == b` is equality; `a < b` (and `<=` / `>` / `>=`) orders two numbers
222
+ or two strings. `??` exposes the underlying `-1`/`0`/`1` ordering that the comparison
223
+ desugaring reads through `@OP.lt` / `@OP.lte` / `@OP.gt` / `@OP.gte` (§7.2).
221
224
 
222
225
  ---
223
226
 
@@ -379,7 +382,10 @@ particular built-ins:
379
382
  that clause must match the specific error received. An error of a shape no
380
383
  clause catches propagates unchanged.
381
384
  - **Built-in and stdlib operations (`@math.divide`, `@OP.sum`, `@Integer`, …) all
382
- propagate** their input error without examining it. To inspect or compare an
385
+ propagate** their input error without examining it. The one deliberate
386
+ exception is the stdlib catcher `@safe` — an ordinary two-clause function
387
+ `(! => false, v => v)` that catches any error into `false` and passes regular
388
+ values through. To inspect or compare an
383
389
  error's payload, you must catch it first and operate on the extracted payload:
384
390
  `!42 | (!a => a) | @Integer` returns `true` (the payload `42` *is* an integer);
385
391
  `!42 | @Integer` returns `!42` (the predicate doesn't handle the error,
@@ -476,8 +482,7 @@ below are the built-in names; write `@` before them to use them. **Operations**
476
482
  input that is not of the queried type (they never return `!`).
477
483
 
478
484
  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.
485
+ object (§7.6); a directory reskins them by placing an `OP.fsn`.
481
486
 
482
487
  ### 7.1 Arithmetic
483
488
 
@@ -492,22 +497,21 @@ division and the other numeric functions live in `@math` (§7.6a): `@math.divide
492
497
 
493
498
  Equality is `@OP.equal` (deep structural equality of a pair; §7.6) — used directly,
494
499
  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.
500
+ comparison members **interpret that result** and are applied *after* it:
501
+
502
+ | Member | `-1` | `0` | `1` | `null` |
503
+ | -------- | ------ | ------ | ------ | ------ |
504
+ | `OP.lt` | `true` | `false`| `false`| `null` |
505
+ | `OP.gt` | `false`| `false`| `true` | `null` |
506
+ | `OP.lte` | `true` | `true` | `false`| `null` |
507
+ | `OP.gte` | `false`| `true` | `true` | `null` |
508
+
509
+ `a < b` desugars to exactly this pipeline: `[a, b] | @OP.compare | @OP.lt` (likewise
510
+ `<=` / `>` / `>=`; equality is `a == b` — see §2.7). Both steps resolve `@OP` per
511
+ directory, so an override reskins the ordering and its reading together. A partial
512
+ order whose `compare` returns `null` for incomparable operands passes that `null`
513
+ straight through; any other input is an `argument_error`. A type mismatch surfaces
514
+ as `@OP.compare`'s own error, before the reader runs.
511
515
 
512
516
  ### 7.3 Boolean
513
517
 
@@ -530,11 +534,13 @@ everything except `false`/`null`, and `@falsey` is its complement.
530
534
  | `parseNumber` | string | integer or float; `!` if not numeric |
531
535
  | `keys` | object | array of key strings |
532
536
  | `values` | object | array of values |
533
- | `toObject` | `[[string-key, value], …]` | object built from entries; later duplicate keys win |
534
537
 
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
+ `concat` (array of strings, any length → concatenation) and `chars` (string →
539
+ array of its characters) are **standard-library** functions built on `join` /
540
+ `split`, not built-ins. So are `entries` (object → array of its `[key, value]` entries, in
541
+ insertion order; built on `keys`) and `toObject` (`[[string-key, value], …]` →
542
+ object, later duplicate keys win; built on the `[=]` setter). They are inverses:
543
+ `obj | @entries | @toObject` is `obj`.
538
544
 
539
545
  Indexed read (`x[k]`) and write (`x[k = v]`) are **core syntax**, not built-ins — there
540
546
  is no `@get`/`@set`; see §8.
@@ -554,6 +560,7 @@ functions provides a runtime type system.
554
560
  | `String` | strings | |
555
561
  | `Array` | arrays | `[_]` |
556
562
  | `Object` | objects | `{_}` |
563
+ | `Collection`| arrays and objects | |
557
564
  | `Function` | any function (builtin, stdlib, user) | |
558
565
  | `NonFinite` | "Infinity", "-Infinity", "NaN" | |
559
566
 
@@ -570,9 +577,10 @@ elements); `compare` reports an ordering. The infix operators (§2.7) desugar to
570
577
 
571
578
  `@OP` is **shadowable per directory**: place an `OP.fsn` sibling that overrides members
572
579
  (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).
580
+ ternary logic — for that directory only. The comparison members (§7.2: `lt`, `gt`,
581
+ `lte`, `gte`) interpret an `@OP.compare` *result* rather than comparing values
582
+ themselves, so an override of `compare` alone already reskins `a < b` (see the
583
+ how-to guide).
576
584
 
577
585
  | Member | Input | Result |
578
586
  | ------------- | ---------------------------------------- | -------------------------------------------------------- |
@@ -584,6 +592,10 @@ caller pipes through `@OP.compare` (see the how-to guide).
584
592
  | `OP.modulo` | `[integer, integer]` | integer remainder; `!` on a non-integer or a `0` divisor |
585
593
  | `OP.equal` | array (any element types) | deep equality: `true` iff every element equals the first |
586
594
  | `OP.compare` | `[number, number]` or `[string, string]` | `-1` / `0` / `1` (first smaller / equal / larger) |
595
+ | `OP.lt` | a compare result (`-1`/`0`/`1`/`null`) | `true` for `-1`, else `false`; `null` passes through |
596
+ | `OP.gt` | a compare result | `true` for `1` |
597
+ | `OP.lte` | a compare result | `true` for `-1` / `0` |
598
+ | `OP.gte` | a compare result | `true` for `0` / `1` |
587
599
  | `OP.and` | array | `true` if every element is truthy (`true` for `[]`) |
588
600
  | `OP.or` | array | `true` if any element is truthy (`false` for `[]`) |
589
601
  | `OP.not` | `_` | `true` if the operand is falsey |
@@ -717,9 +729,8 @@ Two built-ins are special in how they resolve:
717
729
  - **`@ENV`** resolves (at step 2) to a fresh object of environment variables.
718
730
  - **`@load`** resolves (at step 2) to a function that loads a file by a **verbatim**
719
731
  filename string — no `.fsn` is appended — relative to the referencing directory,
720
- returning that file's value. If the argument is not a string, or the file does not
721
- exist, the result is an error (`{"kind":"load_bad_arg",...}` or
722
- `{"kind":"file_not_found","path":...}` respectively). This is the only way to load
732
+ returning that file's value. A non-string argument is an `argument_error`; a
733
+ missing file is a `reference_error` (`file not found`). This is the only way to load
723
734
  a file whose name is computed at runtime or is not a plain identifier (e.g.
724
735
  `"data.config.fsn"`).
725
736
 
@@ -127,7 +127,7 @@ number's absolute value:
127
127
 
128
128
  ```fusion
129
129
  (n =>
130
- [n, 0] | @OP.compare | @lt | (
130
+ [n, 0] | @OP.compare | @OP.lt | (
131
131
  true => n | @OP.negate,
132
132
  false => n
133
133
  )
@@ -142,9 +142,10 @@ echo '5' | fusion lesson.fsn # => 5
142
142
  ```
143
143
 
144
144
  Read the middle line carefully: `[n, 0] | @OP.compare` orders the pair as `-1`/`0`/`1`,
145
- and `@lt` turns that into `true`/`false`. That boolean is then piped into a *second,
146
- inline function* whose two clauses are the two branches. **An `if` is just a function
147
- with a `true` clause and a `false` clause.**
145
+ and `@OP.lt` turns that into `true`/`false` (the infix `n < 0` is sugar for exactly
146
+ this pipeline). That boolean is then piped into a *second, inline function* whose two
147
+ clauses are the two branches. **An `if` is just a function with a `true` clause and a
148
+ `false` clause.**
148
149
 
149
150
  Note: you don't need to restrict yourself to the two values `true` and `false` as
150
151
  an intermediate result. Don't use it purely as an `if / else`. Use it like a `case`
@@ -216,7 +217,7 @@ Create a function that sorts a pair of values:
216
217
 
217
218
  ```fusion
218
219
  (
219
- [a, b] ? (p => p | @OP.compare | @lt) => [a, b],
220
+ [a, b] ? (p => p | @OP.compare | @OP.lt) => [a, b],
220
221
  [a, b] => [b, a]
221
222
  )
222
223
  ```
data/examples/double.fsn CHANGED
@@ -1,4 +1,7 @@
1
+ # Double every number in an array or object.
2
+ # Equivalent to: (c ? @Collection => c |: (n => 2 * n))
1
3
  (
2
4
  [] => [],
3
- [first, ...rest] => [2 * first, ...rest|@]
5
+ [first, ...rest] => [2 * first, ...rest|@],
6
+ obj ? @Object => obj | @entries |: ([key, value] => [key, 2 * value]) | @toObject
4
7
  )
@@ -1,6 +1,9 @@
1
1
  # Factorial: n! for a non-negative integer.
2
2
  (
3
- _ ? (i ? @Integer => (i ?? 0)|@lt, _ => true) => !"Only non-negative integers, please!",
4
- 0 => 1,
5
- n => n * (n - 1)|@
3
+ n ? (i ? @Integer => i >= 0) =>
4
+ n | (
5
+ 0 => 1,
6
+ n => n * (n - 1)|@
7
+ ),
8
+ _ => !"Only non-negative integers, please!"
6
9
  )
data/examples/gcd.fsn ADDED
@@ -0,0 +1,9 @@
1
+ # Greatest common divisor of a pair of non-negative integers (Euclidean algorithm).
2
+ (
3
+ pair ? ([a ? @Integer, b ? @Integer] => a >= 0 && b >= 0) =>
4
+ pair | (
5
+ [a, 0] => a,
6
+ [a, b] => [b, a % b] | @
7
+ ),
8
+ _ => !"Only pairs of non-negative integers, please!"
9
+ )
@@ -0,0 +1,2 @@
1
+ # This directory computes with matrices: the operators are the matrix reskin.
2
+ @matrix/OP
@@ -0,0 +1,2 @@
1
+ # Average two matrices: elementwise sum, then halve each entry.
2
+ ([a, b] => [0.5, a + b] | @matrix/scale)
@@ -0,0 +1,2 @@
1
+ # Solve the linear system a * x = b for x (b a column matrix): x = a⁻¹ * b.
2
+ ([a, b] => /a * b)