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
|
@@ -11,30 +11,16 @@ module Fusion
|
|
|
11
11
|
@interp = interp
|
|
12
12
|
define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
define.call("
|
|
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
|
-
|
|
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
|
|
99
|
+
def math_floor(v)
|
|
55
100
|
return v if v.is_a?(ErrorVal)
|
|
56
|
-
return
|
|
57
|
-
return error("
|
|
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
|
|
104
|
+
v.floor
|
|
61
105
|
end
|
|
62
106
|
|
|
63
|
-
def
|
|
107
|
+
def math_ceil(v)
|
|
64
108
|
return v if v.is_a?(ErrorVal)
|
|
65
|
-
return
|
|
66
|
-
return error("
|
|
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
|
|
112
|
+
v.ceil
|
|
70
113
|
end
|
|
71
114
|
|
|
72
|
-
|
|
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
|
|
75
|
-
return error("
|
|
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]
|
|
122
|
+
v[0].to_f / v[1]
|
|
79
123
|
end
|
|
80
124
|
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
136
|
+
def math_abs(v)
|
|
97
137
|
return v if v.is_a?(ErrorVal)
|
|
98
|
-
return
|
|
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
|
|
140
|
+
v.abs
|
|
104
141
|
end
|
|
105
142
|
|
|
106
|
-
|
|
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
|
|
147
|
+
return rand if v == NULL
|
|
148
|
+
return rand(v) if integer?(v) && v > 0
|
|
109
149
|
|
|
110
|
-
-
|
|
150
|
+
argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
|
|
111
151
|
end
|
|
112
152
|
|
|
113
|
-
def
|
|
153
|
+
def math_sin(v)
|
|
114
154
|
return v if v.is_a?(ErrorVal)
|
|
115
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
191
|
+
Math.log(v)
|
|
128
192
|
end
|
|
129
193
|
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
# ---
|
|
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
|
-
|
|
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
|
|
216
|
+
return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
152
217
|
|
|
153
|
-
|
|
218
|
+
v.sum(0)
|
|
154
219
|
end
|
|
155
220
|
|
|
156
|
-
def
|
|
221
|
+
def op_product(v)
|
|
157
222
|
return v if v.is_a?(ErrorVal)
|
|
158
|
-
return
|
|
223
|
+
return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
159
224
|
|
|
160
|
-
|
|
225
|
+
v.reduce(1, :*)
|
|
161
226
|
end
|
|
162
227
|
|
|
163
|
-
def
|
|
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
|
-
|
|
232
|
+
-v
|
|
167
233
|
end
|
|
168
234
|
|
|
169
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
253
|
+
v[0] / v[1]
|
|
176
254
|
end
|
|
177
255
|
|
|
178
|
-
def
|
|
256
|
+
def op_modulo(v)
|
|
179
257
|
return v if v.is_a?(ErrorVal)
|
|
180
|
-
return
|
|
181
|
-
return error("
|
|
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]
|
|
261
|
+
v[0] % v[1]
|
|
184
262
|
end
|
|
185
263
|
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|