fusion-lang 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +24 -0
- data/.simplecov +11 -0
- data/CHANGELOG.md +42 -0
- data/README.md +2 -1
- data/Rakefile +8 -0
- data/docs/lang/design.md +54 -5
- data/docs/lang/implementation.md +41 -0
- data/docs/user/how-to-guides.md +90 -5
- data/docs/user/reference.md +48 -37
- data/docs/user/tutorial.md +6 -5
- data/examples/double.fsn +4 -1
- data/examples/factorial.fsn +6 -3
- data/examples/gcd.fsn +9 -0
- data/examples/matrix/OP.fsn +2 -0
- data/examples/matrix/average.fsn +2 -0
- data/examples/matrix/solve.fsn +2 -0
- data/exe/fusion +2 -2
- data/lib/fusion/ast.rb +76 -28
- data/lib/fusion/atom.rb +1 -1
- data/lib/fusion/cli/decoder.rb +6 -6
- data/lib/fusion/cli/encoder.rb +2 -2
- data/lib/fusion/cli/options.rb +11 -8
- data/lib/fusion/cli/parser.rb +1 -1
- data/lib/fusion/cli/repl.rb +6 -6
- data/lib/fusion/cli/serializer.rb +6 -7
- data/lib/fusion/cli.rb +1 -1
- data/lib/fusion/interpreter/builtins.rb +51 -22
- data/lib/fusion/interpreter/thunk.rb +2 -2
- data/lib/fusion/interpreter.rb +31 -23
- data/lib/fusion/lexer.rb +62 -40
- data/lib/fusion/parser.rb +72 -35
- data/lib/fusion/version.rb +3 -1
- data/lib/fusion.rb +0 -1
- data/stdlib/all.fsn +2 -2
- data/stdlib/any.fsn +3 -3
- data/stdlib/compact.fsn +2 -3
- data/stdlib/concat.fsn +4 -3
- data/stdlib/entries.fsn +6 -0
- data/stdlib/filter.fsn +3 -3
- data/stdlib/map.fsn +3 -2
- data/stdlib/matrix/Matrix.fsn +8 -0
- data/stdlib/matrix/OP.fsn +18 -0
- data/stdlib/matrix/add.fsn +7 -0
- data/stdlib/matrix/column.fsn +6 -0
- data/stdlib/matrix/determinant.fsn +16 -0
- data/stdlib/matrix/dimensions.fsn +5 -0
- data/stdlib/matrix/identity.fsn +6 -0
- data/stdlib/matrix/invert.fsn +26 -0
- data/stdlib/matrix/minor.fsn +15 -0
- data/stdlib/matrix/multiply.fsn +10 -0
- data/stdlib/matrix/negate.fsn +5 -0
- data/stdlib/matrix/product.fsn +11 -0
- data/stdlib/matrix/rotate.fsn +10 -0
- data/stdlib/matrix/row.fsn +6 -0
- data/stdlib/matrix/scale.fsn +6 -0
- data/stdlib/matrix/subtract.fsn +7 -0
- data/stdlib/matrix/sum.fsn +14 -0
- data/stdlib/matrix/transpose.fsn +5 -0
- data/stdlib/range.fsn +2 -2
- data/stdlib/safe.fsn +7 -0
- data/stdlib/sanitize.fsn +2 -3
- data/stdlib/toObject.fsn +7 -0
- data/stdlib/vector/Vector.fsn +7 -0
- data/stdlib/vector/add.fsn +6 -0
- data/stdlib/vector/cross.fsn +6 -0
- data/stdlib/vector/dot.fsn +6 -0
- data/stdlib/vector/norm.fsn +6 -0
- data/stdlib/vector/scale.fsn +5 -0
- data/stdlib/vector/subtract.fsn +6 -0
- data/stdlib/zip.fsn +6 -0
- metadata +37 -5
- data/stdlib/gt.fsn +0 -9
- data/stdlib/gte.fsn +0 -9
- data/stdlib/lt.fsn +0 -9
- data/stdlib/lte.fsn +0 -9
data/exe/fusion
CHANGED
|
@@ -22,9 +22,9 @@ end
|
|
|
22
22
|
begin
|
|
23
23
|
options = Fusion::CLI::Options.parse(ARGV)
|
|
24
24
|
Fusion::CLI.run(options)
|
|
25
|
-
rescue Fusion::CLI::Options::UsageError =>
|
|
25
|
+
rescue Fusion::CLI::Options::UsageError => e
|
|
26
26
|
# A command-line misuse is plain text on stderr, not a payloaded Fusion error,
|
|
27
27
|
# whether caught while parsing options or early during execution, e.g. by
|
|
28
28
|
# detecting empty stdin plus "-!".
|
|
29
|
-
abort("fusion: #{
|
|
29
|
+
abort("fusion: #{e.message}\n\n#{Fusion::CLI::Options::USAGE}")
|
|
30
30
|
end
|
data/lib/fusion/ast.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Fusion
|
|
|
14
14
|
# A syntactic identifier: a bound/looked-up name, a `.key`, or a `...rest`
|
|
15
15
|
# binder. Mirrors the lexer's ident rule (Lexer#ident_start? / #ident_part?).
|
|
16
16
|
# Object *keys* are arbitrary strings, not identifiers, so they stay `String`.
|
|
17
|
-
|
|
17
|
+
IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
18
18
|
|
|
19
19
|
# Expression and Pattern nodes each form a closed family, declared up front
|
|
20
20
|
# (empty) so a member of either may reference the other. Each module is
|
|
@@ -28,31 +28,66 @@ module Fusion
|
|
|
28
28
|
# Auxiliary typed parts: the elements a collection node holds. NOT themselves
|
|
29
29
|
# expressions or patterns, so they live outside the marker families and never
|
|
30
30
|
# satisfy an `Expression`/`Pattern` field.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
|
|
32
|
+
# An array element
|
|
33
|
+
ArrayItem = TypedData.define(value: Expression)
|
|
34
|
+
|
|
35
|
+
# `...expr` inside an array
|
|
36
|
+
ArraySpread = TypedData.define(value: Expression)
|
|
37
|
+
|
|
38
|
+
# `"k": expr` inside an object
|
|
39
|
+
KeyValuePair = TypedData.define(key: String, value: Expression)
|
|
40
|
+
|
|
41
|
+
# `...expr` inside an object
|
|
42
|
+
ObjectSpread = TypedData.define(value: Expression)
|
|
43
|
+
|
|
44
|
+
# One `pattern => body` of a function
|
|
45
|
+
Clause = TypedData.define(pattern: Pattern, body: Expression)
|
|
46
|
+
|
|
47
|
+
# A sub-pattern of an array pattern
|
|
48
|
+
PatternItem = TypedData.define(pattern: Pattern)
|
|
49
|
+
|
|
50
|
+
# `"k": pat` inside an object pattern
|
|
51
|
+
PatternPair = TypedData.define(key: String, pattern: Pattern)
|
|
52
|
+
|
|
53
|
+
# `...name` in array/object pattern. (name nil = don't bind)
|
|
54
|
+
PatternRest = TypedData.define(name: ->(v) { IDENTIFIER === v || v.nil? })
|
|
39
55
|
|
|
40
56
|
module Expression
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
# Atom literal (incl NULL)
|
|
58
|
+
Lit = TypedData.define(value: Atom)
|
|
59
|
+
|
|
60
|
+
# `!expr` or bare `!` (payload nil = !null)
|
|
61
|
+
ErrLit = TypedData.define(payload: ->(v) { Expression === v || v.nil? })
|
|
62
|
+
|
|
63
|
+
ArrLit = TypedData.define(items: ->(v) { v.is_a?(Array) && v.all? { |e| ArrayItem === e || ArraySpread === e } })
|
|
64
|
+
|
|
65
|
+
# [KeyValuePair|ObjectSpread], distinct fixed keys
|
|
66
|
+
ObjLit = TypedData.define(pairs: ->(v) {
|
|
45
67
|
v.is_a?(Array) &&
|
|
46
68
|
v.all? { |m| KeyValuePair === m || ObjectSpread === m } &&
|
|
47
69
|
v.filter_map { |m| m.key if KeyValuePair === m }.then { |keys| keys.uniq.size == keys.size }
|
|
48
70
|
})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
|
|
72
|
+
# [] = the empty function
|
|
73
|
+
FuncLit = TypedData.define(clauses: ->(v) { v.is_a?(Array) && v.all? { |c| Clause === c } })
|
|
74
|
+
|
|
75
|
+
# Read a builtin/bound name
|
|
76
|
+
Ident = TypedData.define(name: IDENTIFIER)
|
|
77
|
+
|
|
78
|
+
FileRef = TypedData.define(variety: ->(v) { [:self, :super, :super_name, :name, :path].include?(v) }, path: ->(v) { String === v || v.nil? })
|
|
79
|
+
|
|
80
|
+
# `left | right`
|
|
81
|
+
Pipe = TypedData.define(left: Expression, right: Expression)
|
|
82
|
+
|
|
83
|
+
# `obj.key`
|
|
84
|
+
Member = TypedData.define(obj: Expression, key: IDENTIFIER)
|
|
85
|
+
|
|
86
|
+
# `obj[expr]`
|
|
87
|
+
Index = TypedData.define(obj: Expression, idx: Expression)
|
|
88
|
+
|
|
89
|
+
# `obj[expr = expr]`
|
|
90
|
+
IndexSet = TypedData.define(obj: Expression, idx: Expression, value: Expression)
|
|
56
91
|
|
|
57
92
|
constants.each do |name|
|
|
58
93
|
node = const_get(name)
|
|
@@ -61,22 +96,35 @@ module Fusion
|
|
|
61
96
|
end
|
|
62
97
|
|
|
63
98
|
module Pattern
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
PWild
|
|
68
|
-
|
|
99
|
+
# literal pattern
|
|
100
|
+
PLit = TypedData.define(value: Atom)
|
|
101
|
+
|
|
102
|
+
# `!` or `!pat` ; inner=PWild matches any error
|
|
103
|
+
PErr = TypedData.define(inner: Pattern)
|
|
104
|
+
|
|
105
|
+
# binds
|
|
106
|
+
PBind = TypedData.define(name: IDENTIFIER)
|
|
107
|
+
|
|
108
|
+
# `_`
|
|
109
|
+
PWild = TypedData.define(dummy: NilClass)
|
|
110
|
+
|
|
111
|
+
# [PatternItem|PatternRest], at most one rest
|
|
112
|
+
PArr = TypedData.define(items: ->(v) {
|
|
69
113
|
v.is_a?(Array) &&
|
|
70
114
|
v.all? { |e| PatternItem === e || PatternRest === e } &&
|
|
71
115
|
v.count { |e| PatternRest === e } <= 1
|
|
72
116
|
})
|
|
73
|
-
|
|
117
|
+
|
|
118
|
+
# [PatternPair|PatternRest], one rest, distinct keys
|
|
119
|
+
PObj = TypedData.define(pairs: ->(v) {
|
|
74
120
|
v.is_a?(Array) &&
|
|
75
121
|
v.all? { |m| PatternPair === m || PatternRest === m } &&
|
|
76
122
|
v.count { |m| PatternRest === m } <= 1 &&
|
|
77
123
|
v.filter_map { |m| m.key if PatternPair === m }.then { |keys| keys.uniq.size == keys.size }
|
|
78
124
|
})
|
|
79
|
-
|
|
125
|
+
|
|
126
|
+
# `inner ? predicate`
|
|
127
|
+
PGuard = TypedData.define(inner: Pattern, pred_expr: Expression)
|
|
80
128
|
|
|
81
129
|
constants.each do |name|
|
|
82
130
|
node = const_get(name)
|
|
@@ -86,7 +134,7 @@ module Fusion
|
|
|
86
134
|
|
|
87
135
|
module Statement
|
|
88
136
|
# The only statement. Only allowed in the REPL. `name = expression`.
|
|
89
|
-
Assignment = TypedData.define(name:
|
|
137
|
+
Assignment = TypedData.define(name: IDENTIFIER, expression: Expression)
|
|
90
138
|
|
|
91
139
|
constants.each do |name|
|
|
92
140
|
node = const_get(name)
|
data/lib/fusion/atom.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Fusion
|
|
|
11
11
|
# A scalar literal value: the JSON atoms plus NULL (everything the lexer
|
|
12
12
|
# emits as a token value, see Lexer#lex_number and #lex_word).
|
|
13
13
|
Atom = ->(v) {
|
|
14
|
-
v == NULL ||
|
|
14
|
+
v == NULL || v == true || v == false ||
|
|
15
15
|
v.is_a?(Integer) || v.is_a?(Float) || v.is_a?(String)
|
|
16
16
|
}
|
|
17
17
|
end
|
data/lib/fusion/cli/decoder.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Fusion
|
|
|
26
26
|
next unless raw.is_a?(Array) && raw.length == 2 && raw[0].is_a?(Integer)
|
|
27
27
|
|
|
28
28
|
# The tag must be exactly the integer 0 or 1 (no 0.0 — Fusion equality is exact).
|
|
29
|
-
[raw[0], raw[1]] if
|
|
29
|
+
[raw[0], raw[1]] if [0, 1].include?(raw[0])
|
|
30
30
|
end
|
|
31
31
|
when :object
|
|
32
32
|
decode_envelope(text, mode) do |raw|
|
|
@@ -68,12 +68,12 @@ module Fusion
|
|
|
68
68
|
return WirePair.new(status: status, data: JSON.generate(inner)) if status
|
|
69
69
|
|
|
70
70
|
WirePair.new(status: 1, data: JSON.generate(
|
|
71
|
-
"kind"
|
|
72
|
-
"origin"
|
|
71
|
+
"kind" => "argument_error",
|
|
72
|
+
"origin" => "input",
|
|
73
73
|
"operation" => "decoding input",
|
|
74
|
-
"status"
|
|
75
|
-
"input"
|
|
76
|
-
"expected"
|
|
74
|
+
"status" => 0,
|
|
75
|
+
"input" => raw,
|
|
76
|
+
"expected" => EXPECTED_ENVELOPE_SHAPES.fetch(mode),
|
|
77
77
|
))
|
|
78
78
|
rescue JSON::ParserError
|
|
79
79
|
# TODO: BUG ???
|
data/lib/fusion/cli/encoder.rb
CHANGED
|
@@ -12,12 +12,12 @@ module Fusion
|
|
|
12
12
|
def encode(wire_pair, mode:)
|
|
13
13
|
case mode
|
|
14
14
|
when :bang
|
|
15
|
-
bang = wire_pair.status
|
|
15
|
+
bang = wire_pair.status == 0 ? "" : "!"
|
|
16
16
|
"#{bang}#{wire_pair.data}"
|
|
17
17
|
when :array
|
|
18
18
|
"[#{wire_pair.status},#{wire_pair.data}]"
|
|
19
19
|
when :object
|
|
20
|
-
key = wire_pair.status
|
|
20
|
+
key = wire_pair.status == 0 ? "value" : "error"
|
|
21
21
|
"{\"#{key}\":#{wire_pair.data}}"
|
|
22
22
|
else
|
|
23
23
|
raise Unreachable, "Unknown output mode #{mode}"
|
data/lib/fusion/cli/options.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Fusion
|
|
|
52
52
|
object {"value": _} marks a value, {"error": _} an error
|
|
53
53
|
TEXT
|
|
54
54
|
|
|
55
|
-
MODES =
|
|
55
|
+
MODES = ['unix', 'bang', 'array', 'object'].freeze
|
|
56
56
|
|
|
57
57
|
attr_reader :use_case, :input_mode, :output_mode, :inline_source, :program_path, :jail
|
|
58
58
|
|
|
@@ -138,12 +138,12 @@ module Fusion
|
|
|
138
138
|
# exe/fusion reports them as plain usage text (never a payloaded error).
|
|
139
139
|
def self.run_parser(parser, argv)
|
|
140
140
|
parser.parse(argv)
|
|
141
|
-
rescue OptionParser::InvalidOption =>
|
|
142
|
-
raise UsageError, "unknown option #{
|
|
143
|
-
rescue OptionParser::MissingArgument =>
|
|
144
|
-
raise UsageError, missing_argument_message(
|
|
145
|
-
rescue OptionParser::ParseError =>
|
|
146
|
-
raise UsageError,
|
|
141
|
+
rescue OptionParser::InvalidOption => e
|
|
142
|
+
raise UsageError, "unknown option #{e.args.join(' ')}"
|
|
143
|
+
rescue OptionParser::MissingArgument => e
|
|
144
|
+
raise UsageError, missing_argument_message(e.args.first)
|
|
145
|
+
rescue OptionParser::ParseError => e
|
|
146
|
+
raise UsageError, e.message
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
# A MODE value -> its symbol, or a UsageError naming the valid modes.
|
|
@@ -174,12 +174,14 @@ module Fusion
|
|
|
174
174
|
unless input_mode.nil? && output_mode.nil? && !error_input && inline_source.nil? && positional.empty?
|
|
175
175
|
raise UsageError, "--repl takes no program, no input, and no modes"
|
|
176
176
|
end
|
|
177
|
+
|
|
177
178
|
program_path = nil
|
|
178
179
|
when :stream
|
|
179
180
|
input_mode ||= :array
|
|
180
181
|
output_mode ||= :array
|
|
181
182
|
raise UsageError, "--stream does not support the unix mode" if input_mode == :unix || output_mode == :unix
|
|
182
183
|
raise UsageError, "-! requires the unix input mode" if error_input
|
|
184
|
+
|
|
183
185
|
program_path = inline_source ? nil : positional.shift
|
|
184
186
|
raise UsageError, "missing program (a .fsn file or -e)" unless inline_source || program_path
|
|
185
187
|
raise UsageError, "too many positional arguments" unless positional.empty?
|
|
@@ -187,6 +189,7 @@ module Fusion
|
|
|
187
189
|
input_mode ||= :unix
|
|
188
190
|
output_mode ||= :unix
|
|
189
191
|
raise UsageError, "-! requires the unix input mode" if error_input && input_mode != :unix
|
|
192
|
+
|
|
190
193
|
program_path = inline_source ? nil : positional.shift
|
|
191
194
|
raise UsageError, "missing program (a .fsn file or -e)" unless inline_source || program_path
|
|
192
195
|
raise UsageError, "too many positional arguments" unless positional.empty?
|
|
@@ -202,7 +205,7 @@ module Fusion
|
|
|
202
205
|
program_path: program_path,
|
|
203
206
|
error_input: error_input,
|
|
204
207
|
skip_blank_lines: skip_blank_lines,
|
|
205
|
-
jail: jail
|
|
208
|
+
jail: jail,
|
|
206
209
|
)
|
|
207
210
|
end
|
|
208
211
|
|
data/lib/fusion/cli/parser.rb
CHANGED
data/lib/fusion/cli/repl.rb
CHANGED
|
@@ -14,10 +14,10 @@ module Fusion
|
|
|
14
14
|
GREEN = "\e[32m"
|
|
15
15
|
RED = "\e[31m"
|
|
16
16
|
|
|
17
|
-
PROMPT = "#{LIGHT_BLUE}fsn> #{RESET}"
|
|
18
|
-
CONTINUATION_PROMPT = "#{LIGHT_BLUE}...> #{RESET}"
|
|
19
|
-
VALUE_MARKER = "#{GREEN}✔ #{RESET}"
|
|
20
|
-
ERROR_MARKER = "#{RED}✗ #{RESET}"
|
|
17
|
+
PROMPT = "#{LIGHT_BLUE}fsn> #{RESET}".freeze
|
|
18
|
+
CONTINUATION_PROMPT = "#{LIGHT_BLUE}...> #{RESET}".freeze
|
|
19
|
+
VALUE_MARKER = "#{GREEN}✔ #{RESET}".freeze
|
|
20
|
+
ERROR_MARKER = "#{RED}✗ #{RESET}".freeze
|
|
21
21
|
|
|
22
22
|
# REPL entries report errors with the same site as inline (`-e`) code.
|
|
23
23
|
SITE = { origin: "code", file: "<inline>" }.freeze
|
|
@@ -32,7 +32,7 @@ module Fusion
|
|
|
32
32
|
require "reline"
|
|
33
33
|
Reline.output = $stderr
|
|
34
34
|
Reline.prompt_proc = proc do |lines|
|
|
35
|
-
lines.each_index.map { |i| i
|
|
35
|
+
lines.each_index.map { |i| i == 0 ? PROMPT : CONTINUATION_PROMPT }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# The session env is a child of the run's root, so it carries the jail;
|
|
@@ -43,7 +43,7 @@ module Fusion
|
|
|
43
43
|
buffer = begin
|
|
44
44
|
Reline.readmultiline(PROMPT, true) { complete?(_1) }
|
|
45
45
|
rescue Interrupt
|
|
46
|
-
|
|
46
|
+
warn("^C") # discard the half-typed entry and re-prompt
|
|
47
47
|
next
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -13,7 +13,7 @@ module Fusion
|
|
|
13
13
|
# Only use "lenient: true" in the REPL!
|
|
14
14
|
def serialize(runtime_value, lenient: false)
|
|
15
15
|
message = catch(:unserializable) do
|
|
16
|
-
if runtime_value.is_a?(Interpreter::ErrorVal)
|
|
16
|
+
if runtime_value.is_a?(Interpreter::ErrorVal) # rubocop:disable Style/GuardClause
|
|
17
17
|
error = runtime_value
|
|
18
18
|
data = convert(error.payload, lenient: lenient || error.runtime?).to_json
|
|
19
19
|
return WirePair.new(status: 1, data: data)
|
|
@@ -27,7 +27,7 @@ module Fusion
|
|
|
27
27
|
origin: "output",
|
|
28
28
|
operation: "serializing result",
|
|
29
29
|
input: runtime_value,
|
|
30
|
-
message: message
|
|
30
|
+
message: message,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
serialize(runtime_error, lenient: true)
|
|
@@ -42,6 +42,7 @@ module Fusion
|
|
|
42
42
|
nil
|
|
43
43
|
when Float
|
|
44
44
|
return runtime_value if runtime_value.finite?
|
|
45
|
+
|
|
45
46
|
throw(:unserializable, "cannot serialize a non-finite number") unless lenient
|
|
46
47
|
|
|
47
48
|
"<#{runtime_value}>" # "<Infinity>" / "<-Infinity>" / "<NaN>"
|
|
@@ -56,11 +57,9 @@ module Fusion
|
|
|
56
57
|
when true, false, String, Numeric
|
|
57
58
|
runtime_value
|
|
58
59
|
when Interpreter::ErrorVal
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
raise Unreachable, "ErrorVal should have been handled at the top level of convert"
|
|
63
|
-
end
|
|
60
|
+
raise Unreachable, "ErrorVal should have been handled at the top level of convert" unless lenient
|
|
61
|
+
|
|
62
|
+
"!#{convert(runtime_value.payload, lenient:).to_json}"
|
|
64
63
|
else
|
|
65
64
|
raise Unreachable, "Unhandled type in convert: #{runtime_value.class}"
|
|
66
65
|
end
|
data/lib/fusion/cli.rb
CHANGED
|
@@ -173,7 +173,7 @@ module Fusion
|
|
|
173
173
|
# WirePair -> stdout/stderr
|
|
174
174
|
def emit_output(wire_pair, output_mode:)
|
|
175
175
|
if output_mode == :unix
|
|
176
|
-
channel = wire_pair.status
|
|
176
|
+
channel = wire_pair.status == 0 ? $stdout : $stderr
|
|
177
177
|
channel.puts(wire_pair.data)
|
|
178
178
|
exit(wire_pair.status)
|
|
179
179
|
else
|
|
@@ -12,9 +12,7 @@ module Fusion
|
|
|
12
12
|
define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
|
|
13
13
|
|
|
14
14
|
# Irreducible primitives kept as built-ins. The sugar-target operators
|
|
15
|
-
# (arithmetic/comparison/boolean) live in `@OP` below
|
|
16
|
-
# derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files that build on
|
|
17
|
-
# `@OP.*`, so they follow a per-directory `@OP` override. Numeric functions
|
|
15
|
+
# (arithmetic/comparison/boolean) live in `@OP` below. Numeric functions
|
|
18
16
|
# (`round`, `divide`, `sin`, …) and constants (`pi`, `e`) live in `@math`.
|
|
19
17
|
define.call("size", method(:size))
|
|
20
18
|
define.call("join", method(:join))
|
|
@@ -23,7 +21,6 @@ module Fusion
|
|
|
23
21
|
define.call("parseNumber", method(:parse_number))
|
|
24
22
|
define.call("keys", method(:keys))
|
|
25
23
|
define.call("values", method(:values))
|
|
26
|
-
define.call("toObject", method(:to_object))
|
|
27
24
|
|
|
28
25
|
# type predicates: return false on any non-matching value, never an error
|
|
29
26
|
define.call("Integer", method(:integer?))
|
|
@@ -33,6 +30,7 @@ module Fusion
|
|
|
33
30
|
define.call("Boolean", method(:boolean?))
|
|
34
31
|
define.call("Array", method(:array?))
|
|
35
32
|
define.call("Object", method(:object?))
|
|
33
|
+
define.call("Collection", method(:collection?))
|
|
36
34
|
define.call("Null", method(:null?))
|
|
37
35
|
define.call("Function", method(:function?))
|
|
38
36
|
define.call("NonFinite", method(:non_finite?))
|
|
@@ -51,6 +49,10 @@ module Fusion
|
|
|
51
49
|
"modulo" => NativeFunc.new("OP.modulo", method(:op_modulo)),
|
|
52
50
|
"equal" => NativeFunc.new("OP.equal", method(:op_equal)),
|
|
53
51
|
"compare" => NativeFunc.new("OP.compare", method(:op_compare)),
|
|
52
|
+
"lt" => NativeFunc.new("OP.lt", method(:op_lt)),
|
|
53
|
+
"gt" => NativeFunc.new("OP.gt", method(:op_gt)),
|
|
54
|
+
"lte" => NativeFunc.new("OP.lte", method(:op_lte)),
|
|
55
|
+
"gte" => NativeFunc.new("OP.gte", method(:op_gte)),
|
|
54
56
|
"and" => NativeFunc.new("OP.and", method(:op_and)),
|
|
55
57
|
"or" => NativeFunc.new("OP.or", method(:op_or)),
|
|
56
58
|
"not" => NativeFunc.new("OP.not", method(:op_not)),
|
|
@@ -143,7 +145,7 @@ module Fusion
|
|
|
143
145
|
def math_rand(v)
|
|
144
146
|
return v if v.is_a?(ErrorVal)
|
|
145
147
|
return rand if v == NULL
|
|
146
|
-
return rand(v) if integer?(v) && v
|
|
148
|
+
return rand(v) if integer?(v) && v > 0
|
|
147
149
|
|
|
148
150
|
argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
|
|
149
151
|
end
|
|
@@ -197,7 +199,7 @@ module Fusion
|
|
|
197
199
|
return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
|
|
198
200
|
|
|
199
201
|
a, b = v
|
|
200
|
-
result = a.is_a?(Integer) && b.is_a?(Integer) &&
|
|
202
|
+
result = a.is_a?(Integer) && b.is_a?(Integer) && b >= 0 ? a**b : a.to_f**b
|
|
201
203
|
return error("math_error", "math.pow", v, "not in domain (complex result)") if result.is_a?(Complex)
|
|
202
204
|
|
|
203
205
|
result
|
|
@@ -206,9 +208,8 @@ module Fusion
|
|
|
206
208
|
# --- OP: the sugar-target operators (reached as `@OP.sum`, `@OP.and`, …) ---
|
|
207
209
|
#
|
|
208
210
|
# 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
|
|
210
|
-
#
|
|
211
|
-
# these.
|
|
211
|
+
# the unary ones take a single value; `compare` returns -1 / 0 / 1, which
|
|
212
|
+
# `lt` / `gt` / `lte` / `gte` then read into a boolean.
|
|
212
213
|
|
|
213
214
|
def op_sum(v)
|
|
214
215
|
return v if v.is_a?(ErrorVal)
|
|
@@ -289,6 +290,26 @@ module Fusion
|
|
|
289
290
|
end
|
|
290
291
|
end
|
|
291
292
|
|
|
293
|
+
# The comparison readers interpret an `@OP.compare` result. The infix sugar
|
|
294
|
+
# `a < b` desugars to `[a, b] | @OP.compare | @OP.lt` (likewise `<=` / `>` /
|
|
295
|
+
# `>=`), so an `@OP` override reskins the ordering and its reading together.
|
|
296
|
+
|
|
297
|
+
def op_lt(v)
|
|
298
|
+
read_compare_result("OP.lt", v, [-1])
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def op_gt(v)
|
|
302
|
+
read_compare_result("OP.gt", v, [1])
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def op_lte(v)
|
|
306
|
+
read_compare_result("OP.lte", v, [-1, 0])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def op_gte(v)
|
|
310
|
+
read_compare_result("OP.gte", v, [0, 1])
|
|
311
|
+
end
|
|
312
|
+
|
|
292
313
|
def op_and(v)
|
|
293
314
|
return v if v.is_a?(ErrorVal)
|
|
294
315
|
return argument_error("OP.and", v, ["_ ? @Array"]) unless v.is_a?(Array)
|
|
@@ -313,7 +334,7 @@ module Fusion
|
|
|
313
334
|
|
|
314
335
|
def size(v)
|
|
315
336
|
return v if v.is_a?(ErrorVal)
|
|
316
|
-
return argument_error("size", v, ["_ ? @String", "_ ? @
|
|
337
|
+
return argument_error("size", v, ["_ ? @String", "_ ? @Collection"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
|
|
317
338
|
|
|
318
339
|
v.length
|
|
319
340
|
end
|
|
@@ -322,6 +343,7 @@ module Fusion
|
|
|
322
343
|
# is the stdlib pair-case built on this.
|
|
323
344
|
def join(v)
|
|
324
345
|
return v if v.is_a?(ErrorVal)
|
|
346
|
+
|
|
325
347
|
expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
|
|
326
348
|
return argument_error("join", v, expected) unless pair?(v)
|
|
327
349
|
|
|
@@ -339,6 +361,7 @@ module Fusion
|
|
|
339
361
|
# stdlib single-string case built on this.
|
|
340
362
|
def split(v)
|
|
341
363
|
return v if v.is_a?(ErrorVal)
|
|
364
|
+
|
|
342
365
|
expected = ["[_ ? @String, _ ? @String]"]
|
|
343
366
|
return argument_error("split", v, expected) unless pair?(v) && v[0].is_a?(String) && v[1].is_a?(String)
|
|
344
367
|
|
|
@@ -388,17 +411,6 @@ module Fusion
|
|
|
388
411
|
v.values
|
|
389
412
|
end
|
|
390
413
|
|
|
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
414
|
private
|
|
403
415
|
|
|
404
416
|
# Type predicates, also reused as internal guards.
|
|
@@ -420,7 +432,7 @@ module Fusion
|
|
|
420
432
|
end
|
|
421
433
|
|
|
422
434
|
def boolean?(v)
|
|
423
|
-
|
|
435
|
+
[true, false].include?(v)
|
|
424
436
|
end
|
|
425
437
|
|
|
426
438
|
def array?(v)
|
|
@@ -431,6 +443,10 @@ module Fusion
|
|
|
431
443
|
v.is_a?(Hash)
|
|
432
444
|
end
|
|
433
445
|
|
|
446
|
+
def collection?(v)
|
|
447
|
+
array?(v) || object?(v)
|
|
448
|
+
end
|
|
449
|
+
|
|
434
450
|
def null?(v)
|
|
435
451
|
v == NULL
|
|
436
452
|
end
|
|
@@ -447,6 +463,19 @@ module Fusion
|
|
|
447
463
|
v.is_a?(Float) && !v.finite?
|
|
448
464
|
end
|
|
449
465
|
|
|
466
|
+
COMPARE_RESULT = ["-1", "0", "1", "null"].freeze
|
|
467
|
+
|
|
468
|
+
# -1 / 0 / 1 (exact integers) → whether the ordering is in `truthy_results`.
|
|
469
|
+
# A partial order's compare may return null (incomparable); that passes
|
|
470
|
+
# through as null rather than being forced to a boolean.
|
|
471
|
+
def read_compare_result(name, v, truthy_results)
|
|
472
|
+
return v if v.is_a?(ErrorVal)
|
|
473
|
+
return NULL if v == NULL
|
|
474
|
+
return argument_error(name, v, COMPARE_RESULT) unless integer?(v) && [-1, 0, 1].include?(v)
|
|
475
|
+
|
|
476
|
+
truthy_results.include?(v)
|
|
477
|
+
end
|
|
478
|
+
|
|
450
479
|
# Build a standardized interpreter error carrying a human-readable
|
|
451
480
|
# `message` (see docs/user/reference.md §6.5). Use this for failures that
|
|
452
481
|
# aren't an input-shape mismatch (math, conversion, access). `operation`
|
|
@@ -33,9 +33,9 @@ module Fusion
|
|
|
33
33
|
@state = :forcing
|
|
34
34
|
begin
|
|
35
35
|
@value = @compute.call
|
|
36
|
-
rescue ReadFailure =>
|
|
36
|
+
rescue ReadFailure => e
|
|
37
37
|
# Memoize the Ruby error itself. Turn it into a Fusion runtime error below.
|
|
38
|
-
@value =
|
|
38
|
+
@value = e
|
|
39
39
|
end
|
|
40
40
|
@state = :done
|
|
41
41
|
@value
|