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
@@ -11,30 +11,16 @@ module Fusion
11
11
  @interp = interp
12
12
  define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
13
13
 
14
- # operations on a pair [a, b] (or a single value)
15
- define.call("add", method(:add))
16
- define.call("subtract", method(:subtract))
17
- define.call("multiply", method(:multiply))
18
- define.call("divide", method(:divide))
19
- define.call("mod", method(:mod))
20
- define.call("negate", method(:negate))
21
- define.call("floor", method(:floor))
22
- define.call("equals", method(:equals))
23
- define.call("lessThan", method(:less_than))
24
- define.call("and", method(:and_))
25
- define.call("or", method(:or_))
26
- define.call("not", method(:not_))
27
- define.call("length", method(:length))
28
- define.call("concat", method(:concat))
29
- define.call("chars", method(:chars))
14
+ # Irreducible primitives kept as built-ins. The sugar-target operators
15
+ # (arithmetic/comparison/boolean) live in `@OP` below. Numeric functions
16
+ # (`round`, `divide`, `sin`, …) and constants (`pi`, `e`) live in `@math`.
17
+ define.call("size", method(:size))
30
18
  define.call("join", method(:join))
19
+ define.call("split", method(:split))
31
20
  define.call("toString", method(:to_string))
32
21
  define.call("parseNumber", method(:parse_number))
33
22
  define.call("keys", method(:keys))
34
23
  define.call("values", method(:values))
35
- define.call("get", method(:get))
36
- define.call("set", method(:set))
37
- define.call("toObject", method(:to_object))
38
24
 
39
25
  # type predicates: return false on any non-matching value, never an error
40
26
  define.call("Integer", method(:integer?))
@@ -44,164 +30,347 @@ module Fusion
44
30
  define.call("Boolean", method(:boolean?))
45
31
  define.call("Array", method(:array?))
46
32
  define.call("Object", method(:object?))
33
+ define.call("Collection", method(:collection?))
47
34
  define.call("Null", method(:null?))
48
35
  define.call("Function", method(:function?))
49
36
  define.call("NonFinite", method(:non_finite?))
50
- end
51
37
 
52
- # --- arithmetic ---
38
+ # `OP` bundles the sugar-target operators as a shadowable builtin object,
39
+ # reached as `@OP.sum`, `@OP.and`, … A directory can swap the operators by
40
+ # placing an `OP.fsn` sibling that overrides members (reaching the
41
+ # originals with `@@`); infix sugar and the derived stdlib helpers resolve
42
+ # `@OP` per directory, so they follow the override.
43
+ table["OP"] = {
44
+ "sum" => NativeFunc.new("OP.sum", method(:op_sum)),
45
+ "product" => NativeFunc.new("OP.product", method(:op_product)),
46
+ "negate" => NativeFunc.new("OP.negate", method(:op_negate)),
47
+ "invert" => NativeFunc.new("OP.invert", method(:op_invert)),
48
+ "quotient" => NativeFunc.new("OP.quotient", method(:op_quotient)),
49
+ "modulo" => NativeFunc.new("OP.modulo", method(:op_modulo)),
50
+ "equal" => NativeFunc.new("OP.equal", method(:op_equal)),
51
+ "compare" => NativeFunc.new("OP.compare", method(:op_compare)),
52
+ "lt" => NativeFunc.new("OP.lt", method(:op_lt)),
53
+ "gt" => NativeFunc.new("OP.gt", method(:op_gt)),
54
+ "lte" => NativeFunc.new("OP.lte", method(:op_lte)),
55
+ "gte" => NativeFunc.new("OP.gte", method(:op_gte)),
56
+ "and" => NativeFunc.new("OP.and", method(:op_and)),
57
+ "or" => NativeFunc.new("OP.or", method(:op_or)),
58
+ "not" => NativeFunc.new("OP.not", method(:op_not)),
59
+ }
60
+
61
+ # `math` bundles numeric functions and constants, reached as `@math.round`,
62
+ # `@math.pi`, … Like `@OP`, it is a shadowable builtin object. `pi`/`e` are
63
+ # plain values; the rest are one-argument functions.
64
+ table["math"] = {
65
+ "round" => NativeFunc.new("math.round", method(:math_round)),
66
+ "floor" => NativeFunc.new("math.floor", method(:math_floor)),
67
+ "ceil" => NativeFunc.new("math.ceil", method(:math_ceil)),
68
+ "divide" => NativeFunc.new("math.divide", method(:math_divide)),
69
+ "sign" => NativeFunc.new("math.sign", method(:math_sign)),
70
+ "abs" => NativeFunc.new("math.abs", method(:math_abs)),
71
+ "rand" => NativeFunc.new("math.rand", method(:math_rand)),
72
+ "sin" => NativeFunc.new("math.sin", method(:math_sin)),
73
+ "cos" => NativeFunc.new("math.cos", method(:math_cos)),
74
+ "exp" => NativeFunc.new("math.exp", method(:math_exp)),
75
+ "log" => NativeFunc.new("math.log", method(:math_log)),
76
+ "pow" => NativeFunc.new("math.pow", method(:math_pow)),
77
+ "sqrt" => NativeFunc.new("math.sqrt", method(:math_sqrt)),
78
+ "pi" => Math::PI,
79
+ "e" => Math::E,
80
+ }
81
+ end
82
+
83
+ # --- math (reached as `@math.round`, `@math.divide`, `@math.pi`, …) ---
84
+
85
+ NUMBER_PAIR = ["[_ ? @Number, _ ? @Number]"].freeze
86
+ NUMBER_ARRAY = ['_ ? (xs => {"c": xs, "f": @Number} | @all)'].freeze
87
+ NUMBER = ["_ ? @Number"].freeze
88
+
89
+ # `round`/`floor`/`ceil` return an integer; a non-finite input is a math_error
90
+ # (their Ruby forms raise `FloatDomainError` on it).
91
+ def math_round(v)
92
+ return v if v.is_a?(ErrorVal)
93
+ return argument_error("math.round", v, NUMBER) unless numeric?(v)
94
+ return error("math_error", "math.round", v, "not a finite number") if non_finite?(v)
95
+
96
+ v.round
97
+ end
53
98
 
54
- def add(v)
99
+ def math_floor(v)
55
100
  return v if v.is_a?(ErrorVal)
56
- return error("argument_error", "add", v, "expected [_, _]") unless pair?(v)
57
- return error("type_error", "add", v, "expected numbers") unless numeric?(v[0])
58
- return error("type_error", "add", v, "expected numbers") unless numeric?(v[1])
101
+ return argument_error("math.floor", v, NUMBER) unless numeric?(v)
102
+ return error("math_error", "math.floor", v, "not a finite number") if non_finite?(v)
59
103
 
60
- v[0] + v[1]
104
+ v.floor
61
105
  end
62
106
 
63
- def subtract(v)
107
+ def math_ceil(v)
64
108
  return v if v.is_a?(ErrorVal)
65
- return error("argument_error", "subtract", v, "expected [_, _]") unless pair?(v)
66
- return error("type_error", "subtract", v, "expected numbers") unless numeric?(v[0])
67
- return error("type_error", "subtract", v, "expected numbers") unless numeric?(v[1])
109
+ return argument_error("math.ceil", v, NUMBER) unless numeric?(v)
110
+ return error("math_error", "math.ceil", v, "not a finite number") if non_finite?(v)
68
111
 
69
- v[0] - v[1]
112
+ v.ceil
70
113
  end
71
114
 
72
- def multiply(v)
115
+ # `divide` always yields a float. Integer division is `@OP.quotient`, the
116
+ # remainder `@OP.modulo`.
117
+ def math_divide(v)
73
118
  return v if v.is_a?(ErrorVal)
74
- return error("argument_error", "multiply", v, "expected [_, _]") unless pair?(v)
75
- return error("type_error", "multiply", v, "expected numbers") unless numeric?(v[0])
76
- return error("type_error", "multiply", v, "expected numbers") unless numeric?(v[1])
119
+ return argument_error("math.divide", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
120
+ return error("math_error", "math.divide", v, "division by zero") if v[1] == 0
77
121
 
78
- v[0] * v[1]
122
+ v[0].to_f / v[1]
79
123
  end
80
124
 
81
- def divide(v)
125
+ # -1 / 0 / 1 (via `<`/`>`, so NaN → 0, never Ruby's `nil`).
126
+ def math_sign(v)
82
127
  return v if v.is_a?(ErrorVal)
83
- return error("argument_error", "divide", v, "expected [_, _]") unless pair?(v)
84
- return error("type_error", "divide", v, "expected numbers") unless numeric?(v[0])
85
- return error("type_error", "divide", v, "expected numbers") unless numeric?(v[1])
86
- return error("math_error", "divide", v, "division by zero") if v[1] == 0
128
+ return argument_error("math.sign", v, NUMBER) unless numeric?(v)
87
129
 
88
- a, b = v
89
- if a.is_a?(Integer) && b.is_a?(Integer) && (a % b).zero?
90
- a / b
91
- else
92
- a.to_f / b
130
+ if v < 0 then -1
131
+ elsif v > 0 then 1
132
+ else 0
93
133
  end
94
134
  end
95
135
 
96
- def mod(v)
136
+ def math_abs(v)
97
137
  return v if v.is_a?(ErrorVal)
98
- return error("argument_error", "mod", v, "expected [_, _]") unless pair?(v)
99
- return error("type_error", "mod", v, "expected numbers") unless numeric?(v[0])
100
- return error("type_error", "mod", v, "expected numbers") unless numeric?(v[1])
101
- return error("math_error", "mod", v, "modulo by zero") if v[1] == 0
138
+ return argument_error("math.abs", v, NUMBER) unless numeric?(v)
102
139
 
103
- v[0] % v[1]
140
+ v.abs
104
141
  end
105
142
 
106
- def negate(v)
143
+ # `null` → a float in `[0.0, 1.0)`; a positive integer `n` → an integer in
144
+ # `[0, n)`.
145
+ def math_rand(v)
107
146
  return v if v.is_a?(ErrorVal)
108
- return error("type_error", "negate", v, "expected a number") unless numeric?(v)
147
+ return rand if v == NULL
148
+ return rand(v) if integer?(v) && v > 0
109
149
 
110
- -v
150
+ argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
111
151
  end
112
152
 
113
- def floor(v)
153
+ def math_sin(v)
114
154
  return v if v.is_a?(ErrorVal)
115
- return error("type_error", "floor", v, "expected a number") unless numeric?(v)
116
- return error("math_error", "floor", v, "not a finite number") if non_finite?(v)
155
+ return argument_error("math.sin", v, NUMBER) unless numeric?(v)
117
156
 
118
- v.floor
157
+ Math.sin(v)
158
+ end
159
+
160
+ def math_cos(v)
161
+ return v if v.is_a?(ErrorVal)
162
+ return argument_error("math.cos", v, NUMBER) unless numeric?(v)
163
+
164
+ Math.cos(v)
119
165
  end
120
166
 
121
- # --- comparison ---
167
+ def math_exp(v)
168
+ return v if v.is_a?(ErrorVal)
169
+ return argument_error("math.exp", v, NUMBER) unless numeric?(v)
170
+
171
+ Math.exp(v)
172
+ end
173
+
174
+ # Square root; the domain is non-negative numbers (a negative would be
175
+ # complex).
176
+ def math_sqrt(v)
177
+ return v if v.is_a?(ErrorVal)
178
+ return argument_error("math.sqrt", v, NUMBER) unless numeric?(v)
179
+ return error("math_error", "math.sqrt", v, "square root of a negative number") if v < 0
180
+
181
+ Math.sqrt(v)
182
+ end
122
183
 
123
- def equals(v)
184
+ # Natural logarithm; the domain is positive numbers (`Math.log` raises on a
185
+ # negative and gives `-Infinity` at 0).
186
+ def math_log(v)
124
187
  return v if v.is_a?(ErrorVal)
125
- return error("argument_error", "equals", v, "expected [_, _]") unless pair?(v)
188
+ return argument_error("math.log", v, NUMBER) unless numeric?(v)
189
+ return error("math_error", "math.log", v, "log of a non-positive number") if v <= 0
126
190
 
127
- @interp.deep_equal?(v[0], v[1])
191
+ Math.log(v)
128
192
  end
129
193
 
130
- def less_than(v)
194
+ # `base ** exponent`: integer when the base and a non-negative integer
195
+ # exponent are integers, a float otherwise. A negative base with a fractional
196
+ # exponent (a complex result) is a math_error.
197
+ def math_pow(v)
131
198
  return v if v.is_a?(ErrorVal)
132
- return error("argument_error", "lessThan", v, "expected [_, _]") unless pair?(v)
199
+ return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
133
200
 
134
201
  a, b = v
135
- if numeric?(a) && numeric?(b)
136
- a < b
137
- elsif a.is_a?(String) && b.is_a?(String)
138
- a < b
139
- else
140
- error("type_error", "lessThan", v, "expected two numbers or two strings")
141
- end
202
+ result = a.is_a?(Integer) && b.is_a?(Integer) && b >= 0 ? a**b : a.to_f**b
203
+ return error("math_error", "math.pow", v, "not in domain (complex result)") if result.is_a?(Complex)
204
+
205
+ result
142
206
  end
143
207
 
144
- # --- boolean ---
208
+ # --- OP: the sugar-target operators (reached as `@OP.sum`, `@OP.and`, …) ---
209
+ #
210
+ # The arithmetic, boolean, and equality members take an array of ANY length;
211
+ # the unary ones take a single value; `compare` returns -1 / 0 / 1, which
212
+ # `lt` / `gt` / `lte` / `gte` then read into a boolean.
145
213
 
146
- # `and`/`or`/`not` judge truthiness (Ruby-style: `false` and `null` are
147
- # falsey, everything else truthy), not strict booleans, and always return a
148
- # boolean. They share the interpreter's `truthy?`, the same test `?` guards use.
149
- def and_(v)
214
+ def op_sum(v)
150
215
  return v if v.is_a?(ErrorVal)
151
- return error("argument_error", "and", v, "expected [_, _]") unless pair?(v)
216
+ return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
152
217
 
153
- @interp.truthy?(v[0]) && @interp.truthy?(v[1])
218
+ v.sum(0)
154
219
  end
155
220
 
156
- def or_(v)
221
+ def op_product(v)
157
222
  return v if v.is_a?(ErrorVal)
158
- return error("argument_error", "or", v, "expected [_, _]") unless pair?(v)
223
+ return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
159
224
 
160
- @interp.truthy?(v[0]) || @interp.truthy?(v[1])
225
+ v.reduce(1, :*)
161
226
  end
162
227
 
163
- def not_(v)
228
+ def op_negate(v)
164
229
  return v if v.is_a?(ErrorVal)
230
+ return argument_error("OP.negate", v, ["_ ? @Number"]) unless numeric?(v)
165
231
 
166
- !@interp.truthy?(v)
232
+ -v
167
233
  end
168
234
 
169
- # --- strings and structure bridges ---
235
+ # The unary reciprocal 1/x, always a float; 0 is a math_error.
236
+ def op_invert(v)
237
+ return v if v.is_a?(ErrorVal)
238
+ return argument_error("OP.invert", v, ["_ ? @Number"]) unless numeric?(v)
239
+ return error("math_error", "OP.invert", v, "division by zero") if v == 0
170
240
 
171
- def length(v)
241
+ 1.0 / v
242
+ end
243
+
244
+ INTEGER_PAIR = ["[_ ? @Integer, _ ? @Integer]"].freeze
245
+
246
+ # Integer division and its remainder — integers only (`@divide` handles the
247
+ # float case). Ruby's `/` and `%` agree in sign, so `q*b + r == a` holds.
248
+ def op_quotient(v)
172
249
  return v if v.is_a?(ErrorVal)
173
- return error("type_error", "length", v, "expected a string, array, or object") unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
250
+ return argument_error("OP.quotient", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
251
+ return error("math_error", "OP.quotient", v, "division by zero") if v[1] == 0
174
252
 
175
- v.length
253
+ v[0] / v[1]
176
254
  end
177
255
 
178
- def concat(v)
256
+ def op_modulo(v)
179
257
  return v if v.is_a?(ErrorVal)
180
- return error("argument_error", "concat", v, "expected [_, _]") unless pair?(v)
181
- return error("type_error", "concat", v, "expected strings") unless v[0].is_a?(String) && v[1].is_a?(String)
258
+ return argument_error("OP.modulo", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
259
+ return error("math_error", "OP.modulo", v, "modulo by zero") if v[1] == 0
182
260
 
183
- v[0] + v[1]
261
+ v[0] % v[1]
184
262
  end
185
263
 
186
- def chars(v)
264
+ # Deep, exact equality across the whole array: true iff every element
265
+ # equals the first (so 0 and 1 elements are vacuously equal). Any types.
266
+ def op_equal(v)
187
267
  return v if v.is_a?(ErrorVal)
188
- return error("type_error", "chars", v, "expected a string") unless v.is_a?(String)
268
+ return argument_error("OP.equal", v, ["_ ? @Array"]) unless v.is_a?(Array)
269
+
270
+ v.all? { |x| @interp.deep_equal?(v[0], x) }
271
+ end
272
+
273
+ COMPARE_PAIR = ["[_ ? @Number, _ ? @Number]", "[_ ? @String, _ ? @String]"].freeze
274
+
275
+ # Order two numbers or two strings: -1, 0, or 1 (no deep equality). Built on
276
+ # `<` rather than `<=>` so a NaN operand yields 0 (unordered), never Ruby's
277
+ # `nil` — NaN is a reachable value (`Infinity - Infinity`).
278
+ def op_compare(v)
279
+ return v if v.is_a?(ErrorVal)
280
+ return argument_error("OP.compare", v, COMPARE_PAIR) unless pair?(v)
281
+
282
+ a, b = v
283
+ unless (numeric?(a) && numeric?(b)) || (a.is_a?(String) && b.is_a?(String))
284
+ return argument_error("OP.compare", v, COMPARE_PAIR)
285
+ end
189
286
 
190
- v.chars
287
+ if a < b then -1
288
+ elsif b < a then 1
289
+ else 0
290
+ end
291
+ end
292
+
293
+ # The comparison readers interpret an `@OP.compare` result. The infix sugar
294
+ # `a < b` desugars to `[a, b] | @OP.compare | @OP.lt` (likewise `<=` / `>` /
295
+ # `>=`), so an `@OP` override reskins the ordering and its reading together.
296
+
297
+ def op_lt(v)
298
+ read_compare_result("OP.lt", v, [-1])
191
299
  end
192
300
 
301
+ def op_gt(v)
302
+ read_compare_result("OP.gt", v, [1])
303
+ end
304
+
305
+ def op_lte(v)
306
+ read_compare_result("OP.lte", v, [-1, 0])
307
+ end
308
+
309
+ def op_gte(v)
310
+ read_compare_result("OP.gte", v, [0, 1])
311
+ end
312
+
313
+ def op_and(v)
314
+ return v if v.is_a?(ErrorVal)
315
+ return argument_error("OP.and", v, ["_ ? @Array"]) unless v.is_a?(Array)
316
+
317
+ v.all? { |x| @interp.truthy?(x) }
318
+ end
319
+
320
+ def op_or(v)
321
+ return v if v.is_a?(ErrorVal)
322
+ return argument_error("OP.or", v, ["_ ? @Array"]) unless v.is_a?(Array)
323
+
324
+ v.any? { |x| @interp.truthy?(x) }
325
+ end
326
+
327
+ def op_not(v)
328
+ return v if v.is_a?(ErrorVal)
329
+
330
+ !@interp.truthy?(v)
331
+ end
332
+
333
+ # --- strings and structure bridges ---
334
+
335
+ def size(v)
336
+ return v if v.is_a?(ErrorVal)
337
+ return argument_error("size", v, ["_ ? @String", "_ ? @Collection"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
338
+
339
+ v.length
340
+ end
341
+
342
+ # `[items, separator]`: join an array of strings into one string. `@concat`
343
+ # is the stdlib pair-case built on this.
193
344
  def join(v)
194
345
  return v if v.is_a?(ErrorVal)
195
- return error("argument_error", "join", v, "expected [_, _]") unless pair?(v)
346
+
347
+ expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
348
+ return argument_error("join", v, expected) unless pair?(v)
196
349
 
197
350
  array, separator = v
198
351
  unless array.is_a?(Array) && separator.is_a?(String) && array.all? { |item| item.is_a?(String) }
199
- return error("type_error", "join", v, "expected [array-of-strings, separator-string]")
352
+ return argument_error("join", v, expected)
200
353
  end
201
354
 
202
355
  array.join(separator)
203
356
  end
204
357
 
358
+ # `[string, separator]`: the inverse of `@join`. Splits on the LITERAL
359
+ # separator (Ruby's `" "` whitespace special-case does not apply) and keeps
360
+ # empty fields; an empty separator splits into characters. `@chars` is the
361
+ # stdlib single-string case built on this.
362
+ def split(v)
363
+ return v if v.is_a?(ErrorVal)
364
+
365
+ expected = ["[_ ? @String, _ ? @String]"]
366
+ return argument_error("split", v, expected) unless pair?(v) && v[0].is_a?(String) && v[1].is_a?(String)
367
+
368
+ string, separator = v
369
+ return string.chars if separator.empty?
370
+
371
+ string.split(Regexp.new(Regexp.escape(separator)), -1)
372
+ end
373
+
205
374
  def to_string(v)
206
375
  return v if v.is_a?(ErrorVal)
207
376
 
@@ -217,7 +386,7 @@ module Fusion
217
386
 
218
387
  def parse_number(v)
219
388
  return v if v.is_a?(ErrorVal)
220
- return error("type_error", "parseNumber", v, "expected a string") unless v.is_a?(String)
389
+ return argument_error("parseNumber", v, ["_ ? @String"]) unless v.is_a?(String)
221
390
 
222
391
  case v
223
392
  when /\A-?\d+\z/ then v.to_i
@@ -230,69 +399,18 @@ module Fusion
230
399
 
231
400
  def keys(v)
232
401
  return v if v.is_a?(ErrorVal)
233
- return error("type_error", "keys", v, "expected an object") unless v.is_a?(Hash)
402
+ return argument_error("keys", v, ["_ ? @Object"]) unless v.is_a?(Hash)
234
403
 
235
404
  v.keys
236
405
  end
237
406
 
238
407
  def values(v)
239
408
  return v if v.is_a?(ErrorVal)
240
- return error("type_error", "values", v, "expected an object") unless v.is_a?(Hash)
409
+ return argument_error("values", v, ["_ ? @Object"]) unless v.is_a?(Hash)
241
410
 
242
411
  v.values
243
412
  end
244
413
 
245
- # Read from an array (integer index, negative counts from the end) or an
246
- # object (string key) — mirroring the `[]` operator (reference §8).
247
- def get(v)
248
- return v if v.is_a?(ErrorVal)
249
- return error("argument_error", "get", v, "expected [_, _]") unless pair?(v)
250
-
251
- container, key = v
252
- if container.is_a?(Array) && key.is_a?(Integer)
253
- i = key.negative? ? container.length + key : key
254
- return container[i] if i >= 0 && i < container.length
255
-
256
- error("access_error", "get", v, "index out of range")
257
- elsif container.is_a?(Hash) && key.is_a?(String)
258
- return container[key] if container.key?(key)
259
-
260
- error("access_error", "get", v, "missing key")
261
- else
262
- error("type_error", "get", v, "bad index type")
263
- end
264
- end
265
-
266
- # Return a new array/object with one entry set. An array index must already
267
- # exist (arrays are not extended); an object key may be new. Addressing
268
- # mirrors `@get` (array by integer index, object by string key).
269
- def set(v)
270
- return v if v.is_a?(ErrorVal)
271
- return error("argument_error", "set", v, "expected [_, _, _]") unless v.is_a?(Array) && v.length == 3
272
-
273
- container, key, value = v
274
- if container.is_a?(Array) && key.is_a?(Integer)
275
- i = key.negative? ? container.length + key : key
276
- return error("access_error", "set", v, "index out of range") unless i >= 0 && i < container.length
277
-
278
- container.dup.tap { |copy| copy[i] = value }
279
- elsif container.is_a?(Hash) && key.is_a?(String)
280
- container.merge(key => value)
281
- else
282
- error("type_error", "set", v, "bad index type")
283
- end
284
- end
285
-
286
- def to_object(v)
287
- return v if v.is_a?(ErrorVal)
288
- return error("type_error", "toObject", v, "expected an array") unless v.is_a?(Array)
289
- unless v.all? { |entry| pair?(entry) && entry[0].is_a?(String) }
290
- return error("type_error", "toObject", v, "expected [string, value] entries")
291
- end
292
-
293
- v.to_h
294
- end
295
-
296
414
  private
297
415
 
298
416
  # Type predicates, also reused as internal guards.
@@ -314,7 +432,7 @@ module Fusion
314
432
  end
315
433
 
316
434
  def boolean?(v)
317
- v == true || v == false
435
+ [true, false].include?(v)
318
436
  end
319
437
 
320
438
  def array?(v)
@@ -325,6 +443,10 @@ module Fusion
325
443
  v.is_a?(Hash)
326
444
  end
327
445
 
446
+ def collection?(v)
447
+ array?(v) || object?(v)
448
+ end
449
+
328
450
  def null?(v)
329
451
  v == NULL
330
452
  end
@@ -341,15 +463,31 @@ module Fusion
341
463
  v.is_a?(Float) && !v.finite?
342
464
  end
343
465
 
344
- # Build a standardized interpreter error (see docs/user/reference.md §6.5).
466
+ COMPARE_RESULT = ["-1", "0", "1", "null"].freeze
467
+
468
+ # -1 / 0 / 1 (exact integers) → whether the ordering is in `truthy_results`.
469
+ # A partial order's compare may return null (incomparable); that passes
470
+ # through as null rather than being forced to a boolean.
471
+ def read_compare_result(name, v, truthy_results)
472
+ return v if v.is_a?(ErrorVal)
473
+ return NULL if v == NULL
474
+ return argument_error(name, v, COMPARE_RESULT) unless integer?(v) && [-1, 0, 1].include?(v)
475
+
476
+ truthy_results.include?(v)
477
+ end
478
+
479
+ # Build a standardized interpreter error carrying a human-readable
480
+ # `message` (see docs/user/reference.md §6.5). Use this for failures that
481
+ # aren't an input-shape mismatch (math, conversion, access). `operation`
482
+ # is the builtin's own `@`-reference (`@#{name}`).
345
483
  def error(kind, name, v, message)
346
- ErrorVal.internal(
347
- kind: kind,
348
- location: "builtin #{name}",
349
- operation: name,
350
- input: v,
351
- message: message
352
- )
484
+ ErrorVal.from_runtime(kind: kind, origin: "builtin", operation: "@#{name}", input: v, message: message)
485
+ end
486
+
487
+ # Build an `argument_error` describing the acceptable inputs as a list of
488
+ # Fusion patterns. The input was unacceptable iff it matches none of them.
489
+ def argument_error(name, v, expected)
490
+ ErrorVal.from_runtime(kind: "argument_error", origin: "builtin", operation: "@#{name}", input: v, expected: expected)
353
491
  end
354
492
  end
355
493
  end