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.
- checksums.yaml +4 -4
- data/.mutant.yml +24 -0
- data/.simplecov +11 -0
- data/CHANGELOG.md +42 -0
- data/README.md +2 -1
- data/Rakefile +8 -0
- data/docs/lang/design.md +54 -5
- data/docs/lang/implementation.md +41 -0
- data/docs/user/how-to-guides.md +90 -5
- data/docs/user/reference.md +48 -37
- data/docs/user/tutorial.md +6 -5
- data/examples/double.fsn +4 -1
- data/examples/factorial.fsn +6 -3
- data/examples/gcd.fsn +9 -0
- data/examples/matrix/OP.fsn +2 -0
- data/examples/matrix/average.fsn +2 -0
- data/examples/matrix/solve.fsn +2 -0
- data/exe/fusion +2 -2
- data/lib/fusion/ast.rb +76 -28
- data/lib/fusion/atom.rb +1 -1
- data/lib/fusion/cli/decoder.rb +6 -6
- data/lib/fusion/cli/encoder.rb +2 -2
- data/lib/fusion/cli/options.rb +11 -8
- data/lib/fusion/cli/parser.rb +1 -1
- data/lib/fusion/cli/repl.rb +6 -6
- data/lib/fusion/cli/serializer.rb +6 -7
- data/lib/fusion/cli.rb +1 -1
- data/lib/fusion/interpreter/builtins.rb +51 -22
- data/lib/fusion/interpreter/thunk.rb +2 -2
- data/lib/fusion/interpreter.rb +31 -23
- data/lib/fusion/lexer.rb +62 -40
- data/lib/fusion/parser.rb +72 -35
- data/lib/fusion/version.rb +3 -1
- data/lib/fusion.rb +0 -1
- data/stdlib/all.fsn +2 -2
- data/stdlib/any.fsn +3 -3
- data/stdlib/compact.fsn +2 -3
- data/stdlib/concat.fsn +4 -3
- data/stdlib/entries.fsn +6 -0
- data/stdlib/filter.fsn +3 -3
- data/stdlib/map.fsn +3 -2
- data/stdlib/matrix/Matrix.fsn +8 -0
- data/stdlib/matrix/OP.fsn +18 -0
- data/stdlib/matrix/add.fsn +7 -0
- data/stdlib/matrix/column.fsn +6 -0
- data/stdlib/matrix/determinant.fsn +16 -0
- data/stdlib/matrix/dimensions.fsn +5 -0
- data/stdlib/matrix/identity.fsn +6 -0
- data/stdlib/matrix/invert.fsn +26 -0
- data/stdlib/matrix/minor.fsn +15 -0
- data/stdlib/matrix/multiply.fsn +10 -0
- data/stdlib/matrix/negate.fsn +5 -0
- data/stdlib/matrix/product.fsn +11 -0
- data/stdlib/matrix/rotate.fsn +10 -0
- data/stdlib/matrix/row.fsn +6 -0
- data/stdlib/matrix/scale.fsn +6 -0
- data/stdlib/matrix/subtract.fsn +7 -0
- data/stdlib/matrix/sum.fsn +14 -0
- data/stdlib/matrix/transpose.fsn +5 -0
- data/stdlib/range.fsn +2 -2
- data/stdlib/safe.fsn +7 -0
- data/stdlib/sanitize.fsn +2 -3
- data/stdlib/toObject.fsn +7 -0
- data/stdlib/vector/Vector.fsn +7 -0
- data/stdlib/vector/add.fsn +6 -0
- data/stdlib/vector/cross.fsn +6 -0
- data/stdlib/vector/dot.fsn +6 -0
- data/stdlib/vector/norm.fsn +6 -0
- data/stdlib/vector/scale.fsn +5 -0
- data/stdlib/vector/subtract.fsn +6 -0
- data/stdlib/zip.fsn +6 -0
- metadata +37 -5
- data/stdlib/gt.fsn +0 -9
- data/stdlib/gte.fsn +0 -9
- data/stdlib/lt.fsn +0 -9
- data/stdlib/lte.fsn +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be9bb991ddd9049b4dfbf56cd1b71395941e6836acb71272be3a72588cf80aea
|
|
4
|
+
data.tar.gz: e47c996224e5ffa767095f8be75c9c0728df566ba7463d351e065925e36cd5d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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** (`@`, `@@`, `@
|
|
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
|
|
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).
|
data/docs/lang/implementation.md
CHANGED
|
@@ -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).
|
data/docs/user/how-to-guides.md
CHANGED
|
@@ -26,7 +26,7 @@ Compute a boolean, then pipe it into a two-clause function:
|
|
|
26
26
|
|
|
27
27
|
```fusion
|
|
28
28
|
(n =>
|
|
29
|
-
|
|
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
|
|
160
|
-
`@lt`/`@gt`/`@lte`/`@gte` interpret an `@OP.compare` *result
|
|
161
|
-
`[a, b] | @OP.compare | @lt
|
|
162
|
-
the
|
|
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),
|
data/docs/user/reference.md
CHANGED
|
@@ -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:
|
|
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
|
-
- `%`, `//`,
|
|
213
|
-
fold run: `a * b % c` is `(a * b) % c`; `a ?? b == 0` is
|
|
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
|
|
219
|
+
computed value onward, parenthesize it: `(a + b)|@f`, `(a < b)|@f`.
|
|
217
220
|
|
|
218
|
-
Comparisons: `a == b` is equality
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
Comparisons: `a == b` is equality; `a < b` (and `<=` / `>` / `>=`) orders two numbers
|
|
222
|
+
or two strings. `??` exposes the underlying `-1`/`0`/`1` ordering that the comparison
|
|
223
|
+
desugaring reads through `@OP.lt` / `@OP.lte` / `@OP.gt` / `@OP.gte` (§7.2).
|
|
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.
|
|
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`.
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
|
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
|
-
|
|
505
|
-
is `a == b` — see §2.7).
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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` (
|
|
536
|
-
characters) are **standard-library** functions built on `join` /
|
|
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
|
|
574
|
-
interpret an `@OP.compare` result
|
|
575
|
-
|
|
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.
|
|
721
|
-
|
|
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
|
|
data/docs/user/tutorial.md
CHANGED
|
@@ -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
|
|
146
|
-
|
|
147
|
-
with a `true` clause and a
|
|
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
|
)
|
data/examples/factorial.fsn
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Factorial: n! for a non-negative integer.
|
|
2
2
|
(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
)
|