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.
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