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.
Files changed (87) 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 +11 -9
  6. data/Rakefile +8 -0
  7. data/docs/lang/design.md +289 -51
  8. data/docs/lang/implementation.md +279 -0
  9. data/docs/lang/roadmap.md +20 -36
  10. data/docs/user/explanation.md +5 -10
  11. data/docs/user/how-to-guides.md +145 -15
  12. data/docs/user/reference.md +365 -140
  13. data/docs/user/tutorial.md +22 -19
  14. data/examples/double.fsn +4 -1
  15. data/examples/factorial.fsn +6 -3
  16. data/examples/fizzbuzz.fsn +1 -4
  17. data/examples/gcd.fsn +9 -0
  18. data/examples/json_test.fsn +4 -0
  19. data/examples/matrix/OP.fsn +2 -0
  20. data/examples/matrix/average.fsn +2 -0
  21. data/examples/matrix/solve.fsn +2 -0
  22. data/examples/palindrome.fsn +1 -1
  23. data/exe/fusion +12 -12
  24. data/lib/fusion/ast.rb +76 -27
  25. data/lib/fusion/atom.rb +1 -1
  26. data/lib/fusion/cli/decoder.rb +13 -8
  27. data/lib/fusion/cli/encoder.rb +2 -2
  28. data/lib/fusion/cli/options.rb +134 -61
  29. data/lib/fusion/cli/parser.rb +4 -4
  30. data/lib/fusion/cli/repl.rb +32 -27
  31. data/lib/fusion/cli/serializer.rb +11 -11
  32. data/lib/fusion/cli.rb +120 -49
  33. data/lib/fusion/interpreter/builtins.rb +298 -160
  34. data/lib/fusion/interpreter/env.rb +42 -12
  35. data/lib/fusion/interpreter/error_val.rb +42 -20
  36. data/lib/fusion/interpreter/thunk.rb +53 -0
  37. data/lib/fusion/interpreter.rb +263 -98
  38. data/lib/fusion/lexer.rb +125 -37
  39. data/lib/fusion/parser.rb +245 -70
  40. data/lib/fusion/version.rb +3 -1
  41. data/lib/fusion.rb +0 -1
  42. data/stdlib/all.fsn +13 -0
  43. data/stdlib/any.fsn +12 -0
  44. data/stdlib/chars.fsn +5 -0
  45. data/stdlib/compact.fsn +5 -0
  46. data/stdlib/concat.fsn +6 -0
  47. data/stdlib/entries.fsn +6 -0
  48. data/stdlib/falsey.fsn +6 -0
  49. data/stdlib/filter.fsn +12 -0
  50. data/stdlib/flatten.fsn +7 -0
  51. data/stdlib/map.fsn +7 -4
  52. data/stdlib/matrix/Matrix.fsn +8 -0
  53. data/stdlib/matrix/OP.fsn +18 -0
  54. data/stdlib/matrix/add.fsn +7 -0
  55. data/stdlib/matrix/column.fsn +6 -0
  56. data/stdlib/matrix/determinant.fsn +16 -0
  57. data/stdlib/matrix/dimensions.fsn +5 -0
  58. data/stdlib/matrix/identity.fsn +6 -0
  59. data/stdlib/matrix/invert.fsn +26 -0
  60. data/stdlib/matrix/minor.fsn +15 -0
  61. data/stdlib/matrix/multiply.fsn +10 -0
  62. data/stdlib/matrix/negate.fsn +5 -0
  63. data/stdlib/matrix/product.fsn +11 -0
  64. data/stdlib/matrix/rotate.fsn +10 -0
  65. data/stdlib/matrix/row.fsn +6 -0
  66. data/stdlib/matrix/scale.fsn +6 -0
  67. data/stdlib/matrix/subtract.fsn +7 -0
  68. data/stdlib/matrix/sum.fsn +14 -0
  69. data/stdlib/matrix/transpose.fsn +5 -0
  70. data/stdlib/range.fsn +2 -2
  71. data/stdlib/reduce.fsn +8 -0
  72. data/stdlib/safe.fsn +7 -0
  73. data/stdlib/sanitize.fsn +2 -3
  74. data/stdlib/toObject.fsn +7 -0
  75. data/stdlib/truthy.fsn +7 -0
  76. data/stdlib/vector/Vector.fsn +7 -0
  77. data/stdlib/vector/add.fsn +6 -0
  78. data/stdlib/vector/cross.fsn +6 -0
  79. data/stdlib/vector/dot.fsn +6 -0
  80. data/stdlib/vector/norm.fsn +6 -0
  81. data/stdlib/vector/scale.fsn +5 -0
  82. data/stdlib/vector/subtract.fsn +6 -0
  83. data/stdlib/zip.fsn +6 -0
  84. metadata +50 -4
  85. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  86. data/stdlib/mapValues.fsn +0 -5
  87. data/stdlib/math/square.fsn +0 -4
@@ -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] | @multiply)
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] | @multiply` built a two-element array and piped it into the built-in
36
- `@multiply`. Fusion has no `*` operator (yet); arithmetic is done by piping a pair
37
- into a named function. **Built-ins are reached with an `@` prefix**, just like files
38
- — `@multiply`, `@add`, and so on. (You'll see why `@` is used for both in Step 8.)
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] | @lessThan | (
130
- true => [0, n] | @subtract,
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] | @lessThan` produces `true` or `false`. That
144
- boolean is then piped into a *second, inline function* whose two clauses are the two
145
- branches. **An `if` is just a function with a `true` clause and a `false` clause.**
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 | @ ] | @add
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] | @subtract | @] | @multiply
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] ? @lessThan => [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] | @multiply), "xs": xs} | @map)
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 list `xs`. You just passed a function as a value
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 `@multiply` finds the built-in and `@map` falls through to
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 (`@add`, `@Integer`),
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] => [[first, first] | @add, ...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] | @lessThan, _ => true) => !"Only non-negative integers, please!",
4
- 0 => 1,
5
- n => [n, [n, 1] | @subtract | @] | @multiply
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
  )
@@ -1,10 +1,7 @@
1
1
  # FizzBuzz for a single integer
2
2
  (
3
3
  n =>
4
- [
5
- [n, 3] | @mod,
6
- [n, 5] | @mod,
7
- ]
4
+ [n % 3, n % 5]
8
5
  |
9
6
  (
10
7
  [0, 0] => "FizzBuzz",
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,4 @@
1
+ # JSON with comments, trailing commas and inline computation.
2
+ {
3
+ "duration": 24 * 60,
4
+ }
@@ -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)
@@ -4,6 +4,6 @@
4
4
  s ? @String => s | @chars | @,
5
5
  [] => "palindrome",
6
6
  [_] => "palindrome",
7
- [_, ...rest, _] ? @ends | @equals => rest | @,
7
+ [_, ...rest, _] ? @ends | @OP.equal => rest | @,
8
8
  _ => "not a palindrome"
9
9
  )
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 '<json-input>' # input as an argument
9
- # fusion -e '(n => [n,2] | @multiply)' '21' # inline program
10
- # fusion --stream path/to/main.fsn # NDJSON in, NDJSON out
11
- # fusion --repl # interactive expressions/statements
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
- options = begin
23
- Fusion::CLI::Options.parse(ARGV)
24
- rescue Fusion::CLI::Options::UsageError => err
25
- # A command-line misuse happens before the input/output contract begins, so
26
- # it is plain text on stderr, not a payloaded error.
27
- abort("fusion: #{err.message}\n\n#{Fusion::CLI::Options::USAGE}")
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
- Identifier = /\A[A-Za-z_][A-Za-z0-9_]*\z/
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
- ArrayItem = TypedData.define(value: Expression) # an array element
32
- ArraySpread = TypedData.define(value: Expression) # ...expr inside an array
33
- KeyValuePair = TypedData.define(key: String, value: Expression) # "k": expr inside an object
34
- ObjectSpread = TypedData.define(value: Expression) # ...expr inside an object
35
- Clause = TypedData.define(pattern: Pattern, body: Expression) # one pattern => body of a function
36
- PatternItem = TypedData.define(pattern: Pattern) # a sub-pattern of an array pattern
37
- PatternPair = TypedData.define(key: String, pattern: Pattern) # "k": pat inside an object pattern
38
- PatternRest = TypedData.define(name: ->(v) { Identifier === v || v.nil? }) # ...name (name nil = ignore) in either
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
- Lit = TypedData.define(value: Atom) # atom literal (incl NULL)
42
- ErrLit = TypedData.define(payload: ->(v) { Expression === v || v.nil? }) # !expr or bare ! (payload nil = !null)
43
- ArrLit = TypedData.define(items: ->(v) { v.is_a?(Array) && v.all? { |e| ArrayItem === e || ArraySpread === e } })
44
- ObjLit = TypedData.define(pairs: ->(v) { # [KeyValuePair|ObjectSpread], distinct fixed keys
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
- FuncLit = TypedData.define(clauses: ->(v) { v.is_a?(Array) && v.all? { |c| Clause === c } }) # [] = the empty function
50
- Ident = TypedData.define(name: Identifier) # read a builtin/bound name
51
- FileRef = TypedData.define(variety: ->(v) { %i[self name path].include?(v) }, path: ->(v) { String === v || v.nil? })
52
- Pipe = TypedData.define(left: Expression, right: Expression) # left | right
53
- Member = TypedData.define(obj: Expression, key: Identifier) # obj.key
54
- Index = TypedData.define(obj: Expression, idx: Expression) # obj[expr]
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
- PLit = TypedData.define(value: Atom) # literal pattern
64
- PErr = TypedData.define(inner: Pattern) # ! or !pat ; inner=PWild matches any error
65
- PBind = TypedData.define(name: Identifier) # binds
66
- PWild = TypedData.define(dummy: NilClass) # _
67
- PArr = TypedData.define(items: ->(v) { # [PatternItem|PatternRest], at most one rest
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
- PObj = TypedData.define(pairs: ->(v) { # [PatternPair|PatternRest], one rest, distinct keys
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
- PGuard = TypedData.define(inner: Pattern, pred_expr: Expression) # inner ? predicate
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: Identifier, expression: Expression)
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 || v == true || v == false ||
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
@@ -10,9 +10,9 @@ module Fusion
10
10
  module Decoder
11
11
  extend self
12
12
 
13
- ENVELOPE_SHAPES = {
14
- array: "[0, _] or [1, _]",
15
- object: '{"value": _} or {"error": _}',
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 raw[0] == 0 || raw[0] == 1
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" => "argument_error",
68
- "location" => "input",
71
+ "kind" => "argument_error",
72
+ "origin" => "input",
69
73
  "operation" => "decoding input",
70
- "input" => raw,
71
- "message" => "expected #{ENVELOPE_SHAPES.fetch(mode)}"
74
+ "status" => 0,
75
+ "input" => raw,
76
+ "expected" => EXPECTED_ENVELOPE_SHAPES.fetch(mode),
72
77
  ))
73
78
  rescue JSON::ParserError
74
79
  # TODO: BUG ???
@@ -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.zero? ? "" : "!"
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.zero? ? "value" : "error"
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}"