fusion-lang 0.0.1.alpha1 → 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 +19 -6
- data/Rakefile +9 -0
- data/docs/lang/design.md +418 -28
- data/docs/lang/implementation.md +238 -0
- data/docs/lang/roadmap.md +20 -57
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +62 -23
- data/docs/user/reference.md +596 -168
- data/docs/user/tutorial.md +32 -29
- data/examples/double.fsn +1 -1
- data/examples/ends.fsn +4 -0
- data/examples/factorial.fsn +2 -2
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/json_test.fsn +4 -0
- data/examples/palindrome.fsn +2 -1
- data/exe/fusion +17 -44
- data/lib/fusion/ast.rb +97 -0
- data/lib/fusion/atom.rb +17 -0
- data/lib/fusion/cli/decoder.rb +84 -0
- data/lib/fusion/cli/encoder.rb +28 -0
- data/lib/fusion/cli/options.rb +212 -0
- data/lib/fusion/cli/parser.rb +38 -0
- data/lib/fusion/cli/repl.rb +78 -0
- data/lib/fusion/cli/serializer.rb +70 -0
- data/lib/fusion/cli.rb +207 -0
- data/lib/fusion/interpreter/builtins.rb +465 -0
- data/lib/fusion/interpreter/env.rb +89 -0
- data/lib/fusion/interpreter/error_val.rb +71 -0
- data/lib/fusion/interpreter/func.rb +22 -0
- data/lib/fusion/interpreter/native_func.rb +22 -0
- data/lib/fusion/interpreter/thunk.rb +53 -0
- data/lib/fusion/interpreter.rb +752 -0
- data/lib/fusion/lexer.rb +249 -0
- data/lib/fusion/null.rb +9 -0
- data/lib/fusion/parser.rb +542 -0
- data/lib/fusion/token.rb +22 -0
- data/lib/fusion/typed_data.rb +23 -0
- data/lib/fusion/version.rb +1 -1
- data/lib/fusion/wire_pair.rb +11 -0
- data/lib/fusion.rb +11 -1122
- 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 -2
- data/stdlib/range.fsn +2 -1
- data/stdlib/reduce.fsn +8 -0
- data/stdlib/sanitize.fsn +12 -0
- data/stdlib/truthy.fsn +7 -0
- metadata +41 -2
- data/stdlib/math/square.fsn +0 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
|
|
5
|
+
module Fusion
|
|
6
|
+
class Interpreter
|
|
7
|
+
module Builtins
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def install(table, interp)
|
|
11
|
+
@interp = interp
|
|
12
|
+
define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
|
|
13
|
+
|
|
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))
|
|
20
|
+
define.call("join", method(:join))
|
|
21
|
+
define.call("split", method(:split))
|
|
22
|
+
define.call("toString", method(:to_string))
|
|
23
|
+
define.call("parseNumber", method(:parse_number))
|
|
24
|
+
define.call("keys", method(:keys))
|
|
25
|
+
define.call("values", method(:values))
|
|
26
|
+
define.call("toObject", method(:to_object))
|
|
27
|
+
|
|
28
|
+
# type predicates: return false on any non-matching value, never an error
|
|
29
|
+
define.call("Integer", method(:integer?))
|
|
30
|
+
define.call("Float", method(:float?))
|
|
31
|
+
define.call("Number", method(:numeric?))
|
|
32
|
+
define.call("String", method(:string?))
|
|
33
|
+
define.call("Boolean", method(:boolean?))
|
|
34
|
+
define.call("Array", method(:array?))
|
|
35
|
+
define.call("Object", method(:object?))
|
|
36
|
+
define.call("Null", method(:null?))
|
|
37
|
+
define.call("Function", method(:function?))
|
|
38
|
+
define.call("NonFinite", method(:non_finite?))
|
|
39
|
+
|
|
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)
|
|
93
|
+
|
|
94
|
+
v.round
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def math_floor(v)
|
|
98
|
+
return v if v.is_a?(ErrorVal)
|
|
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)
|
|
101
|
+
|
|
102
|
+
v.floor
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def math_ceil(v)
|
|
106
|
+
return v if v.is_a?(ErrorVal)
|
|
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)
|
|
109
|
+
|
|
110
|
+
v.ceil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `divide` always yields a float. Integer division is `@OP.quotient`, the
|
|
114
|
+
# remainder `@OP.modulo`.
|
|
115
|
+
def math_divide(v)
|
|
116
|
+
return v if v.is_a?(ErrorVal)
|
|
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
|
|
119
|
+
|
|
120
|
+
v[0].to_f / v[1]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# -1 / 0 / 1 (via `<`/`>`, so NaN → 0, never Ruby's `nil`).
|
|
124
|
+
def math_sign(v)
|
|
125
|
+
return v if v.is_a?(ErrorVal)
|
|
126
|
+
return argument_error("math.sign", v, NUMBER) unless numeric?(v)
|
|
127
|
+
|
|
128
|
+
if v < 0 then -1
|
|
129
|
+
elsif v > 0 then 1
|
|
130
|
+
else 0
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def math_abs(v)
|
|
135
|
+
return v if v.is_a?(ErrorVal)
|
|
136
|
+
return argument_error("math.abs", v, NUMBER) unless numeric?(v)
|
|
137
|
+
|
|
138
|
+
v.abs
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `null` → a float in `[0.0, 1.0)`; a positive integer `n` → an integer in
|
|
142
|
+
# `[0, n)`.
|
|
143
|
+
def math_rand(v)
|
|
144
|
+
return v if v.is_a?(ErrorVal)
|
|
145
|
+
return rand if v == NULL
|
|
146
|
+
return rand(v) if integer?(v) && v.positive?
|
|
147
|
+
|
|
148
|
+
argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def math_sin(v)
|
|
152
|
+
return v if v.is_a?(ErrorVal)
|
|
153
|
+
return argument_error("math.sin", v, NUMBER) unless numeric?(v)
|
|
154
|
+
|
|
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)
|
|
170
|
+
end
|
|
171
|
+
|
|
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
|
|
181
|
+
|
|
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)
|
|
185
|
+
return v if v.is_a?(ErrorVal)
|
|
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
|
|
188
|
+
|
|
189
|
+
Math.log(v)
|
|
190
|
+
end
|
|
191
|
+
|
|
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)
|
|
196
|
+
return v if v.is_a?(ErrorVal)
|
|
197
|
+
return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
|
|
198
|
+
|
|
199
|
+
a, b = v
|
|
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
|
|
204
|
+
end
|
|
205
|
+
|
|
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.
|
|
212
|
+
|
|
213
|
+
def op_sum(v)
|
|
214
|
+
return v if v.is_a?(ErrorVal)
|
|
215
|
+
return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
216
|
+
|
|
217
|
+
v.sum(0)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def op_product(v)
|
|
221
|
+
return v if v.is_a?(ErrorVal)
|
|
222
|
+
return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
|
|
223
|
+
|
|
224
|
+
v.reduce(1, :*)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def op_negate(v)
|
|
228
|
+
return v if v.is_a?(ErrorVal)
|
|
229
|
+
return argument_error("OP.negate", v, ["_ ? @Number"]) unless numeric?(v)
|
|
230
|
+
|
|
231
|
+
-v
|
|
232
|
+
end
|
|
233
|
+
|
|
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
|
|
242
|
+
|
|
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)
|
|
248
|
+
return v if v.is_a?(ErrorVal)
|
|
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
|
|
251
|
+
|
|
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) }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def op_not(v)
|
|
307
|
+
return v if v.is_a?(ErrorVal)
|
|
308
|
+
|
|
309
|
+
!@interp.truthy?(v)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# --- strings and structure bridges ---
|
|
313
|
+
|
|
314
|
+
def size(v)
|
|
315
|
+
return v if v.is_a?(ErrorVal)
|
|
316
|
+
return argument_error("size", v, ["_ ? @String", "_ ? @Array", "_ ? @Object"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
|
|
317
|
+
|
|
318
|
+
v.length
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# `[items, separator]`: join an array of strings into one string. `@concat`
|
|
322
|
+
# is the stdlib pair-case built on this.
|
|
323
|
+
def join(v)
|
|
324
|
+
return v if v.is_a?(ErrorVal)
|
|
325
|
+
expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
|
|
326
|
+
return argument_error("join", v, expected) unless pair?(v)
|
|
327
|
+
|
|
328
|
+
array, separator = v
|
|
329
|
+
unless array.is_a?(Array) && separator.is_a?(String) && array.all? { |item| item.is_a?(String) }
|
|
330
|
+
return argument_error("join", v, expected)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
array.join(separator)
|
|
334
|
+
end
|
|
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
|
+
|
|
351
|
+
def to_string(v)
|
|
352
|
+
return v if v.is_a?(ErrorVal)
|
|
353
|
+
|
|
354
|
+
case v
|
|
355
|
+
when String then v
|
|
356
|
+
when Integer, Float then v.to_s
|
|
357
|
+
when true then "true"
|
|
358
|
+
when false then "false"
|
|
359
|
+
when NULL then "null"
|
|
360
|
+
else error("conversion_error", "toString", v, "cannot stringify this value type")
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def parse_number(v)
|
|
365
|
+
return v if v.is_a?(ErrorVal)
|
|
366
|
+
return argument_error("parseNumber", v, ["_ ? @String"]) unless v.is_a?(String)
|
|
367
|
+
|
|
368
|
+
case v
|
|
369
|
+
when /\A-?\d+\z/ then v.to_i
|
|
370
|
+
when /\A-?\d+(\.\d+)?([eE][+-]?\d+)?\z/ then v.to_f
|
|
371
|
+
else error("conversion_error", "parseNumber", v, "not a numeric string")
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# --- object key enumeration (Tier 0: patterns can't enumerate unknown keys) ---
|
|
376
|
+
|
|
377
|
+
def keys(v)
|
|
378
|
+
return v if v.is_a?(ErrorVal)
|
|
379
|
+
return argument_error("keys", v, ["_ ? @Object"]) unless v.is_a?(Hash)
|
|
380
|
+
|
|
381
|
+
v.keys
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def values(v)
|
|
385
|
+
return v if v.is_a?(ErrorVal)
|
|
386
|
+
return argument_error("values", v, ["_ ? @Object"]) unless v.is_a?(Hash)
|
|
387
|
+
|
|
388
|
+
v.values
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def to_object(v)
|
|
392
|
+
return v if v.is_a?(ErrorVal)
|
|
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)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
v.to_h
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
private
|
|
403
|
+
|
|
404
|
+
# Type predicates, also reused as internal guards.
|
|
405
|
+
|
|
406
|
+
def integer?(v)
|
|
407
|
+
v.is_a?(Integer) && !boolean?(v)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def float?(v)
|
|
411
|
+
v.is_a?(Float)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def numeric?(v)
|
|
415
|
+
v.is_a?(Numeric) && !boolean?(v)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def string?(v)
|
|
419
|
+
v.is_a?(String)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def boolean?(v)
|
|
423
|
+
v == true || v == false
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def array?(v)
|
|
427
|
+
v.is_a?(Array)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def object?(v)
|
|
431
|
+
v.is_a?(Hash)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def null?(v)
|
|
435
|
+
v == NULL
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def function?(v)
|
|
439
|
+
v.is_a?(Func) || v.is_a?(NativeFunc)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def pair?(v)
|
|
443
|
+
v.is_a?(Array) && v.length == 2
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def non_finite?(v)
|
|
447
|
+
v.is_a?(Float) && !v.finite?
|
|
448
|
+
end
|
|
449
|
+
|
|
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}`).
|
|
454
|
+
def error(kind, name, v, message)
|
|
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)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# Environment: maps names -> values, with a parent chain. Built-ins live at root.
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class Env
|
|
10
|
+
# Raised by #bind when a name is already bound in this env's own scope —
|
|
11
|
+
# i.e. a duplicate pattern binder like `[a, a]`. Interpreter#apply catches
|
|
12
|
+
# it and reports a binding_error (see docs/user/reference.md §6.5).
|
|
13
|
+
class DuplicateBinding < StandardError
|
|
14
|
+
attr_reader :name
|
|
15
|
+
|
|
16
|
+
def initialize(name)
|
|
17
|
+
@name = name
|
|
18
|
+
super("identifier already bound: #{name}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :parent
|
|
23
|
+
|
|
24
|
+
def initialize(parent = nil)
|
|
25
|
+
@vars = {} # pattern bindings, keyed by identifier
|
|
26
|
+
@context = {} # hidden interpreter context, keyed by symbol
|
|
27
|
+
@parent = parent
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def child
|
|
31
|
+
Env.new(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
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
|
|
47
|
+
|
|
48
|
+
@vars[name] = value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def lookup(name)
|
|
52
|
+
if @vars.key?(name)
|
|
53
|
+
@vars[name]
|
|
54
|
+
elsif @parent
|
|
55
|
+
@parent.lookup(name)
|
|
56
|
+
else
|
|
57
|
+
:__unbound__
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
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
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# An error value, always carrying a payload (any JSON-like Fusion value).
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class ErrorVal
|
|
10
|
+
attr_reader :payload
|
|
11
|
+
|
|
12
|
+
def initialize(payload, runtime: false)
|
|
13
|
+
@payload = payload
|
|
14
|
+
@runtime = runtime
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Attach the call-site `file` to a runtime error. Idempotent.
|
|
18
|
+
def with_call_site(file)
|
|
19
|
+
# Only stamp runtime-produced errors. After this check we are sure that the payload wasn't user-constructed.
|
|
20
|
+
return self unless @runtime
|
|
21
|
+
raise Unreachable, "Unexpected runtime error payload: #{@payload}" unless @payload.is_a?(Hash)
|
|
22
|
+
# Don't double stamp. Idempotency.
|
|
23
|
+
return self if @payload.key?("file")
|
|
24
|
+
# Only stamp certain errors.
|
|
25
|
+
return self unless ["builtin", "stdlib"].include?(@payload["origin"])
|
|
26
|
+
|
|
27
|
+
# Insert "file" after "origin"
|
|
28
|
+
reordered = {}
|
|
29
|
+
@payload.each do |key, value|
|
|
30
|
+
reordered[key] = value
|
|
31
|
+
reordered["file"] = file if key == "origin"
|
|
32
|
+
end
|
|
33
|
+
@payload = reordered
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Was this error runtime-produced (as opposed to user-constructed via `!expr`)?
|
|
38
|
+
# Runtime errors use lenient serialization (docs/user/reference.md §9.3) and
|
|
39
|
+
# get a call-site `file` stamped.
|
|
40
|
+
def runtime?
|
|
41
|
+
@runtime
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build a runtime-produced error with the standardized payload shape
|
|
45
|
+
# documented in docs/user/reference.md §6.5.
|
|
46
|
+
def self.from_runtime(kind:, origin:, operation:, input:, file: nil, expected: nil, message: nil)
|
|
47
|
+
raise Unreachable, "an error with `expected` must not also carry a `message`" if expected && message
|
|
48
|
+
|
|
49
|
+
received_error = input.is_a?(ErrorVal)
|
|
50
|
+
|
|
51
|
+
payload = { "kind" => kind, "origin" => origin }
|
|
52
|
+
payload["file"] = file if file
|
|
53
|
+
payload["operation"] = operation
|
|
54
|
+
payload["status"] = received_error ? 1 : 0
|
|
55
|
+
payload["input"] = received_error ? input.payload : input
|
|
56
|
+
payload["expected"] = expected if expected
|
|
57
|
+
payload["message"] = message if message
|
|
58
|
+
|
|
59
|
+
new(payload, runtime: true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inspect
|
|
63
|
+
"!#{payload.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
inspect
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# A function closes over the environment in which it was defined.
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class Func
|
|
10
|
+
attr_reader :clauses, :env
|
|
11
|
+
|
|
12
|
+
def initialize(clauses, env)
|
|
13
|
+
@clauses = clauses # [AST::Clause, ...]
|
|
14
|
+
@env = env
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect
|
|
18
|
+
"<func/#{clauses.length}>"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# A native (Ruby-implemented) function. Apply treats it like a Func.
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class NativeFunc
|
|
10
|
+
attr_reader :name, :fn
|
|
11
|
+
|
|
12
|
+
def initialize(name, fn)
|
|
13
|
+
@name = name
|
|
14
|
+
@fn = fn
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect
|
|
18
|
+
"<builtin #{name}>"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|