fusion-lang 0.0.1.alpha2 → 0.0.1
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/README.md +9 -8
- data/docs/lang/design.md +240 -51
- data/docs/lang/implementation.md +238 -0
- data/docs/lang/roadmap.md +20 -36
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +60 -15
- data/docs/user/reference.md +356 -142
- data/docs/user/tutorial.md +21 -19
- data/examples/double.fsn +1 -1
- data/examples/factorial.fsn +2 -2
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/json_test.fsn +4 -0
- data/examples/palindrome.fsn +1 -1
- data/exe/fusion +10 -10
- data/lib/fusion/ast.rb +2 -1
- data/lib/fusion/cli/decoder.rb +10 -5
- data/lib/fusion/cli/options.rb +130 -60
- data/lib/fusion/cli/parser.rb +3 -3
- data/lib/fusion/cli/repl.rb +30 -25
- data/lib/fusion/cli/serializer.rb +5 -4
- data/lib/fusion/cli.rb +119 -48
- data/lib/fusion/interpreter/builtins.rb +260 -151
- 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 +239 -82
- data/lib/fusion/lexer.rb +69 -3
- data/lib/fusion/parser.rb +189 -51
- data/lib/fusion/version.rb +1 -1
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +6 -0
- data/stdlib/concat.fsn +5 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/gt.fsn +9 -0
- data/stdlib/gte.fsn +9 -0
- data/stdlib/lt.fsn +9 -0
- data/stdlib/lte.fsn +9 -0
- data/stdlib/map.fsn +6 -4
- data/stdlib/range.fsn +2 -2
- data/stdlib/reduce.fsn +8 -0
- data/stdlib/sanitize.fsn +2 -2
- data/stdlib/truthy.fsn +7 -0
- metadata +18 -4
- data/lib/fusion/interpreter/file_thunk.rb +0 -39
- data/stdlib/mapValues.fsn +0 -5
- data/stdlib/math/square.fsn +0 -4
|
@@ -11,29 +11,18 @@ module Fusion
|
|
|
11
11
|
@interp = interp
|
|
12
12
|
define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
define.call("
|
|
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; their friendly
|
|
16
|
+
# derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files that build on
|
|
17
|
+
# `@OP.*`, so they follow a per-directory `@OP` override. Numeric functions
|
|
18
|
+
# (`round`, `divide`, `sin`, …) and constants (`pi`, `e`) live in `@math`.
|
|
19
|
+
define.call("size", method(:size))
|
|
30
20
|
define.call("join", method(:join))
|
|
21
|
+
define.call("split", method(:split))
|
|
31
22
|
define.call("toString", method(:to_string))
|
|
32
23
|
define.call("parseNumber", method(:parse_number))
|
|
33
24
|
define.call("keys", method(:keys))
|
|
34
25
|
define.call("values", method(:values))
|
|
35
|
-
define.call("get", method(:get))
|
|
36
|
-
define.call("set", method(:set))
|
|
37
26
|
define.call("toObject", method(:to_object))
|
|
38
27
|
|
|
39
28
|
# type predicates: return false on any non-matching value, never an error
|
|
@@ -47,161 +36,318 @@ module Fusion
|
|
|
47
36
|
define.call("Null", method(:null?))
|
|
48
37
|
define.call("Function", method(:function?))
|
|
49
38
|
define.call("NonFinite", method(:non_finite?))
|
|
50
|
-
end
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
# `OP` bundles the sugar-target operators as a shadowable builtin object,
|
|
41
|
+
# reached as `@OP.sum`, `@OP.and`, … A directory can swap the operators by
|
|
42
|
+
# placing an `OP.fsn` sibling that overrides members (reaching the
|
|
43
|
+
# originals with `@@`); infix sugar and the derived stdlib helpers resolve
|
|
44
|
+
# `@OP` per directory, so they follow the override.
|
|
45
|
+
table["OP"] = {
|
|
46
|
+
"sum" => NativeFunc.new("OP.sum", method(:op_sum)),
|
|
47
|
+
"product" => NativeFunc.new("OP.product", method(:op_product)),
|
|
48
|
+
"negate" => NativeFunc.new("OP.negate", method(:op_negate)),
|
|
49
|
+
"invert" => NativeFunc.new("OP.invert", method(:op_invert)),
|
|
50
|
+
"quotient" => NativeFunc.new("OP.quotient", method(:op_quotient)),
|
|
51
|
+
"modulo" => NativeFunc.new("OP.modulo", method(:op_modulo)),
|
|
52
|
+
"equal" => NativeFunc.new("OP.equal", method(:op_equal)),
|
|
53
|
+
"compare" => NativeFunc.new("OP.compare", method(:op_compare)),
|
|
54
|
+
"and" => NativeFunc.new("OP.and", method(:op_and)),
|
|
55
|
+
"or" => NativeFunc.new("OP.or", method(:op_or)),
|
|
56
|
+
"not" => NativeFunc.new("OP.not", method(:op_not)),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# `math` bundles numeric functions and constants, reached as `@math.round`,
|
|
60
|
+
# `@math.pi`, … Like `@OP`, it is a shadowable builtin object. `pi`/`e` are
|
|
61
|
+
# plain values; the rest are one-argument functions.
|
|
62
|
+
table["math"] = {
|
|
63
|
+
"round" => NativeFunc.new("math.round", method(:math_round)),
|
|
64
|
+
"floor" => NativeFunc.new("math.floor", method(:math_floor)),
|
|
65
|
+
"ceil" => NativeFunc.new("math.ceil", method(:math_ceil)),
|
|
66
|
+
"divide" => NativeFunc.new("math.divide", method(:math_divide)),
|
|
67
|
+
"sign" => NativeFunc.new("math.sign", method(:math_sign)),
|
|
68
|
+
"abs" => NativeFunc.new("math.abs", method(:math_abs)),
|
|
69
|
+
"rand" => NativeFunc.new("math.rand", method(:math_rand)),
|
|
70
|
+
"sin" => NativeFunc.new("math.sin", method(:math_sin)),
|
|
71
|
+
"cos" => NativeFunc.new("math.cos", method(:math_cos)),
|
|
72
|
+
"exp" => NativeFunc.new("math.exp", method(:math_exp)),
|
|
73
|
+
"log" => NativeFunc.new("math.log", method(:math_log)),
|
|
74
|
+
"pow" => NativeFunc.new("math.pow", method(:math_pow)),
|
|
75
|
+
"sqrt" => NativeFunc.new("math.sqrt", method(:math_sqrt)),
|
|
76
|
+
"pi" => Math::PI,
|
|
77
|
+
"e" => Math::E,
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- math (reached as `@math.round`, `@math.divide`, `@math.pi`, …) ---
|
|
82
|
+
|
|
83
|
+
NUMBER_PAIR = ["[_ ? @Number, _ ? @Number]"].freeze
|
|
84
|
+
NUMBER_ARRAY = ['_ ? (xs => {"c": xs, "f": @Number} | @all)'].freeze
|
|
85
|
+
NUMBER = ["_ ? @Number"].freeze
|
|
86
|
+
|
|
87
|
+
# `round`/`floor`/`ceil` return an integer; a non-finite input is a math_error
|
|
88
|
+
# (their Ruby forms raise `FloatDomainError` on it).
|
|
89
|
+
def math_round(v)
|
|
90
|
+
return v if v.is_a?(ErrorVal)
|
|
91
|
+
return argument_error("math.round", v, NUMBER) unless numeric?(v)
|
|
92
|
+
return error("math_error", "math.round", v, "not a finite number") if non_finite?(v)
|
|
53
93
|
|
|
54
|
-
|
|
94
|
+
v.round
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def math_floor(v)
|
|
55
98
|
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])
|
|
99
|
+
return argument_error("math.floor", v, NUMBER) unless numeric?(v)
|
|
100
|
+
return error("math_error", "math.floor", v, "not a finite number") if non_finite?(v)
|
|
59
101
|
|
|
60
|
-
v
|
|
102
|
+
v.floor
|
|
61
103
|
end
|
|
62
104
|
|
|
63
|
-
def
|
|
105
|
+
def math_ceil(v)
|
|
64
106
|
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])
|
|
107
|
+
return argument_error("math.ceil", v, NUMBER) unless numeric?(v)
|
|
108
|
+
return error("math_error", "math.ceil", v, "not a finite number") if non_finite?(v)
|
|
68
109
|
|
|
69
|
-
v
|
|
110
|
+
v.ceil
|
|
70
111
|
end
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
# `divide` always yields a float. Integer division is `@OP.quotient`, the
|
|
114
|
+
# remainder `@OP.modulo`.
|
|
115
|
+
def math_divide(v)
|
|
73
116
|
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])
|
|
117
|
+
return argument_error("math.divide", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
|
|
118
|
+
return error("math_error", "math.divide", v, "division by zero") if v[1] == 0
|
|
77
119
|
|
|
78
|
-
v[0]
|
|
120
|
+
v[0].to_f / v[1]
|
|
79
121
|
end
|
|
80
122
|
|
|
81
|
-
|
|
123
|
+
# -1 / 0 / 1 (via `<`/`>`, so NaN → 0, never Ruby's `nil`).
|
|
124
|
+
def math_sign(v)
|
|
82
125
|
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
|
|
126
|
+
return argument_error("math.sign", v, NUMBER) unless numeric?(v)
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
else
|
|
92
|
-
a.to_f / b
|
|
128
|
+
if v < 0 then -1
|
|
129
|
+
elsif v > 0 then 1
|
|
130
|
+
else 0
|
|
93
131
|
end
|
|
94
132
|
end
|
|
95
133
|
|
|
96
|
-
def
|
|
134
|
+
def math_abs(v)
|
|
97
135
|
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
|
|
136
|
+
return argument_error("math.abs", v, NUMBER) unless numeric?(v)
|
|
102
137
|
|
|
103
|
-
v
|
|
138
|
+
v.abs
|
|
104
139
|
end
|
|
105
140
|
|
|
106
|
-
|
|
141
|
+
# `null` → a float in `[0.0, 1.0)`; a positive integer `n` → an integer in
|
|
142
|
+
# `[0, n)`.
|
|
143
|
+
def math_rand(v)
|
|
107
144
|
return v if v.is_a?(ErrorVal)
|
|
108
|
-
return
|
|
145
|
+
return rand if v == NULL
|
|
146
|
+
return rand(v) if integer?(v) && v.positive?
|
|
109
147
|
|
|
110
|
-
-
|
|
148
|
+
argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
|
|
111
149
|
end
|
|
112
150
|
|
|
113
|
-
def
|
|
151
|
+
def math_sin(v)
|
|
114
152
|
return v if v.is_a?(ErrorVal)
|
|
115
|
-
return
|
|
116
|
-
return error("math_error", "floor", v, "not a finite number") if non_finite?(v)
|
|
153
|
+
return argument_error("math.sin", v, NUMBER) unless numeric?(v)
|
|
117
154
|
|
|
118
|
-
v
|
|
155
|
+
Math.sin(v)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def math_cos(v)
|
|
159
|
+
return v if v.is_a?(ErrorVal)
|
|
160
|
+
return argument_error("math.cos", v, NUMBER) unless numeric?(v)
|
|
161
|
+
|
|
162
|
+
Math.cos(v)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def math_exp(v)
|
|
166
|
+
return v if v.is_a?(ErrorVal)
|
|
167
|
+
return argument_error("math.exp", v, NUMBER) unless numeric?(v)
|
|
168
|
+
|
|
169
|
+
Math.exp(v)
|
|
119
170
|
end
|
|
120
171
|
|
|
121
|
-
#
|
|
172
|
+
# Square root; the domain is non-negative numbers (a negative would be
|
|
173
|
+
# complex).
|
|
174
|
+
def math_sqrt(v)
|
|
175
|
+
return v if v.is_a?(ErrorVal)
|
|
176
|
+
return argument_error("math.sqrt", v, NUMBER) unless numeric?(v)
|
|
177
|
+
return error("math_error", "math.sqrt", v, "square root of a negative number") if v < 0
|
|
178
|
+
|
|
179
|
+
Math.sqrt(v)
|
|
180
|
+
end
|
|
122
181
|
|
|
123
|
-
|
|
182
|
+
# Natural logarithm; the domain is positive numbers (`Math.log` raises on a
|
|
183
|
+
# negative and gives `-Infinity` at 0).
|
|
184
|
+
def math_log(v)
|
|
124
185
|
return v if v.is_a?(ErrorVal)
|
|
125
|
-
return
|
|
186
|
+
return argument_error("math.log", v, NUMBER) unless numeric?(v)
|
|
187
|
+
return error("math_error", "math.log", v, "log of a non-positive number") if v <= 0
|
|
126
188
|
|
|
127
|
-
|
|
189
|
+
Math.log(v)
|
|
128
190
|
end
|
|
129
191
|
|
|
130
|
-
|
|
192
|
+
# `base ** exponent`: integer when the base and a non-negative integer
|
|
193
|
+
# exponent are integers, a float otherwise. A negative base with a fractional
|
|
194
|
+
# exponent (a complex result) is a math_error.
|
|
195
|
+
def math_pow(v)
|
|
131
196
|
return v if v.is_a?(ErrorVal)
|
|
132
|
-
return
|
|
197
|
+
return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
|
|
133
198
|
|
|
134
199
|
a, b = v
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
else
|
|
140
|
-
error("type_error", "lessThan", v, "expected two numbers or two strings")
|
|
141
|
-
end
|
|
200
|
+
result = a.is_a?(Integer) && b.is_a?(Integer) && !b.negative? ? a**b : a.to_f**b
|
|
201
|
+
return error("math_error", "math.pow", v, "not in domain (complex result)") if result.is_a?(Complex)
|
|
202
|
+
|
|
203
|
+
result
|
|
142
204
|
end
|
|
143
205
|
|
|
144
|
-
# ---
|
|
206
|
+
# --- OP: the sugar-target operators (reached as `@OP.sum`, `@OP.and`, …) ---
|
|
207
|
+
#
|
|
208
|
+
# The arithmetic, boolean, and equality members take an array of ANY length;
|
|
209
|
+
# the unary ones take a single value; `compare` returns -1 / 0 / 1. The
|
|
210
|
+
# friendly derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files built on
|
|
211
|
+
# these.
|
|
145
212
|
|
|
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)
|
|
213
|
+
def op_sum(v)
|
|
150
214
|
return v if v.is_a?(ErrorVal)
|
|
151
|
-
return
|
|
215
|
+
return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
152
216
|
|
|
153
|
-
|
|
217
|
+
v.sum(0)
|
|
154
218
|
end
|
|
155
219
|
|
|
156
|
-
def
|
|
220
|
+
def op_product(v)
|
|
157
221
|
return v if v.is_a?(ErrorVal)
|
|
158
|
-
return
|
|
222
|
+
return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
159
223
|
|
|
160
|
-
|
|
224
|
+
v.reduce(1, :*)
|
|
161
225
|
end
|
|
162
226
|
|
|
163
|
-
def
|
|
227
|
+
def op_negate(v)
|
|
164
228
|
return v if v.is_a?(ErrorVal)
|
|
229
|
+
return argument_error("OP.negate", v, ["_ ? @Number"]) unless numeric?(v)
|
|
165
230
|
|
|
166
|
-
|
|
231
|
+
-v
|
|
167
232
|
end
|
|
168
233
|
|
|
169
|
-
#
|
|
234
|
+
# The unary reciprocal 1/x, always a float; 0 is a math_error.
|
|
235
|
+
def op_invert(v)
|
|
236
|
+
return v if v.is_a?(ErrorVal)
|
|
237
|
+
return argument_error("OP.invert", v, ["_ ? @Number"]) unless numeric?(v)
|
|
238
|
+
return error("math_error", "OP.invert", v, "division by zero") if v == 0
|
|
239
|
+
|
|
240
|
+
1.0 / v
|
|
241
|
+
end
|
|
170
242
|
|
|
171
|
-
|
|
243
|
+
INTEGER_PAIR = ["[_ ? @Integer, _ ? @Integer]"].freeze
|
|
244
|
+
|
|
245
|
+
# Integer division and its remainder — integers only (`@divide` handles the
|
|
246
|
+
# float case). Ruby's `/` and `%` agree in sign, so `q*b + r == a` holds.
|
|
247
|
+
def op_quotient(v)
|
|
172
248
|
return v if v.is_a?(ErrorVal)
|
|
173
|
-
return
|
|
249
|
+
return argument_error("OP.quotient", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
|
|
250
|
+
return error("math_error", "OP.quotient", v, "division by zero") if v[1] == 0
|
|
174
251
|
|
|
175
|
-
v
|
|
252
|
+
v[0] / v[1]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def op_modulo(v)
|
|
256
|
+
return v if v.is_a?(ErrorVal)
|
|
257
|
+
return argument_error("OP.modulo", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
|
|
258
|
+
return error("math_error", "OP.modulo", v, "modulo by zero") if v[1] == 0
|
|
259
|
+
|
|
260
|
+
v[0] % v[1]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Deep, exact equality across the whole array: true iff every element
|
|
264
|
+
# equals the first (so 0 and 1 elements are vacuously equal). Any types.
|
|
265
|
+
def op_equal(v)
|
|
266
|
+
return v if v.is_a?(ErrorVal)
|
|
267
|
+
return argument_error("OP.equal", v, ["_ ? @Array"]) unless v.is_a?(Array)
|
|
268
|
+
|
|
269
|
+
v.all? { |x| @interp.deep_equal?(v[0], x) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
COMPARE_PAIR = ["[_ ? @Number, _ ? @Number]", "[_ ? @String, _ ? @String]"].freeze
|
|
273
|
+
|
|
274
|
+
# Order two numbers or two strings: -1, 0, or 1 (no deep equality). Built on
|
|
275
|
+
# `<` rather than `<=>` so a NaN operand yields 0 (unordered), never Ruby's
|
|
276
|
+
# `nil` — NaN is a reachable value (`Infinity - Infinity`).
|
|
277
|
+
def op_compare(v)
|
|
278
|
+
return v if v.is_a?(ErrorVal)
|
|
279
|
+
return argument_error("OP.compare", v, COMPARE_PAIR) unless pair?(v)
|
|
280
|
+
|
|
281
|
+
a, b = v
|
|
282
|
+
unless (numeric?(a) && numeric?(b)) || (a.is_a?(String) && b.is_a?(String))
|
|
283
|
+
return argument_error("OP.compare", v, COMPARE_PAIR)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if a < b then -1
|
|
287
|
+
elsif b < a then 1
|
|
288
|
+
else 0
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def op_and(v)
|
|
293
|
+
return v if v.is_a?(ErrorVal)
|
|
294
|
+
return argument_error("OP.and", v, ["_ ? @Array"]) unless v.is_a?(Array)
|
|
295
|
+
|
|
296
|
+
v.all? { |x| @interp.truthy?(x) }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def op_or(v)
|
|
300
|
+
return v if v.is_a?(ErrorVal)
|
|
301
|
+
return argument_error("OP.or", v, ["_ ? @Array"]) unless v.is_a?(Array)
|
|
302
|
+
|
|
303
|
+
v.any? { |x| @interp.truthy?(x) }
|
|
176
304
|
end
|
|
177
305
|
|
|
178
|
-
def
|
|
306
|
+
def op_not(v)
|
|
179
307
|
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)
|
|
182
308
|
|
|
183
|
-
v
|
|
309
|
+
!@interp.truthy?(v)
|
|
184
310
|
end
|
|
185
311
|
|
|
186
|
-
|
|
312
|
+
# --- strings and structure bridges ---
|
|
313
|
+
|
|
314
|
+
def size(v)
|
|
187
315
|
return v if v.is_a?(ErrorVal)
|
|
188
|
-
return
|
|
316
|
+
return argument_error("size", v, ["_ ? @String", "_ ? @Array", "_ ? @Object"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
|
|
189
317
|
|
|
190
|
-
v.
|
|
318
|
+
v.length
|
|
191
319
|
end
|
|
192
320
|
|
|
321
|
+
# `[items, separator]`: join an array of strings into one string. `@concat`
|
|
322
|
+
# is the stdlib pair-case built on this.
|
|
193
323
|
def join(v)
|
|
194
324
|
return v if v.is_a?(ErrorVal)
|
|
195
|
-
|
|
325
|
+
expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
|
|
326
|
+
return argument_error("join", v, expected) unless pair?(v)
|
|
196
327
|
|
|
197
328
|
array, separator = v
|
|
198
329
|
unless array.is_a?(Array) && separator.is_a?(String) && array.all? { |item| item.is_a?(String) }
|
|
199
|
-
return
|
|
330
|
+
return argument_error("join", v, expected)
|
|
200
331
|
end
|
|
201
332
|
|
|
202
333
|
array.join(separator)
|
|
203
334
|
end
|
|
204
335
|
|
|
336
|
+
# `[string, separator]`: the inverse of `@join`. Splits on the LITERAL
|
|
337
|
+
# separator (Ruby's `" "` whitespace special-case does not apply) and keeps
|
|
338
|
+
# empty fields; an empty separator splits into characters. `@chars` is the
|
|
339
|
+
# stdlib single-string case built on this.
|
|
340
|
+
def split(v)
|
|
341
|
+
return v if v.is_a?(ErrorVal)
|
|
342
|
+
expected = ["[_ ? @String, _ ? @String]"]
|
|
343
|
+
return argument_error("split", v, expected) unless pair?(v) && v[0].is_a?(String) && v[1].is_a?(String)
|
|
344
|
+
|
|
345
|
+
string, separator = v
|
|
346
|
+
return string.chars if separator.empty?
|
|
347
|
+
|
|
348
|
+
string.split(Regexp.new(Regexp.escape(separator)), -1)
|
|
349
|
+
end
|
|
350
|
+
|
|
205
351
|
def to_string(v)
|
|
206
352
|
return v if v.is_a?(ErrorVal)
|
|
207
353
|
|
|
@@ -217,7 +363,7 @@ module Fusion
|
|
|
217
363
|
|
|
218
364
|
def parse_number(v)
|
|
219
365
|
return v if v.is_a?(ErrorVal)
|
|
220
|
-
return
|
|
366
|
+
return argument_error("parseNumber", v, ["_ ? @String"]) unless v.is_a?(String)
|
|
221
367
|
|
|
222
368
|
case v
|
|
223
369
|
when /\A-?\d+\z/ then v.to_i
|
|
@@ -230,64 +376,24 @@ module Fusion
|
|
|
230
376
|
|
|
231
377
|
def keys(v)
|
|
232
378
|
return v if v.is_a?(ErrorVal)
|
|
233
|
-
return
|
|
379
|
+
return argument_error("keys", v, ["_ ? @Object"]) unless v.is_a?(Hash)
|
|
234
380
|
|
|
235
381
|
v.keys
|
|
236
382
|
end
|
|
237
383
|
|
|
238
384
|
def values(v)
|
|
239
385
|
return v if v.is_a?(ErrorVal)
|
|
240
|
-
return
|
|
386
|
+
return argument_error("values", v, ["_ ? @Object"]) unless v.is_a?(Hash)
|
|
241
387
|
|
|
242
388
|
v.values
|
|
243
389
|
end
|
|
244
390
|
|
|
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
391
|
def to_object(v)
|
|
287
392
|
return v if v.is_a?(ErrorVal)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
393
|
+
# Each entry must be a [string, _] pair.
|
|
394
|
+
expected = ['_ ? (xs => {"c": xs, "f": ([_ ? @String, _] => true)} | @all)']
|
|
395
|
+
unless v.is_a?(Array) && v.all? { |entry| pair?(entry) && entry[0].is_a?(String) }
|
|
396
|
+
return argument_error("toObject", v, expected)
|
|
291
397
|
end
|
|
292
398
|
|
|
293
399
|
v.to_h
|
|
@@ -341,15 +447,18 @@ module Fusion
|
|
|
341
447
|
v.is_a?(Float) && !v.finite?
|
|
342
448
|
end
|
|
343
449
|
|
|
344
|
-
# Build a standardized interpreter error
|
|
450
|
+
# Build a standardized interpreter error carrying a human-readable
|
|
451
|
+
# `message` (see docs/user/reference.md §6.5). Use this for failures that
|
|
452
|
+
# aren't an input-shape mismatch (math, conversion, access). `operation`
|
|
453
|
+
# is the builtin's own `@`-reference (`@#{name}`).
|
|
345
454
|
def error(kind, name, v, message)
|
|
346
|
-
ErrorVal.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
455
|
+
ErrorVal.from_runtime(kind: kind, origin: "builtin", operation: "@#{name}", input: v, message: message)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Build an `argument_error` describing the acceptable inputs as a list of
|
|
459
|
+
# Fusion patterns. The input was unacceptable iff it matches none of them.
|
|
460
|
+
def argument_error(name, v, expected)
|
|
461
|
+
ErrorVal.from_runtime(kind: "argument_error", origin: "builtin", operation: "@#{name}", input: v, expected: expected)
|
|
353
462
|
end
|
|
354
463
|
end
|
|
355
464
|
end
|
|
@@ -22,21 +22,28 @@ module Fusion
|
|
|
22
22
|
attr_reader :parent
|
|
23
23
|
|
|
24
24
|
def initialize(parent = nil)
|
|
25
|
-
@vars = {}
|
|
25
|
+
@vars = {} # pattern bindings, keyed by identifier
|
|
26
|
+
@context = {} # hidden interpreter context, keyed by symbol
|
|
26
27
|
@parent = parent
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@vars[name] = value
|
|
32
|
-
self
|
|
30
|
+
def child
|
|
31
|
+
Env.new(self)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# The topmost ancestor — the binding-free root a run is built on. The
|
|
35
|
+
# interpreter loads files relative to it, so they stay isolated.
|
|
36
|
+
def root
|
|
37
|
+
@parent ? @parent.root : self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Pattern bindings:
|
|
41
|
+
# - Shadowing a binding from a parent Env is always allowed.
|
|
42
|
+
# - A duplicate identifier in the same Env is usually an error, but allowed on the REPL.
|
|
43
|
+
def bind(name, value, checked: true)
|
|
44
|
+
if checked && @vars.key?(name)
|
|
45
|
+
raise DuplicateBinding, name
|
|
46
|
+
end
|
|
40
47
|
|
|
41
48
|
@vars[name] = value
|
|
42
49
|
end
|
|
@@ -51,8 +58,31 @@ module Fusion
|
|
|
51
58
|
end
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
# Hidden interpreter context:
|
|
62
|
+
# - `:dir`: the directory @-references resolve against (a path String).
|
|
63
|
+
# - `:file`: the current file's absolute path, used for error origins (a
|
|
64
|
+
# String; absent for inline/REPL code, which reports as origin
|
|
65
|
+
# "code" with file "<inline>").
|
|
66
|
+
# - `:self`: the current top-level unit's own Thunk, used for recursion via a bare `@`.
|
|
67
|
+
# - `:jail`: the run's jail root confining @-resolution (an absolute path
|
|
68
|
+
# String, or nil for unconfined). Set once on the root and, unlike
|
|
69
|
+
# the others, never overridden by a descendant.
|
|
70
|
+
# - `:call_site`: the innermost user-code `file` a stdlib body borrows for its
|
|
71
|
+
# errors (a String). Set on a stdlib function's clause env in
|
|
72
|
+
# Interpreter#apply; user/inline code derives its own and omits it.
|
|
73
|
+
def set_context(key, value)
|
|
74
|
+
@context[key] = value
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def context(key)
|
|
79
|
+
if @context.key?(key)
|
|
80
|
+
@context[key]
|
|
81
|
+
elsif @parent
|
|
82
|
+
@parent.context(key)
|
|
83
|
+
else
|
|
84
|
+
:__unbound__
|
|
85
|
+
end
|
|
56
86
|
end
|
|
57
87
|
end
|
|
58
88
|
end
|