fusion-lang 0.0.1.alpha2 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +24 -0
- data/.simplecov +11 -0
- data/CHANGELOG.md +42 -0
- data/README.md +11 -9
- data/Rakefile +8 -0
- data/docs/lang/design.md +289 -51
- data/docs/lang/implementation.md +279 -0
- data/docs/lang/roadmap.md +20 -36
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +145 -15
- data/docs/user/reference.md +365 -140
- data/docs/user/tutorial.md +22 -19
- data/examples/double.fsn +4 -1
- data/examples/factorial.fsn +6 -3
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/gcd.fsn +9 -0
- data/examples/json_test.fsn +4 -0
- data/examples/matrix/OP.fsn +2 -0
- data/examples/matrix/average.fsn +2 -0
- data/examples/matrix/solve.fsn +2 -0
- data/examples/palindrome.fsn +1 -1
- data/exe/fusion +12 -12
- data/lib/fusion/ast.rb +76 -27
- data/lib/fusion/atom.rb +1 -1
- data/lib/fusion/cli/decoder.rb +13 -8
- data/lib/fusion/cli/encoder.rb +2 -2
- data/lib/fusion/cli/options.rb +134 -61
- data/lib/fusion/cli/parser.rb +4 -4
- data/lib/fusion/cli/repl.rb +32 -27
- data/lib/fusion/cli/serializer.rb +11 -11
- data/lib/fusion/cli.rb +120 -49
- data/lib/fusion/interpreter/builtins.rb +298 -160
- data/lib/fusion/interpreter/env.rb +42 -12
- data/lib/fusion/interpreter/error_val.rb +42 -20
- data/lib/fusion/interpreter/thunk.rb +53 -0
- data/lib/fusion/interpreter.rb +263 -98
- data/lib/fusion/lexer.rb +125 -37
- data/lib/fusion/parser.rb +245 -70
- data/lib/fusion/version.rb +3 -1
- data/lib/fusion.rb +0 -1
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +5 -0
- data/stdlib/concat.fsn +6 -0
- data/stdlib/entries.fsn +6 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/map.fsn +7 -4
- 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/reduce.fsn +8 -0
- data/stdlib/safe.fsn +7 -0
- data/stdlib/sanitize.fsn +2 -3
- data/stdlib/toObject.fsn +7 -0
- data/stdlib/truthy.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 +50 -4
- data/lib/fusion/interpreter/file_thunk.rb +0 -39
- data/stdlib/mapValues.fsn +0 -5
- data/stdlib/math/square.fsn +0 -4
data/docs/user/tutorial.md
CHANGED
|
@@ -16,7 +16,7 @@ Fusion programs read JSON on standard input and write JSON on standard output. L
|
|
|
16
16
|
create our first program. Create a file `lesson.fsn` containing exactly:
|
|
17
17
|
|
|
18
18
|
```fusion
|
|
19
|
-
(n => [n, 2] | @
|
|
19
|
+
(n => [n, 2] | @OP.product)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Now run it:
|
|
@@ -32,10 +32,11 @@ being told:
|
|
|
32
32
|
statements. The file *is* the program.
|
|
33
33
|
- The input `21` was piped *into* the function. That is what `|` means: **`value |
|
|
34
34
|
function`** applies the function to the value.
|
|
35
|
-
- `[n, 2] | @
|
|
36
|
-
`@
|
|
37
|
-
into a
|
|
38
|
-
— `@
|
|
35
|
+
- `[n, 2] | @OP.product` built a two-element array and piped it into `@OP.product`, a
|
|
36
|
+
member of the built-in operator object `@OP`. Fusion has no `*` operator (yet);
|
|
37
|
+
arithmetic is done by piping a pair into a built-in. **Built-ins are reached with an
|
|
38
|
+
`@` prefix**, just like files — `@OP.product`, `@OP.sum`, and so on. (You'll see why
|
|
39
|
+
`@` is used for both in Step 8.)
|
|
39
40
|
|
|
40
41
|
Note: on the right side of `=>` you can use regular parentheses `()` to group
|
|
41
42
|
expressions and influence execution order.
|
|
@@ -126,8 +127,8 @@ number's absolute value:
|
|
|
126
127
|
|
|
127
128
|
```fusion
|
|
128
129
|
(n =>
|
|
129
|
-
[n, 0] | @
|
|
130
|
-
true =>
|
|
130
|
+
[n, 0] | @OP.compare | @OP.lt | (
|
|
131
|
+
true => n | @OP.negate,
|
|
131
132
|
false => n
|
|
132
133
|
)
|
|
133
134
|
)
|
|
@@ -140,9 +141,11 @@ echo '-5' | fusion lesson.fsn # => 5
|
|
|
140
141
|
echo '5' | fusion lesson.fsn # => 5
|
|
141
142
|
```
|
|
142
143
|
|
|
143
|
-
Read the middle line carefully: `[n, 0] | @
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
Read the middle line carefully: `[n, 0] | @OP.compare` orders the pair as `-1`/`0`/`1`,
|
|
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.**
|
|
146
149
|
|
|
147
150
|
Note: you don't need to restrict yourself to the two values `true` and `false` as
|
|
148
151
|
an intermediate result. Don't use it purely as an `if / else`. Use it like a `case`
|
|
@@ -161,7 +164,7 @@ To make recursion easier, `@` always means *the current file*. Create a new file
|
|
|
161
164
|
```fusion
|
|
162
165
|
(
|
|
163
166
|
[] => 0,
|
|
164
|
-
[x, ...rest] => [x, rest | @ ] | @
|
|
167
|
+
[x, ...rest] => [x, rest | @ ] | @OP.sum
|
|
165
168
|
)
|
|
166
169
|
```
|
|
167
170
|
|
|
@@ -198,7 +201,7 @@ Let's compute the factorial:
|
|
|
198
201
|
```fusion
|
|
199
202
|
(
|
|
200
203
|
0 => 1,
|
|
201
|
-
n ? @Integer => [n, [n, 1] | @
|
|
204
|
+
n ? @Integer => [n, [n, -1] | @OP.sum | @] | @OP.product
|
|
202
205
|
)
|
|
203
206
|
```
|
|
204
207
|
|
|
@@ -214,7 +217,7 @@ Create a function that sorts a pair of values:
|
|
|
214
217
|
|
|
215
218
|
```fusion
|
|
216
219
|
(
|
|
217
|
-
[a, b] ? @
|
|
220
|
+
[a, b] ? (p => p | @OP.compare | @OP.lt) => [a, b],
|
|
218
221
|
[a, b] => [b, a]
|
|
219
222
|
)
|
|
220
223
|
```
|
|
@@ -232,7 +235,7 @@ standard library and are reached with a plain `@name` — the same `@map` you'd
|
|
|
232
235
|
a sibling file. The classic `map` is in the standard library. Create `doubler.fsn`:
|
|
233
236
|
|
|
234
237
|
```fusion
|
|
235
|
-
(xs => {"f": (n => [n, 2] | @
|
|
238
|
+
(xs => {"f": (n => [n, 2] | @OP.product), "c": xs} | @map)
|
|
236
239
|
```
|
|
237
240
|
|
|
238
241
|
```sh
|
|
@@ -240,7 +243,7 @@ echo '[1, 2, 3]' | fusion doubler.fsn # => [2, 4, 6]
|
|
|
240
243
|
```
|
|
241
244
|
|
|
242
245
|
Because every Fusion function takes exactly one argument, `map` takes an *object*
|
|
243
|
-
bundling the function `f` and the
|
|
246
|
+
bundling the function `f` and the collection `c`. You just passed a function as a value
|
|
244
247
|
nested within an object — functions are values like any other.
|
|
245
248
|
|
|
246
249
|
Now the payoff for using `@` everywhere. A bare `@name` is resolved in the following
|
|
@@ -249,7 +252,7 @@ order:
|
|
|
249
252
|
2. A **built-in** called `name`.
|
|
250
253
|
3. A **standard-library** file `name.fsn`.
|
|
251
254
|
|
|
252
|
-
The first match wins. So `@
|
|
255
|
+
The first match wins. So `@OP` finds the built-in and `@map` falls through to
|
|
253
256
|
the standard library. And if *you* put a `map.fsn` next to your program, *your* `map`
|
|
254
257
|
shadows the standard one — but only for files in that directory.
|
|
255
258
|
|
|
@@ -264,7 +267,7 @@ So far you have written programs that succeed. What happens when something goes
|
|
|
264
267
|
wrong? Try dividing by zero. Save as `boom.fsn`:
|
|
265
268
|
|
|
266
269
|
```fusion
|
|
267
|
-
(n => [n, 0] | @divide)
|
|
270
|
+
(n => [n, 0] | @math.divide)
|
|
268
271
|
```
|
|
269
272
|
|
|
270
273
|
```sh
|
|
@@ -307,7 +310,7 @@ Catching is done with an error pattern:
|
|
|
307
310
|
Here's `safeDivide` that returns `null` instead of failing:
|
|
308
311
|
|
|
309
312
|
```fusion
|
|
310
|
-
(p => p | @divide | (! => null, n => n))
|
|
313
|
+
(p => p | @math.divide | (! => null, n => n))
|
|
311
314
|
```
|
|
312
315
|
|
|
313
316
|
Run `echo '[10, 0]' | fusion safeDivide.fsn` and you get `null` rather than an
|
|
@@ -331,7 +334,7 @@ In about an hour you have used every major feature of the language:
|
|
|
331
334
|
- `if` is a function matching `true`/`false`; a loop is recursion via `@` (a bare
|
|
332
335
|
`@` means "this file").
|
|
333
336
|
- `?` attaches a predicate to refine a match, and predicates double as types.
|
|
334
|
-
- Everything reachable lives in one `@` namespace: built-ins (`@
|
|
337
|
+
- Everything reachable lives in one `@` namespace: built-ins (`@OP`, `@Integer`),
|
|
335
338
|
the standard library (`@map`), sibling files (`@helper`), and the current file
|
|
336
339
|
(`@`). A bare `@name` checks sibling → built-in → standard library, so you can
|
|
337
340
|
locally shadow a built-in or stdlib function per directory.
|
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] => [
|
|
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/fizzbuzz.fsn
CHANGED
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
|
+
)
|
data/examples/palindrome.fsn
CHANGED
data/exe/fusion
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Usage:
|
|
7
7
|
# echo '[1,2,3]' | fusion path/to/main.fsn
|
|
8
|
-
# fusion path/to/main.fsn
|
|
9
|
-
# fusion -e '(n => [n,2] | @multiply)'
|
|
10
|
-
# fusion --stream path/to/main.fsn
|
|
11
|
-
# fusion --repl
|
|
8
|
+
# fusion path/to/main.fsn # no input: the file's value is the result
|
|
9
|
+
# echo '21' | fusion -e '(n => [n,2] | @multiply)' # inline program
|
|
10
|
+
# fusion --stream path/to/main.fsn # NDJSON in, NDJSON out
|
|
11
|
+
# fusion --repl # interactive expressions/statements
|
|
12
12
|
#
|
|
13
13
|
# Run `fusion --help` for the input/output modes (--input / --output / -!).
|
|
14
14
|
|
|
@@ -19,12 +19,12 @@ if ARGV.intersect?(["--help", "-h"])
|
|
|
19
19
|
exit 0
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
Fusion::CLI::Options.parse(ARGV)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
22
|
+
begin
|
|
23
|
+
options = Fusion::CLI::Options.parse(ARGV)
|
|
24
|
+
Fusion::CLI.run(options)
|
|
25
|
+
rescue Fusion::CLI::Options::UsageError => e
|
|
26
|
+
# A command-line misuse is plain text on stderr, not a payloaded Fusion error,
|
|
27
|
+
# whether caught while parsing options or early during execution, e.g. by
|
|
28
|
+
# detecting empty stdin plus "-!".
|
|
29
|
+
abort("fusion: #{e.message}\n\n#{Fusion::CLI::Options::USAGE}")
|
|
28
30
|
end
|
|
29
|
-
|
|
30
|
-
Fusion::CLI.run(options)
|
data/lib/fusion/ast.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Fusion
|
|
|
14
14
|
# A syntactic identifier: a bound/looked-up name, a `.key`, or a `...rest`
|
|
15
15
|
# binder. Mirrors the lexer's ident rule (Lexer#ident_start? / #ident_part?).
|
|
16
16
|
# Object *keys* are arbitrary strings, not identifiers, so they stay `String`.
|
|
17
|
-
|
|
17
|
+
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
18
18
|
|
|
19
19
|
# Expression and Pattern nodes each form a closed family, declared up front
|
|
20
20
|
# (empty) so a member of either may reference the other. Each module is
|
|
@@ -28,30 +28,66 @@ module Fusion
|
|
|
28
28
|
# Auxiliary typed parts: the elements a collection node holds. NOT themselves
|
|
29
29
|
# expressions or patterns, so they live outside the marker families and never
|
|
30
30
|
# satisfy an `Expression`/`Pattern` field.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
|
|
32
|
+
# An array element
|
|
33
|
+
ArrayItem = TypedData.define(value: Expression)
|
|
34
|
+
|
|
35
|
+
# `...expr` inside an array
|
|
36
|
+
ArraySpread = TypedData.define(value: Expression)
|
|
37
|
+
|
|
38
|
+
# `"k": expr` inside an object
|
|
39
|
+
KeyValuePair = TypedData.define(key: String, value: Expression)
|
|
40
|
+
|
|
41
|
+
# `...expr` inside an object
|
|
42
|
+
ObjectSpread = TypedData.define(value: Expression)
|
|
43
|
+
|
|
44
|
+
# One `pattern => body` of a function
|
|
45
|
+
Clause = TypedData.define(pattern: Pattern, body: Expression)
|
|
46
|
+
|
|
47
|
+
# A sub-pattern of an array pattern
|
|
48
|
+
PatternItem = TypedData.define(pattern: Pattern)
|
|
49
|
+
|
|
50
|
+
# `"k": pat` inside an object pattern
|
|
51
|
+
PatternPair = TypedData.define(key: String, pattern: Pattern)
|
|
52
|
+
|
|
53
|
+
# `...name` in array/object pattern. (name nil = don't bind)
|
|
54
|
+
PatternRest = TypedData.define(name: ->(v) { IDENTIFIER === v || v.nil? })
|
|
39
55
|
|
|
40
56
|
module Expression
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
# Atom literal (incl NULL)
|
|
58
|
+
Lit = TypedData.define(value: Atom)
|
|
59
|
+
|
|
60
|
+
# `!expr` or bare `!` (payload nil = !null)
|
|
61
|
+
ErrLit = TypedData.define(payload: ->(v) { Expression === v || v.nil? })
|
|
62
|
+
|
|
63
|
+
ArrLit = TypedData.define(items: ->(v) { v.is_a?(Array) && v.all? { |e| ArrayItem === e || ArraySpread === e } })
|
|
64
|
+
|
|
65
|
+
# [KeyValuePair|ObjectSpread], distinct fixed keys
|
|
66
|
+
ObjLit = TypedData.define(pairs: ->(v) {
|
|
45
67
|
v.is_a?(Array) &&
|
|
46
68
|
v.all? { |m| KeyValuePair === m || ObjectSpread === m } &&
|
|
47
69
|
v.filter_map { |m| m.key if KeyValuePair === m }.then { |keys| keys.uniq.size == keys.size }
|
|
48
70
|
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
|
|
72
|
+
# [] = the empty function
|
|
73
|
+
FuncLit = TypedData.define(clauses: ->(v) { v.is_a?(Array) && v.all? { |c| Clause === c } })
|
|
74
|
+
|
|
75
|
+
# Read a builtin/bound name
|
|
76
|
+
Ident = TypedData.define(name: IDENTIFIER)
|
|
77
|
+
|
|
78
|
+
FileRef = TypedData.define(variety: ->(v) { [:self, :super, :super_name, :name, :path].include?(v) }, path: ->(v) { String === v || v.nil? })
|
|
79
|
+
|
|
80
|
+
# `left | right`
|
|
81
|
+
Pipe = TypedData.define(left: Expression, right: Expression)
|
|
82
|
+
|
|
83
|
+
# `obj.key`
|
|
84
|
+
Member = TypedData.define(obj: Expression, key: IDENTIFIER)
|
|
85
|
+
|
|
86
|
+
# `obj[expr]`
|
|
87
|
+
Index = TypedData.define(obj: Expression, idx: Expression)
|
|
88
|
+
|
|
89
|
+
# `obj[expr = expr]`
|
|
90
|
+
IndexSet = TypedData.define(obj: Expression, idx: Expression, value: Expression)
|
|
55
91
|
|
|
56
92
|
constants.each do |name|
|
|
57
93
|
node = const_get(name)
|
|
@@ -60,22 +96,35 @@ module Fusion
|
|
|
60
96
|
end
|
|
61
97
|
|
|
62
98
|
module Pattern
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
PWild
|
|
67
|
-
|
|
99
|
+
# literal pattern
|
|
100
|
+
PLit = TypedData.define(value: Atom)
|
|
101
|
+
|
|
102
|
+
# `!` or `!pat` ; inner=PWild matches any error
|
|
103
|
+
PErr = TypedData.define(inner: Pattern)
|
|
104
|
+
|
|
105
|
+
# binds
|
|
106
|
+
PBind = TypedData.define(name: IDENTIFIER)
|
|
107
|
+
|
|
108
|
+
# `_`
|
|
109
|
+
PWild = TypedData.define(dummy: NilClass)
|
|
110
|
+
|
|
111
|
+
# [PatternItem|PatternRest], at most one rest
|
|
112
|
+
PArr = TypedData.define(items: ->(v) {
|
|
68
113
|
v.is_a?(Array) &&
|
|
69
114
|
v.all? { |e| PatternItem === e || PatternRest === e } &&
|
|
70
115
|
v.count { |e| PatternRest === e } <= 1
|
|
71
116
|
})
|
|
72
|
-
|
|
117
|
+
|
|
118
|
+
# [PatternPair|PatternRest], one rest, distinct keys
|
|
119
|
+
PObj = TypedData.define(pairs: ->(v) {
|
|
73
120
|
v.is_a?(Array) &&
|
|
74
121
|
v.all? { |m| PatternPair === m || PatternRest === m } &&
|
|
75
122
|
v.count { |m| PatternRest === m } <= 1 &&
|
|
76
123
|
v.filter_map { |m| m.key if PatternPair === m }.then { |keys| keys.uniq.size == keys.size }
|
|
77
124
|
})
|
|
78
|
-
|
|
125
|
+
|
|
126
|
+
# `inner ? predicate`
|
|
127
|
+
PGuard = TypedData.define(inner: Pattern, pred_expr: Expression)
|
|
79
128
|
|
|
80
129
|
constants.each do |name|
|
|
81
130
|
node = const_get(name)
|
|
@@ -85,7 +134,7 @@ module Fusion
|
|
|
85
134
|
|
|
86
135
|
module Statement
|
|
87
136
|
# The only statement. Only allowed in the REPL. `name = expression`.
|
|
88
|
-
Assignment = TypedData.define(name:
|
|
137
|
+
Assignment = TypedData.define(name: IDENTIFIER, expression: Expression)
|
|
89
138
|
|
|
90
139
|
constants.each do |name|
|
|
91
140
|
node = const_get(name)
|
data/lib/fusion/atom.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Fusion
|
|
|
11
11
|
# A scalar literal value: the JSON atoms plus NULL (everything the lexer
|
|
12
12
|
# emits as a token value, see Lexer#lex_number and #lex_word).
|
|
13
13
|
Atom = ->(v) {
|
|
14
|
-
v == NULL ||
|
|
14
|
+
v == NULL || v == true || v == false ||
|
|
15
15
|
v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(String)
|
|
16
16
|
}
|
|
17
17
|
end
|
data/lib/fusion/cli/decoder.rb
CHANGED
|
@@ -10,9 +10,9 @@ module Fusion
|
|
|
10
10
|
module Decoder
|
|
11
11
|
extend self
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
array: "[0, _]
|
|
15
|
-
object: '{"value": _}
|
|
13
|
+
EXPECTED_ENVELOPE_SHAPES = {
|
|
14
|
+
array: ["[0, _]", "[1, _]"],
|
|
15
|
+
object: ['{"value": _}', '{"error": _}'],
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
18
|
# String -> WirePair
|
|
@@ -26,7 +26,7 @@ module Fusion
|
|
|
26
26
|
next unless raw.is_a?(Array) && raw.length == 2 && raw[0].is_a?(Integer)
|
|
27
27
|
|
|
28
28
|
# The tag must be exactly the integer 0 or 1 (no 0.0 — Fusion equality is exact).
|
|
29
|
-
[raw[0], raw[1]] if
|
|
29
|
+
[raw[0], raw[1]] if [0, 1].include?(raw[0])
|
|
30
30
|
end
|
|
31
31
|
when :object
|
|
32
32
|
decode_envelope(text, mode) do |raw|
|
|
@@ -50,6 +50,10 @@ module Fusion
|
|
|
50
50
|
return WirePair.new(status: 0, data: text) unless stripped.start_with?("!")
|
|
51
51
|
|
|
52
52
|
payload = stripped.delete_prefix("!")
|
|
53
|
+
# TODO: BUG? should we really convert "" to "null"?
|
|
54
|
+
# Feels inconsistent with '-!' flag, but might be slightly different case.
|
|
55
|
+
# Also, definitely inconsistent with non-error case above.
|
|
56
|
+
# Also, the "!" == "!null" leniency holds only in code, not in input/output JSON.
|
|
53
57
|
WirePair.new(status: 1, data: payload.strip.empty? ? "null" : payload)
|
|
54
58
|
end
|
|
55
59
|
|
|
@@ -64,11 +68,12 @@ module Fusion
|
|
|
64
68
|
return WirePair.new(status: status, data: JSON.generate(inner)) if status
|
|
65
69
|
|
|
66
70
|
WirePair.new(status: 1, data: JSON.generate(
|
|
67
|
-
"kind"
|
|
68
|
-
"
|
|
71
|
+
"kind" => "argument_error",
|
|
72
|
+
"origin" => "input",
|
|
69
73
|
"operation" => "decoding input",
|
|
70
|
-
"
|
|
71
|
-
"
|
|
74
|
+
"status" => 0,
|
|
75
|
+
"input" => raw,
|
|
76
|
+
"expected" => EXPECTED_ENVELOPE_SHAPES.fetch(mode),
|
|
72
77
|
))
|
|
73
78
|
rescue JSON::ParserError
|
|
74
79
|
# TODO: BUG ???
|
data/lib/fusion/cli/encoder.rb
CHANGED
|
@@ -12,12 +12,12 @@ module Fusion
|
|
|
12
12
|
def encode(wire_pair, mode:)
|
|
13
13
|
case mode
|
|
14
14
|
when :bang
|
|
15
|
-
bang = wire_pair.status
|
|
15
|
+
bang = wire_pair.status == 0 ? "" : "!"
|
|
16
16
|
"#{bang}#{wire_pair.data}"
|
|
17
17
|
when :array
|
|
18
18
|
"[#{wire_pair.status},#{wire_pair.data}]"
|
|
19
19
|
when :object
|
|
20
|
-
key = wire_pair.status
|
|
20
|
+
key = wire_pair.status == 0 ? "value" : "error"
|
|
21
21
|
"{\"#{key}\":#{wire_pair.data}}"
|
|
22
22
|
else
|
|
23
23
|
raise Unreachable, "Unknown output mode #{mode}"
|