fusion-lang 0.0.1.alpha1 → 0.0.1.alpha2
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 +12 -0
- data/Rakefile +9 -0
- data/docs/lang/design.md +204 -3
- data/docs/lang/roadmap.md +1 -22
- data/docs/user/how-to-guides.md +5 -11
- data/docs/user/reference.md +342 -128
- data/docs/user/tutorial.md +11 -10
- data/examples/ends.fsn +4 -0
- data/examples/palindrome.fsn +2 -1
- data/exe/fusion +15 -42
- data/lib/fusion/ast.rb +96 -0
- data/lib/fusion/atom.rb +17 -0
- data/lib/fusion/cli/decoder.rb +79 -0
- data/lib/fusion/cli/encoder.rb +28 -0
- data/lib/fusion/cli/options.rb +142 -0
- data/lib/fusion/cli/parser.rb +38 -0
- data/lib/fusion/cli/repl.rb +73 -0
- data/lib/fusion/cli/serializer.rb +69 -0
- data/lib/fusion/cli.rb +136 -0
- data/lib/fusion/interpreter/builtins.rb +356 -0
- data/lib/fusion/interpreter/env.rb +59 -0
- data/lib/fusion/interpreter/error_val.rb +49 -0
- data/lib/fusion/interpreter/file_thunk.rb +39 -0
- data/lib/fusion/interpreter/func.rb +22 -0
- data/lib/fusion/interpreter/native_func.rb +22 -0
- data/lib/fusion/interpreter.rb +595 -0
- data/lib/fusion/lexer.rb +183 -0
- data/lib/fusion/null.rb +9 -0
- data/lib/fusion/parser.rb +404 -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/map.fsn +3 -1
- data/stdlib/mapValues.fsn +5 -0
- data/stdlib/math/square.fsn +4 -1
- data/stdlib/range.fsn +2 -1
- data/stdlib/sanitize.fsn +12 -0
- metadata +26 -1
data/lib/fusion/cli.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === CLI tools ===
|
|
4
|
+
#
|
|
5
|
+
# Core data types:
|
|
6
|
+
# - WirePair
|
|
7
|
+
# - Fusion runtime value
|
|
8
|
+
|
|
9
|
+
require_relative "wire_pair"
|
|
10
|
+
require_relative "cli/options"
|
|
11
|
+
|
|
12
|
+
require_relative "cli/decoder"
|
|
13
|
+
require_relative "cli/parser"
|
|
14
|
+
require_relative "interpreter"
|
|
15
|
+
require_relative "cli/serializer"
|
|
16
|
+
require_relative "cli/encoder"
|
|
17
|
+
|
|
18
|
+
require_relative "cli/repl"
|
|
19
|
+
|
|
20
|
+
module Fusion
|
|
21
|
+
module CLI
|
|
22
|
+
extend self
|
|
23
|
+
|
|
24
|
+
# Run the use case selected on the command line.
|
|
25
|
+
def run(options)
|
|
26
|
+
case options.use_case
|
|
27
|
+
when :pipe then run_pipe(options)
|
|
28
|
+
when :stream then run_stream(options)
|
|
29
|
+
when :repl then Repl.new.run
|
|
30
|
+
else raise Unreachable, "Unknown use case #{options.use_case}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# pipe: load the program, pipe one input through it, emit one output.
|
|
35
|
+
def run_pipe(options)
|
|
36
|
+
function = load_program(options)
|
|
37
|
+
input = parse(load_input(options))
|
|
38
|
+
output = apply(input, function)
|
|
39
|
+
emit_output(serialize(output), output_mode: options.output_mode)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# stream: load the program once, then treat stdin/stdout as NDJSON streams —
|
|
43
|
+
# one input per line, one output line per input. Errors stay in-band (the
|
|
44
|
+
# unix mode is unavailable here), so the exit code is always 0.
|
|
45
|
+
def run_stream(options)
|
|
46
|
+
function = load_program(options)
|
|
47
|
+
$stdout.sync = true
|
|
48
|
+
$stdin.each_line do |line|
|
|
49
|
+
next if line.strip.empty?
|
|
50
|
+
|
|
51
|
+
input = parse(decode(line, mode: options.input_mode))
|
|
52
|
+
output = apply(input, function)
|
|
53
|
+
$stdout.puts(encode(serialize(output), mode: options.output_mode))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# String -> WirePair
|
|
58
|
+
# Doesn't support mode `:unix`
|
|
59
|
+
def decode(string, mode:)
|
|
60
|
+
Decoder.decode(string, mode:)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# WirePair -> runtime value
|
|
64
|
+
def parse(wire_pair)
|
|
65
|
+
Parser.parse(wire_pair)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# input | function -> output
|
|
69
|
+
def apply(input, function)
|
|
70
|
+
Interpreter.safe_apply(function, input)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# expression -> runtime value
|
|
74
|
+
# Mutates environment (REPL variable binding)
|
|
75
|
+
def evaluate(expression, environment)
|
|
76
|
+
Interpreter.safe_evaluate(expression, environment)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# runtime value -> WirePair
|
|
80
|
+
def serialize(runtime_value)
|
|
81
|
+
Serializer.serialize(runtime_value)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# WirePair -> String
|
|
85
|
+
# Doesn't support mode `:unix`
|
|
86
|
+
def encode(wire_pair, mode:)
|
|
87
|
+
Encoder.encode(wire_pair, mode:)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# input -> WirePair
|
|
93
|
+
def load_input(options)
|
|
94
|
+
text = options.explicit_input || ($stdin.tty? ? "" : $stdin.read)
|
|
95
|
+
empty = text.strip.empty?
|
|
96
|
+
|
|
97
|
+
if options.input_mode == :unix || empty
|
|
98
|
+
WirePair.new(
|
|
99
|
+
status: options.error_input? ? 1 : 0,
|
|
100
|
+
data: empty ? "null" : text
|
|
101
|
+
)
|
|
102
|
+
else
|
|
103
|
+
decode(text, mode: options.input_mode)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# WirePair -> output
|
|
108
|
+
def emit_output(wire_pair, output_mode:)
|
|
109
|
+
if output_mode == :unix
|
|
110
|
+
channel = wire_pair.status.zero? ? $stdout : $stderr
|
|
111
|
+
channel.puts(wire_pair.data)
|
|
112
|
+
exit(wire_pair.status)
|
|
113
|
+
else
|
|
114
|
+
$stdout.puts(encode(wire_pair, mode: output_mode))
|
|
115
|
+
exit 0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Load the program (a `.fsn` file or an inline `-e` source) into the runtime
|
|
120
|
+
# value it evaluates to — usually a function. A parse error or a non-function
|
|
121
|
+
# value flows on as the program value and surfaces when `apply` runs it.
|
|
122
|
+
def load_program(options)
|
|
123
|
+
interpreter = Fusion::Interpreter.new
|
|
124
|
+
if options.inline_source
|
|
125
|
+
ast = Fusion::Parser.parse_file(options.inline_source, location: "code <inline>")
|
|
126
|
+
return ast if ast.is_a?(Fusion::Interpreter::ErrorVal) # a parse error
|
|
127
|
+
|
|
128
|
+
env = interpreter.root_env.child
|
|
129
|
+
env.define("__dir__", Dir.pwd)
|
|
130
|
+
interpreter.eval_expr(ast, env)
|
|
131
|
+
else
|
|
132
|
+
interpreter.load_file(File.expand_path(options.program_path)).force
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
# 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))
|
|
30
|
+
define.call("join", method(:join))
|
|
31
|
+
define.call("toString", method(:to_string))
|
|
32
|
+
define.call("parseNumber", method(:parse_number))
|
|
33
|
+
define.call("keys", method(:keys))
|
|
34
|
+
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
|
+
|
|
39
|
+
# type predicates: return false on any non-matching value, never an error
|
|
40
|
+
define.call("Integer", method(:integer?))
|
|
41
|
+
define.call("Float", method(:float?))
|
|
42
|
+
define.call("Number", method(:numeric?))
|
|
43
|
+
define.call("String", method(:string?))
|
|
44
|
+
define.call("Boolean", method(:boolean?))
|
|
45
|
+
define.call("Array", method(:array?))
|
|
46
|
+
define.call("Object", method(:object?))
|
|
47
|
+
define.call("Null", method(:null?))
|
|
48
|
+
define.call("Function", method(:function?))
|
|
49
|
+
define.call("NonFinite", method(:non_finite?))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# --- arithmetic ---
|
|
53
|
+
|
|
54
|
+
def add(v)
|
|
55
|
+
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])
|
|
59
|
+
|
|
60
|
+
v[0] + v[1]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def subtract(v)
|
|
64
|
+
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])
|
|
68
|
+
|
|
69
|
+
v[0] - v[1]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def multiply(v)
|
|
73
|
+
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])
|
|
77
|
+
|
|
78
|
+
v[0] * v[1]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def divide(v)
|
|
82
|
+
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
|
|
87
|
+
|
|
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
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def mod(v)
|
|
97
|
+
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
|
|
102
|
+
|
|
103
|
+
v[0] % v[1]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def negate(v)
|
|
107
|
+
return v if v.is_a?(ErrorVal)
|
|
108
|
+
return error("type_error", "negate", v, "expected a number") unless numeric?(v)
|
|
109
|
+
|
|
110
|
+
-v
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def floor(v)
|
|
114
|
+
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)
|
|
117
|
+
|
|
118
|
+
v.floor
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# --- comparison ---
|
|
122
|
+
|
|
123
|
+
def equals(v)
|
|
124
|
+
return v if v.is_a?(ErrorVal)
|
|
125
|
+
return error("argument_error", "equals", v, "expected [_, _]") unless pair?(v)
|
|
126
|
+
|
|
127
|
+
@interp.deep_equal?(v[0], v[1])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def less_than(v)
|
|
131
|
+
return v if v.is_a?(ErrorVal)
|
|
132
|
+
return error("argument_error", "lessThan", v, "expected [_, _]") unless pair?(v)
|
|
133
|
+
|
|
134
|
+
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
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# --- boolean ---
|
|
145
|
+
|
|
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)
|
|
150
|
+
return v if v.is_a?(ErrorVal)
|
|
151
|
+
return error("argument_error", "and", v, "expected [_, _]") unless pair?(v)
|
|
152
|
+
|
|
153
|
+
@interp.truthy?(v[0]) && @interp.truthy?(v[1])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def or_(v)
|
|
157
|
+
return v if v.is_a?(ErrorVal)
|
|
158
|
+
return error("argument_error", "or", v, "expected [_, _]") unless pair?(v)
|
|
159
|
+
|
|
160
|
+
@interp.truthy?(v[0]) || @interp.truthy?(v[1])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def not_(v)
|
|
164
|
+
return v if v.is_a?(ErrorVal)
|
|
165
|
+
|
|
166
|
+
!@interp.truthy?(v)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# --- strings and structure bridges ---
|
|
170
|
+
|
|
171
|
+
def length(v)
|
|
172
|
+
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)
|
|
174
|
+
|
|
175
|
+
v.length
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def concat(v)
|
|
179
|
+
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
|
+
|
|
183
|
+
v[0] + v[1]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def chars(v)
|
|
187
|
+
return v if v.is_a?(ErrorVal)
|
|
188
|
+
return error("type_error", "chars", v, "expected a string") unless v.is_a?(String)
|
|
189
|
+
|
|
190
|
+
v.chars
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def join(v)
|
|
194
|
+
return v if v.is_a?(ErrorVal)
|
|
195
|
+
return error("argument_error", "join", v, "expected [_, _]") unless pair?(v)
|
|
196
|
+
|
|
197
|
+
array, separator = v
|
|
198
|
+
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]")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
array.join(separator)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def to_string(v)
|
|
206
|
+
return v if v.is_a?(ErrorVal)
|
|
207
|
+
|
|
208
|
+
case v
|
|
209
|
+
when String then v
|
|
210
|
+
when Integer, Float then v.to_s
|
|
211
|
+
when true then "true"
|
|
212
|
+
when false then "false"
|
|
213
|
+
when NULL then "null"
|
|
214
|
+
else error("conversion_error", "toString", v, "cannot stringify this value type")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parse_number(v)
|
|
219
|
+
return v if v.is_a?(ErrorVal)
|
|
220
|
+
return error("type_error", "parseNumber", v, "expected a string") unless v.is_a?(String)
|
|
221
|
+
|
|
222
|
+
case v
|
|
223
|
+
when /\A-?\d+\z/ then v.to_i
|
|
224
|
+
when /\A-?\d+(\.\d+)?([eE][+-]?\d+)?\z/ then v.to_f
|
|
225
|
+
else error("conversion_error", "parseNumber", v, "not a numeric string")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# --- object key enumeration (Tier 0: patterns can't enumerate unknown keys) ---
|
|
230
|
+
|
|
231
|
+
def keys(v)
|
|
232
|
+
return v if v.is_a?(ErrorVal)
|
|
233
|
+
return error("type_error", "keys", v, "expected an object") unless v.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
v.keys
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def values(v)
|
|
239
|
+
return v if v.is_a?(ErrorVal)
|
|
240
|
+
return error("type_error", "values", v, "expected an object") unless v.is_a?(Hash)
|
|
241
|
+
|
|
242
|
+
v.values
|
|
243
|
+
end
|
|
244
|
+
|
|
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
|
+
private
|
|
297
|
+
|
|
298
|
+
# Type predicates, also reused as internal guards.
|
|
299
|
+
|
|
300
|
+
def integer?(v)
|
|
301
|
+
v.is_a?(Integer) && !boolean?(v)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def float?(v)
|
|
305
|
+
v.is_a?(Float)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def numeric?(v)
|
|
309
|
+
v.is_a?(Numeric) && !boolean?(v)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def string?(v)
|
|
313
|
+
v.is_a?(String)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def boolean?(v)
|
|
317
|
+
v == true || v == false
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def array?(v)
|
|
321
|
+
v.is_a?(Array)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def object?(v)
|
|
325
|
+
v.is_a?(Hash)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def null?(v)
|
|
329
|
+
v == NULL
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def function?(v)
|
|
333
|
+
v.is_a?(Func) || v.is_a?(NativeFunc)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def pair?(v)
|
|
337
|
+
v.is_a?(Array) && v.length == 2
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def non_finite?(v)
|
|
341
|
+
v.is_a?(Float) && !v.finite?
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Build a standardized interpreter error (see docs/user/reference.md §6.5).
|
|
345
|
+
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
|
+
)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
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 = {}
|
|
26
|
+
@parent = parent
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Unchecked insert, for interpreter-internal names (__dir__, built-ins, …).
|
|
30
|
+
def define(name, value)
|
|
31
|
+
@vars[name] = value
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Insert a pattern binding, rejecting a duplicate binder. Only this env's
|
|
36
|
+
# own scope is checked: a binder may shadow a name from a parent env, but
|
|
37
|
+
# must be unique within one pattern/clause.
|
|
38
|
+
def bind(name, value)
|
|
39
|
+
raise DuplicateBinding, name if @vars.key?(name)
|
|
40
|
+
|
|
41
|
+
@vars[name] = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def lookup(name)
|
|
45
|
+
if @vars.key?(name)
|
|
46
|
+
@vars[name]
|
|
47
|
+
elsif @parent
|
|
48
|
+
@parent.lookup(name)
|
|
49
|
+
else
|
|
50
|
+
:__unbound__
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def child
|
|
55
|
+
Env.new(self)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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)
|
|
13
|
+
@payload = payload
|
|
14
|
+
@internal = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Whether this is an interpreter-produced error (vs. a user-constructed
|
|
18
|
+
# `!expr`). Governs serialization — see docs/user/reference.md §9.3.
|
|
19
|
+
def internal_error?
|
|
20
|
+
@internal
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Build an interpreter-produced error with the standardized payload shape
|
|
24
|
+
# documented in docs/user/reference.md §6.5.
|
|
25
|
+
def self.internal(kind:, location:, operation:, input:, message: nil)
|
|
26
|
+
error = new(
|
|
27
|
+
"kind" => kind,
|
|
28
|
+
"location" => location,
|
|
29
|
+
"operation" => operation,
|
|
30
|
+
"input" => input,
|
|
31
|
+
**(message ? { "message" => message } : {})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Mark as "@internal" to activate lenient serialization.
|
|
35
|
+
error.instance_variable_set(:@internal, true)
|
|
36
|
+
|
|
37
|
+
error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"!#{payload.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s
|
|
45
|
+
inspect
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# === Interpreter internals ===
|
|
4
|
+
#
|
|
5
|
+
# Lazy, memoized reference to a file's value (a "thunk" / promise).
|
|
6
|
+
|
|
7
|
+
module Fusion
|
|
8
|
+
class Interpreter
|
|
9
|
+
class FileThunk
|
|
10
|
+
def initialize(loader, abspath)
|
|
11
|
+
@loader = loader
|
|
12
|
+
@abspath = abspath
|
|
13
|
+
@state = :unforced # :unforced | :forcing | :done
|
|
14
|
+
@value = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def force
|
|
18
|
+
case @state
|
|
19
|
+
when :done then @value
|
|
20
|
+
when :forcing
|
|
21
|
+
# We are already evaluating this file and were asked for it again
|
|
22
|
+
# without any intervening function boundary => non-productive data cycle.
|
|
23
|
+
ErrorVal.internal(
|
|
24
|
+
kind: "reference_error",
|
|
25
|
+
location: @loader.file_location(@abspath),
|
|
26
|
+
operation: "forcing a file reference",
|
|
27
|
+
input: @abspath,
|
|
28
|
+
message: "non-productive data cycle"
|
|
29
|
+
)
|
|
30
|
+
else
|
|
31
|
+
@state = :forcing
|
|
32
|
+
@value = @loader.evaluate_file(@abspath)
|
|
33
|
+
@state = :done
|
|
34
|
+
@value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
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
|