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/lib/fusion/interpreter.rb
CHANGED
|
@@ -80,10 +80,10 @@ module Fusion
|
|
|
80
80
|
rescue Unreachable
|
|
81
81
|
# An interpreter bug. Allowed to surface.
|
|
82
82
|
raise
|
|
83
|
-
rescue StandardError =>
|
|
83
|
+
rescue StandardError => e
|
|
84
84
|
Interpreter::ErrorVal.from_runtime(
|
|
85
85
|
kind: "internal_error", origin: "interpreter", operation: "running the program",
|
|
86
|
-
input: NULL, message:
|
|
86
|
+
input: NULL, message: e.message,
|
|
87
87
|
)
|
|
88
88
|
rescue SystemExit
|
|
89
89
|
# Let exit/abort through.
|
|
@@ -91,13 +91,13 @@ module Fusion
|
|
|
91
91
|
rescue SystemStackError
|
|
92
92
|
Interpreter::ErrorVal.from_runtime(
|
|
93
93
|
kind: "limit_error", origin: "interpreter", operation: "running the program",
|
|
94
|
-
input: NULL, message: "stack level too deep"
|
|
94
|
+
input: NULL, message: "stack level too deep",
|
|
95
95
|
)
|
|
96
|
-
rescue Exception =>
|
|
96
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
97
97
|
# Final net: any other escaped Ruby error becomes a payloaded error too.
|
|
98
98
|
Interpreter::ErrorVal.from_runtime(
|
|
99
99
|
kind: "internal_error", origin: "interpreter", operation: "running the program",
|
|
100
|
-
input: NULL, message:
|
|
100
|
+
input: NULL, message: e.message,
|
|
101
101
|
)
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -169,9 +169,9 @@ module Fusion
|
|
|
169
169
|
end
|
|
170
170
|
rescue Errno::ENOENT
|
|
171
171
|
raise Thunk::ReadFailure, "file not found"
|
|
172
|
-
rescue SystemCallError =>
|
|
172
|
+
rescue SystemCallError => e # EISDIR, EACCES, ... (file-system access failures)
|
|
173
173
|
# Drop Ruby's "@ io_fread - <path>" tail.
|
|
174
|
-
raise Thunk::ReadFailure,
|
|
174
|
+
raise Thunk::ReadFailure, e.message.split(" @ ").first.downcase
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
# Evaluate a top-level unit that has no file of its own:
|
|
@@ -191,7 +191,7 @@ module Fusion
|
|
|
191
191
|
# `site` is the `{origin:, file:}` of the referencing code; `reference` is its
|
|
192
192
|
# own source text (`"@name"`) — the single `operation` every failure reports.
|
|
193
193
|
def resolve_name(name, dir, site, reference)
|
|
194
|
-
sibling_file = File.expand_path(name
|
|
194
|
+
sibling_file = File.expand_path("#{name}.fsn", dir)
|
|
195
195
|
if File.exist?(sibling_file)
|
|
196
196
|
return jail_error(site, reference, NULL) unless within_jail?(sibling_file)
|
|
197
197
|
|
|
@@ -251,7 +251,7 @@ module Fusion
|
|
|
251
251
|
return @builtins[name]
|
|
252
252
|
end
|
|
253
253
|
|
|
254
|
-
stdlib_file = File.join(@stdlib_dir, name
|
|
254
|
+
stdlib_file = File.join(@stdlib_dir, "#{name}.fsn")
|
|
255
255
|
if File.exist?(stdlib_file)
|
|
256
256
|
# The reference reports the user's source text, not the internal stdlib path.
|
|
257
257
|
return load_file(stdlib_file).force(operation: reference, input: NULL, site: site)
|
|
@@ -263,7 +263,7 @@ module Fusion
|
|
|
263
263
|
# Resolve a pure path "@dir/a" or "@../a": file only, never builtin/stdlib.
|
|
264
264
|
# `reference` is the source text (`"@../a"`), the single `operation` reported.
|
|
265
265
|
def resolve_path(relpath, dir, site, reference)
|
|
266
|
-
target = File.expand_path(relpath
|
|
266
|
+
target = File.expand_path("#{relpath}.fsn", dir)
|
|
267
267
|
return jail_error(site, reference, NULL) unless within_jail?(target)
|
|
268
268
|
|
|
269
269
|
load_file(target).force(operation: reference, input: NULL, site: site)
|
|
@@ -374,11 +374,11 @@ module Fusion
|
|
|
374
374
|
when ArrayItem
|
|
375
375
|
out.append(value)
|
|
376
376
|
when ArraySpread
|
|
377
|
-
if value.is_a?(Array)
|
|
378
|
-
out.concat(value)
|
|
379
|
-
else
|
|
377
|
+
if !value.is_a?(Array)
|
|
380
378
|
return ErrorVal.from_runtime(kind: "argument_error", **code_site(env), operation: "[...] array spread", input: value, expected: ["_ ? @Array"])
|
|
381
379
|
end
|
|
380
|
+
|
|
381
|
+
out.concat(value)
|
|
382
382
|
else
|
|
383
383
|
raise Unreachable, "Unknown array item #{item.class}"
|
|
384
384
|
end
|
|
@@ -402,11 +402,11 @@ module Fusion
|
|
|
402
402
|
when KeyValuePair
|
|
403
403
|
out[pair.key] = value
|
|
404
404
|
when ObjectSpread
|
|
405
|
-
if value.is_a?(Hash)
|
|
406
|
-
out.merge!(value)
|
|
407
|
-
else
|
|
405
|
+
if !value.is_a?(Hash)
|
|
408
406
|
return ErrorVal.from_runtime(kind: "argument_error", **code_site(env), operation: "{...} object spread", input: value, expected: ["_ ? @Object"])
|
|
409
407
|
end
|
|
408
|
+
|
|
409
|
+
out.merge!(value)
|
|
410
410
|
else
|
|
411
411
|
raise Unreachable, "Unknown object pair #{pair.class}"
|
|
412
412
|
end
|
|
@@ -535,16 +535,21 @@ module Fusion
|
|
|
535
535
|
# becomes a payloaded error rather than a raw backtrace on stderr.
|
|
536
536
|
begin
|
|
537
537
|
f.fn.call(v)
|
|
538
|
-
rescue StandardError =>
|
|
538
|
+
rescue StandardError => e
|
|
539
539
|
# TODO: move math errors into the builtins. This should become a safety net for unpredicted errors.
|
|
540
|
-
kind =
|
|
541
|
-
|
|
540
|
+
kind = case e
|
|
541
|
+
when FloatDomainError, ZeroDivisionError, Math::DomainError
|
|
542
|
+
"math_error"
|
|
543
|
+
else
|
|
544
|
+
"internal_error"
|
|
545
|
+
end
|
|
546
|
+
ErrorVal.from_runtime(kind: kind, origin: "builtin", operation: "@#{f.name}", input: v, message: e.message)
|
|
542
547
|
end
|
|
543
548
|
elsif f.is_a?(Func)
|
|
544
549
|
# Stdlib code has no user file of its own: errors inside it (and in the
|
|
545
550
|
# built-ins it calls) report the user `call_site` that reached it. User and
|
|
546
551
|
# inline functions are their own call site (derived lexically from their env).
|
|
547
|
-
body_call_site =
|
|
552
|
+
body_call_site = code_site(f.env)[:origin] == "stdlib" ? call_site : nil
|
|
548
553
|
|
|
549
554
|
f.clauses.each do |clause|
|
|
550
555
|
# Bindings are inserted directly into a fresh child env as the pattern
|
|
@@ -646,7 +651,7 @@ module Fusion
|
|
|
646
651
|
if predicate_result.is_a?(ErrorVal)
|
|
647
652
|
# An unresolved @-reference, or an error raised while applying the
|
|
648
653
|
# predicate, becomes the clause's result.
|
|
649
|
-
|
|
654
|
+
predicate_result
|
|
650
655
|
else
|
|
651
656
|
# Ruby-style truthiness: the clause matches unless the predicate
|
|
652
657
|
# yields `false` or `null`.
|
|
@@ -672,11 +677,11 @@ module Fusion
|
|
|
672
677
|
return r if r.is_a?(ErrorVal)
|
|
673
678
|
return false unless r
|
|
674
679
|
end
|
|
675
|
-
true
|
|
676
680
|
else
|
|
677
681
|
before = items[0...rest_index]
|
|
678
682
|
after = items[(rest_index + 1)..]
|
|
679
683
|
return false if value.length < before.length + after.length
|
|
684
|
+
|
|
680
685
|
before.each_with_index do |item, i|
|
|
681
686
|
r = match(item.pattern, value[i], env)
|
|
682
687
|
return r if r.is_a?(ErrorVal)
|
|
@@ -693,8 +698,8 @@ module Fusion
|
|
|
693
698
|
mid = value[before.length...(value.length - after.length)]
|
|
694
699
|
env.bind(rest_name, mid)
|
|
695
700
|
end
|
|
696
|
-
true
|
|
697
701
|
end
|
|
702
|
+
true
|
|
698
703
|
end
|
|
699
704
|
|
|
700
705
|
def match_object(pattern, value, env)
|
|
@@ -708,9 +713,11 @@ module Fusion
|
|
|
708
713
|
rest_name = pair.name # may be nil (ignore) or a string
|
|
709
714
|
when PatternPair
|
|
710
715
|
return false unless value.key?(pair.key)
|
|
716
|
+
|
|
711
717
|
r = match(pair.pattern, value[pair.key], env)
|
|
712
718
|
return r if r.is_a?(ErrorVal)
|
|
713
719
|
return false unless r
|
|
720
|
+
|
|
714
721
|
matched_keys << pair.key
|
|
715
722
|
else
|
|
716
723
|
raise Unreachable, "Unknown object pattern pair #{pair.class}"
|
|
@@ -739,6 +746,7 @@ module Fusion
|
|
|
739
746
|
def deep_equal?(a, b)
|
|
740
747
|
return true if a.equal?(b)
|
|
741
748
|
return false if a.class != b.class
|
|
749
|
+
|
|
742
750
|
case a
|
|
743
751
|
when Array
|
|
744
752
|
a.length == b.length && a.each_index.all? { |i| deep_equal?(a[i], b[i]) }
|
data/lib/fusion/lexer.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Fusion
|
|
|
20
20
|
"=" => :equals,
|
|
21
21
|
"+" => :plus, "-" => :minus, "*" => :star,
|
|
22
22
|
"%" => :percent, "~" => :tilde,
|
|
23
|
+
"<" => :lt, ">" => :gt,
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
25
26
|
def initialize(src)
|
|
@@ -34,7 +35,7 @@ module Fusion
|
|
|
34
35
|
t = next_token
|
|
35
36
|
out << t
|
|
36
37
|
# A file-reference path is one tight token, lexed only right after `@`/`@@`.
|
|
37
|
-
if
|
|
38
|
+
if [:at, :atat].include?(t.type)
|
|
38
39
|
p = try_lex_path
|
|
39
40
|
out << p unless p.nil?
|
|
40
41
|
end
|
|
@@ -79,17 +80,35 @@ module Fusion
|
|
|
79
80
|
@i += 2
|
|
80
81
|
return Token.new(type: :qq, value: "??", pos: start)
|
|
81
82
|
end
|
|
83
|
+
if c == "<" && peek(1) == "="
|
|
84
|
+
@i += 2
|
|
85
|
+
return Token.new(type: :lte, value: "<=", pos: start)
|
|
86
|
+
end
|
|
87
|
+
if c == ">" && peek(1) == "="
|
|
88
|
+
@i += 2
|
|
89
|
+
return Token.new(type: :gte, value: ">=", pos: start)
|
|
90
|
+
end
|
|
82
91
|
if c == "&" && peek(1) == "&"
|
|
83
92
|
@i += 2
|
|
84
93
|
return Token.new(type: :andand, value: "&&", pos: start)
|
|
85
94
|
end
|
|
86
95
|
if c == "|"
|
|
87
96
|
case peek(1)
|
|
88
|
-
when "|"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
when "
|
|
92
|
-
|
|
97
|
+
when "|"
|
|
98
|
+
@i += 2
|
|
99
|
+
return Token.new(type: :oror, value: "||", pos: start)
|
|
100
|
+
when ":"
|
|
101
|
+
@i += 2
|
|
102
|
+
return Token.new(type: :pipemap, value: "|:", pos: start)
|
|
103
|
+
when "?"
|
|
104
|
+
@i += 2
|
|
105
|
+
return Token.new(type: :pipefilter, value: "|?", pos: start)
|
|
106
|
+
when "+"
|
|
107
|
+
@i += 2
|
|
108
|
+
return Token.new(type: :pipereduce, value: "|+", pos: start)
|
|
109
|
+
else
|
|
110
|
+
@i += 1
|
|
111
|
+
return Token.new(type: :pipe, value: "|", pos: start)
|
|
93
112
|
end
|
|
94
113
|
end
|
|
95
114
|
if c == "!"
|
|
@@ -105,6 +124,7 @@ module Fusion
|
|
|
105
124
|
if ident_start?(c)
|
|
106
125
|
return lex_word(start)
|
|
107
126
|
end
|
|
127
|
+
|
|
108
128
|
if (type = PUNCT[c])
|
|
109
129
|
@i += 1
|
|
110
130
|
return Token.new(type: type, value: c, pos: start)
|
|
@@ -114,13 +134,13 @@ module Fusion
|
|
|
114
134
|
|
|
115
135
|
def skip_trivia
|
|
116
136
|
loop do
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
case peek
|
|
138
|
+
when " ", "\t", "\n", "\r"
|
|
119
139
|
@i += 1
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@i += 1 until
|
|
140
|
+
when "#"
|
|
141
|
+
break if !at_line_start?
|
|
142
|
+
|
|
143
|
+
@i += 1 until [nil, "\n"].include?(peek)
|
|
124
144
|
else
|
|
125
145
|
break
|
|
126
146
|
end
|
|
@@ -130,7 +150,7 @@ module Fusion
|
|
|
130
150
|
# True when only whitespace precedes @i on the current physical line.
|
|
131
151
|
def at_line_start?
|
|
132
152
|
j = @i - 1
|
|
133
|
-
j -= 1 while j >= 0 &&
|
|
153
|
+
j -= 1 while j >= 0 && [" ", "\t"].include?(@src[j])
|
|
134
154
|
j < 0 || @src[j] == "\n" || @src[j] == "\r"
|
|
135
155
|
end
|
|
136
156
|
|
|
@@ -138,37 +158,39 @@ module Fusion
|
|
|
138
158
|
@i += 1 # opening quote
|
|
139
159
|
buf = +""
|
|
140
160
|
while (c = peek)
|
|
141
|
-
|
|
161
|
+
case c
|
|
162
|
+
when '"'
|
|
142
163
|
@i += 1
|
|
143
164
|
return Token.new(type: :string, value: buf, pos: start)
|
|
144
|
-
|
|
165
|
+
when "\\"
|
|
145
166
|
@i += 1
|
|
146
167
|
e = peek
|
|
147
168
|
buf << case e
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
when '"' then '"'
|
|
170
|
+
when "\\" then "\\"
|
|
171
|
+
when "/" then "/"
|
|
172
|
+
when "n" then "\n"
|
|
173
|
+
when "t" then "\t"
|
|
174
|
+
when "r" then "\r"
|
|
175
|
+
when "b" then "\b"
|
|
176
|
+
when "f" then "\f"
|
|
177
|
+
when "u"
|
|
178
|
+
hex = @src[@i + 1, 4]
|
|
179
|
+
@i += 4
|
|
180
|
+
code_point = hex.to_i(16)
|
|
181
|
+
# Reject surrogates and out-of-range code points up front:
|
|
182
|
+
# pack("U") would otherwise build an invalid-encoding string
|
|
183
|
+
# whose malformed-UTF-8 error surfaces far downstream.
|
|
184
|
+
if code_point.between?(0xD800, 0xDFFF) || code_point > 0x10FFFF
|
|
185
|
+
raise ParseError, "Invalid unicode escape \\u#{hex}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
[code_point].pack("U")
|
|
189
|
+
else
|
|
190
|
+
raise ParseError, "Bad escape \\#{e}"
|
|
191
|
+
end
|
|
170
192
|
@i += 1
|
|
171
|
-
|
|
193
|
+
when "\n", "\r"
|
|
172
194
|
raise ParseError, "Raw newline in string starting at #{start}; use \\n"
|
|
173
195
|
else
|
|
174
196
|
buf << c
|
|
@@ -187,10 +209,10 @@ module Fusion
|
|
|
187
209
|
j += 1
|
|
188
210
|
j += 1 while j < @n && digit?(@src[j])
|
|
189
211
|
end
|
|
190
|
-
if
|
|
212
|
+
if ["e", "E"].include?(@src[j])
|
|
191
213
|
is_float = true
|
|
192
214
|
j += 1
|
|
193
|
-
j += 1 if
|
|
215
|
+
j += 1 if ["+", "-"].include?(@src[j])
|
|
194
216
|
j += 1 while j < @n && digit?(@src[j])
|
|
195
217
|
end
|
|
196
218
|
text = @src[@i...j]
|
data/lib/fusion/parser.rb
CHANGED
|
@@ -30,8 +30,8 @@ module Fusion
|
|
|
30
30
|
expr = p.parse_expr
|
|
31
31
|
p.expect(:eof)
|
|
32
32
|
expr
|
|
33
|
-
rescue ParseError =>
|
|
34
|
-
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message:
|
|
33
|
+
rescue ParseError => e
|
|
34
|
+
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: e.message)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Parse one REPL entry — a statement (`identifier "=" expr`) or a bare
|
|
@@ -45,8 +45,8 @@ module Fusion
|
|
|
45
45
|
entry = p.parse_repl_entry
|
|
46
46
|
p.expect(:eof)
|
|
47
47
|
entry
|
|
48
|
-
rescue ParseError =>
|
|
49
|
-
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message:
|
|
48
|
+
rescue ParseError => e
|
|
49
|
+
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: e.message)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# A leading `identifier =` marks a statement; anything else is an expression.
|
|
@@ -73,7 +73,7 @@ module Fusion
|
|
|
73
73
|
# --- Operator sugar (reference §2.7) --------------------------------------
|
|
74
74
|
# The ladder below desugars every operator to a pipe into an `@OP.*` member
|
|
75
75
|
# (or, for the map-pipes, a stdlib call). Tightest to loosest:
|
|
76
|
-
# postfix · unary (! - / ~) · pipe (| |: |? |+) · * / % // · + - · ?? · == · && · ||
|
|
76
|
+
# postfix · unary (! - / ~) · pipe (| |: |? |+) · * / % // · + - · ?? < <= > >= · == · && · ||
|
|
77
77
|
# `@OP.*` is a shadowable `:name` reference, so a local `@OP` reskins the operators.
|
|
78
78
|
|
|
79
79
|
def parse_or
|
|
@@ -103,12 +103,24 @@ module Fusion
|
|
|
103
103
|
fold_operator(operands, "equal")
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
#
|
|
106
|
+
# The comparisons desugar to a compare piped into its reading member:
|
|
107
|
+
# `a < b` → `[a, b] | @OP.compare | @OP.lt`.
|
|
108
|
+
COMPARISON_MEMBERS = { lt: "lt", lte: "lte", gt: "gt", gte: "gte" }.freeze
|
|
109
|
+
|
|
110
|
+
# `??` (compare) and the comparisons are binary and left-associative — they
|
|
111
|
+
# do not fold, and `a < b < c` does not chain (it compares a boolean).
|
|
107
112
|
def parse_ordering
|
|
108
113
|
node = parse_additive
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
loop do
|
|
115
|
+
if at?(:qq)
|
|
116
|
+
advance
|
|
117
|
+
node = pipe_operator([node, parse_additive], "compare")
|
|
118
|
+
elsif COMPARISON_MEMBERS.key?(peek.type)
|
|
119
|
+
member = COMPARISON_MEMBERS[advance.type]
|
|
120
|
+
node = pipe_into(pipe_operator([node, parse_additive], "compare"), member)
|
|
121
|
+
else
|
|
122
|
+
break
|
|
123
|
+
end
|
|
112
124
|
end
|
|
113
125
|
node
|
|
114
126
|
end
|
|
@@ -131,7 +143,7 @@ module Fusion
|
|
|
131
143
|
# are binary and break the run. Standard left-to-right: `a * b % c` is `(a * b) % c`.
|
|
132
144
|
def parse_multiplicative
|
|
133
145
|
node = parse_pipe
|
|
134
|
-
run = nil
|
|
146
|
+
run = nil # accumulating product-run terms, or nil
|
|
135
147
|
loop do
|
|
136
148
|
if at?(:star) || at?(:slash)
|
|
137
149
|
inverted = at?(:slash)
|
|
@@ -165,10 +177,12 @@ module Fusion
|
|
|
165
177
|
elsif at?(:pipemap) || at?(:pipefilter) || at?(:pipereduce)
|
|
166
178
|
target = { pipemap: "map", pipefilter: "filter", pipereduce: "reduce" }[peek.type]
|
|
167
179
|
advance
|
|
168
|
-
arg = Expression::ObjLit.new(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
arg = Expression::ObjLit.new(
|
|
181
|
+
pairs: [
|
|
182
|
+
KeyValuePair.new(key: "c", value: node),
|
|
183
|
+
KeyValuePair.new(key: "f", value: parse_unary),
|
|
184
|
+
],
|
|
185
|
+
)
|
|
172
186
|
node = Expression::Pipe.new(left: arg, right: name_ref(target))
|
|
173
187
|
else
|
|
174
188
|
break
|
|
@@ -179,8 +193,7 @@ module Fusion
|
|
|
179
193
|
|
|
180
194
|
# Tokens that can begin a unary operand — used to decide whether `!` has an
|
|
181
195
|
# operand or is the bare `!null`.
|
|
182
|
-
PRIMARY_STARTERS =
|
|
183
|
-
lbracket lbrace lparen ident at atat].freeze
|
|
196
|
+
PRIMARY_STARTERS = [:number, :string, :true_kw, :false_kw, :null_kw, :bang, :minus, :slash, :tilde, :lbracket, :lbrace, :lparen, :ident, :at, :atat].freeze
|
|
184
197
|
|
|
185
198
|
# Unary prefixes: `!x` builds an error (bare `!` is `!null`); `-x` negates,
|
|
186
199
|
# `/x` inverts, `~x` is logical not. All bind tighter than `|`.
|
|
@@ -261,12 +274,15 @@ module Fusion
|
|
|
261
274
|
def parse_primary
|
|
262
275
|
t = peek
|
|
263
276
|
case t.type
|
|
264
|
-
when :number, :string
|
|
265
|
-
|
|
277
|
+
when :number, :string, :true_kw, :false_kw, :null_kw
|
|
278
|
+
advance
|
|
279
|
+
Expression::Lit.new(value: t.value)
|
|
266
280
|
when :lbracket then parse_array
|
|
267
281
|
when :lbrace then parse_object
|
|
268
282
|
when :lparen then parse_function_or_group
|
|
269
|
-
when :ident
|
|
283
|
+
when :ident
|
|
284
|
+
advance
|
|
285
|
+
Expression::Ident.new(name: t.value)
|
|
270
286
|
when :at then parse_fileref
|
|
271
287
|
when :atat then parse_superref
|
|
272
288
|
else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}"
|
|
@@ -284,6 +300,7 @@ module Fusion
|
|
|
284
300
|
|
|
285
301
|
tok = advance
|
|
286
302
|
raise ParseError, "`@@` cannot take an upward path (at #{tok.pos})" if tok.value.include?("..")
|
|
303
|
+
|
|
287
304
|
Expression::FileRef.new(variety: :super_name, path: tok.value)
|
|
288
305
|
end
|
|
289
306
|
|
|
@@ -309,6 +326,7 @@ module Fusion
|
|
|
309
326
|
items << ArrayItem.new(value: parse_expr)
|
|
310
327
|
end
|
|
311
328
|
break unless at?(:comma)
|
|
329
|
+
|
|
312
330
|
advance
|
|
313
331
|
end
|
|
314
332
|
expect(:rbracket)
|
|
@@ -329,11 +347,13 @@ module Fusion
|
|
|
329
347
|
key_tok = expect(:string)
|
|
330
348
|
key = key_tok.value
|
|
331
349
|
raise ParseError, "duplicate key #{key.inspect} (at #{key_tok.pos})" if keys.include?(key)
|
|
350
|
+
|
|
332
351
|
keys << key
|
|
333
352
|
expect(:colon)
|
|
334
353
|
pairs << KeyValuePair.new(key: key, value: parse_expr)
|
|
335
354
|
end
|
|
336
355
|
break unless at?(:comma)
|
|
356
|
+
|
|
337
357
|
advance
|
|
338
358
|
end
|
|
339
359
|
expect(:rbrace)
|
|
@@ -358,12 +378,11 @@ module Fusion
|
|
|
358
378
|
expect(:arrow)
|
|
359
379
|
body = parse_expr
|
|
360
380
|
clauses << Clause.new(pattern: pat, body: body)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
end
|
|
381
|
+
|
|
382
|
+
break if !at?(:comma)
|
|
383
|
+
|
|
384
|
+
advance
|
|
385
|
+
break if at?(:rparen) # trailing comma
|
|
367
386
|
end
|
|
368
387
|
expect(:rparen)
|
|
369
388
|
Expression::FuncLit.new(clauses: clauses)
|
|
@@ -384,10 +403,11 @@ module Fusion
|
|
|
384
403
|
case t.type
|
|
385
404
|
when :lparen, :lbracket, :lbrace then depth += 1
|
|
386
405
|
when :rparen, :rbracket, :rbrace
|
|
387
|
-
return false if depth
|
|
406
|
+
return false if depth == 0 # hit our closing ) first
|
|
407
|
+
|
|
388
408
|
depth -= 1
|
|
389
409
|
when :arrow
|
|
390
|
-
return true if depth
|
|
410
|
+
return true if depth == 0
|
|
391
411
|
when :eof
|
|
392
412
|
return false
|
|
393
413
|
end
|
|
@@ -412,8 +432,7 @@ module Fusion
|
|
|
412
432
|
|
|
413
433
|
# Tokens that can begin a `guardedpat` (used to detect whether `!` is
|
|
414
434
|
# followed by a payload pattern or stands alone).
|
|
415
|
-
GUARDEDPAT_STARTERS =
|
|
416
|
-
lbracket lbrace ident].freeze
|
|
435
|
+
GUARDEDPAT_STARTERS = [:number, :string, :true_kw, :false_kw, :null_kw, :minus, :lbracket, :lbrace, :ident].freeze
|
|
417
436
|
|
|
418
437
|
def parse_errpat
|
|
419
438
|
expect(:bang)
|
|
@@ -440,9 +459,12 @@ module Fusion
|
|
|
440
459
|
def parse_corepat
|
|
441
460
|
t = peek
|
|
442
461
|
case t.type
|
|
443
|
-
when :number, :string
|
|
444
|
-
|
|
445
|
-
|
|
462
|
+
when :number, :string, :true_kw, :false_kw, :null_kw
|
|
463
|
+
advance
|
|
464
|
+
Pattern::PLit.new(value: t.value)
|
|
465
|
+
when :minus
|
|
466
|
+
advance
|
|
467
|
+
Pattern::PLit.new(value: -expect(:number).value) # negative literal
|
|
446
468
|
when :lbracket then parse_arraypat
|
|
447
469
|
when :lbrace then parse_objectpat
|
|
448
470
|
when :ident
|
|
@@ -473,12 +495,14 @@ module Fusion
|
|
|
473
495
|
advance
|
|
474
496
|
break if at?(:rbracket) # trailing comma
|
|
475
497
|
raise ParseError, "a pattern may contain at most one `...rest` (at #{peek.pos})" if at?(:spread)
|
|
498
|
+
|
|
476
499
|
items << PatternItem.new(pattern: parse_guardedpat)
|
|
477
500
|
end
|
|
478
501
|
break
|
|
479
502
|
end
|
|
480
503
|
items << PatternItem.new(pattern: parse_guardedpat)
|
|
481
504
|
break unless at?(:comma)
|
|
505
|
+
|
|
482
506
|
advance
|
|
483
507
|
end
|
|
484
508
|
expect(:rbracket)
|
|
@@ -499,14 +523,17 @@ module Fusion
|
|
|
499
523
|
unless at?(:rbrace)
|
|
500
524
|
raise ParseError, "in an object pattern, `...rest` must come last (at #{peek.pos})"
|
|
501
525
|
end
|
|
526
|
+
|
|
502
527
|
break
|
|
503
528
|
end
|
|
504
529
|
key_pos = peek.pos
|
|
505
530
|
pair = parse_pattern_pair
|
|
506
531
|
raise ParseError, "duplicate key #{pair.key.inspect} (at #{key_pos})" if keys.include?(pair.key)
|
|
532
|
+
|
|
507
533
|
keys << pair.key
|
|
508
534
|
pairs << pair
|
|
509
535
|
break unless at?(:comma)
|
|
536
|
+
|
|
510
537
|
advance
|
|
511
538
|
end
|
|
512
539
|
expect(:rbrace)
|
|
@@ -530,12 +557,22 @@ module Fusion
|
|
|
530
557
|
end
|
|
531
558
|
|
|
532
559
|
# ---- token helpers ----
|
|
533
|
-
def peek(o = 0)
|
|
534
|
-
|
|
535
|
-
|
|
560
|
+
def peek(o = 0)
|
|
561
|
+
@toks[@i + o]
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def at?(type)
|
|
565
|
+
peek.type == type
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def advance
|
|
569
|
+
@toks[@i].tap { @i += 1 }
|
|
570
|
+
end
|
|
571
|
+
|
|
536
572
|
def expect(type)
|
|
537
573
|
t = peek
|
|
538
574
|
raise ParseError, "Expected #{type} but got #{t.type} (#{t.value.inspect}) at #{t.pos}" unless t.type == type
|
|
575
|
+
|
|
539
576
|
advance
|
|
540
577
|
end
|
|
541
578
|
end
|
data/lib/fusion/version.rb
CHANGED
data/lib/fusion.rb
CHANGED
data/stdlib/all.fsn
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
# the rest.
|
|
5
5
|
(
|
|
6
6
|
{"f": _ ? @Function, "c": []} => true,
|
|
7
|
-
{"f": f ? @Function, "c": [x, ...rest]} =>
|
|
7
|
+
{"f": f ? @Function, "c": [x, ...rest]} => x | f | (
|
|
8
8
|
_ ? @falsey => false,
|
|
9
9
|
_ => {"f": f, "c": rest} | @,
|
|
10
10
|
),
|
|
11
11
|
{"f": f ? @Function, "c": obj ? @Object} => {"f": f, "c": obj | @values} | @,
|
|
12
|
-
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@all", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @
|
|
12
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@all", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Collection}"]},
|
|
13
13
|
)
|
data/stdlib/any.fsn
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
(
|
|
5
5
|
{"f": _ ? @Function, "c": []} => false,
|
|
6
6
|
{"f": f ? @Function, "c": [x, ...rest]} => x | f | (
|
|
7
|
-
_ ? @
|
|
8
|
-
_ =>
|
|
7
|
+
_ ? @truthy => true,
|
|
8
|
+
_ => {"f": f, "c": rest} | @,
|
|
9
9
|
),
|
|
10
10
|
{"f": f ? @Function, "c": obj ? @Object} => {"f": f, "c": obj | @values} | @,
|
|
11
|
-
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@any", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @
|
|
11
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@any", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Collection}"]},
|
|
12
12
|
)
|
data/stdlib/compact.fsn
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Drop null entries from an array (by element) or an object (by value). Built on @filter.
|
|
2
2
|
(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@compact", "status": 0, "input": x, "expected": ["_ ? @Array", "_ ? @Object"]}
|
|
3
|
+
c ? @Collection => c |? (null => false, _ => true),
|
|
4
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@compact", "status": 0, "input": x, "expected": ["_ ? @Collection"]}
|
|
6
5
|
)
|
data/stdlib/concat.fsn
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# Concatenate
|
|
1
|
+
# Concatenate an array of strings, of any length. Built on @join with an empty
|
|
2
|
+
# separator.
|
|
2
3
|
(
|
|
3
|
-
|
|
4
|
-
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@concat", "status": 0, "input": x, "expected": ["
|
|
4
|
+
strings ? (xs ? @Array => {"c": xs, "f": @String} | @all) => [strings, ""] | @join,
|
|
5
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@concat", "status": 0, "input": x, "expected": ["_ ? (xs ? @Array => {\"c\": xs, \"f\": @String} | @all)"]}
|
|
5
6
|
)
|