fusion-lang 0.0.1.alpha2 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -8
- data/docs/lang/design.md +240 -51
- data/docs/lang/implementation.md +238 -0
- data/docs/lang/roadmap.md +20 -36
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +60 -15
- data/docs/user/reference.md +356 -142
- data/docs/user/tutorial.md +21 -19
- data/examples/double.fsn +1 -1
- data/examples/factorial.fsn +2 -2
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/json_test.fsn +4 -0
- data/examples/palindrome.fsn +1 -1
- data/exe/fusion +10 -10
- data/lib/fusion/ast.rb +2 -1
- data/lib/fusion/cli/decoder.rb +10 -5
- data/lib/fusion/cli/options.rb +130 -60
- data/lib/fusion/cli/parser.rb +3 -3
- data/lib/fusion/cli/repl.rb +30 -25
- data/lib/fusion/cli/serializer.rb +5 -4
- data/lib/fusion/cli.rb +119 -48
- data/lib/fusion/interpreter/builtins.rb +260 -151
- data/lib/fusion/interpreter/env.rb +42 -12
- data/lib/fusion/interpreter/error_val.rb +42 -20
- data/lib/fusion/interpreter/thunk.rb +53 -0
- data/lib/fusion/interpreter.rb +239 -82
- data/lib/fusion/lexer.rb +69 -3
- data/lib/fusion/parser.rb +189 -51
- data/lib/fusion/version.rb +1 -1
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +6 -0
- data/stdlib/concat.fsn +5 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/gt.fsn +9 -0
- data/stdlib/gte.fsn +9 -0
- data/stdlib/lt.fsn +9 -0
- data/stdlib/lte.fsn +9 -0
- data/stdlib/map.fsn +6 -4
- data/stdlib/range.fsn +2 -2
- data/stdlib/reduce.fsn +8 -0
- data/stdlib/sanitize.fsn +2 -2
- data/stdlib/truthy.fsn +7 -0
- metadata +18 -4
- data/lib/fusion/interpreter/file_thunk.rb +0 -39
- data/stdlib/mapValues.fsn +0 -5
- data/stdlib/math/square.fsn +0 -4
data/lib/fusion/lexer.rb
CHANGED
|
@@ -15,9 +15,11 @@ module Fusion
|
|
|
15
15
|
"[" => :lbracket, "]" => :rbracket,
|
|
16
16
|
"{" => :lbrace, "}" => :rbrace,
|
|
17
17
|
"," => :comma, ":" => :colon,
|
|
18
|
-
"
|
|
18
|
+
"?" => :question, "." => :dot,
|
|
19
19
|
"@" => :at, "/" => :slash,
|
|
20
20
|
"=" => :equals,
|
|
21
|
+
"+" => :plus, "-" => :minus, "*" => :star,
|
|
22
|
+
"%" => :percent, "~" => :tilde,
|
|
21
23
|
}.freeze
|
|
22
24
|
|
|
23
25
|
def initialize(src)
|
|
@@ -31,6 +33,11 @@ module Fusion
|
|
|
31
33
|
loop do
|
|
32
34
|
t = next_token
|
|
33
35
|
out << t
|
|
36
|
+
# A file-reference path is one tight token, lexed only right after `@`/`@@`.
|
|
37
|
+
if t.type == :at || t.type == :atat
|
|
38
|
+
p = try_lex_path
|
|
39
|
+
out << p unless p.nil?
|
|
40
|
+
end
|
|
34
41
|
break if t.type == :eof
|
|
35
42
|
end
|
|
36
43
|
out
|
|
@@ -55,6 +62,36 @@ module Fusion
|
|
|
55
62
|
@i += 3
|
|
56
63
|
return Token.new(type: :spread, value: "...", pos: start)
|
|
57
64
|
end
|
|
65
|
+
if c == "@" && peek(1) == "@"
|
|
66
|
+
@i += 2
|
|
67
|
+
return Token.new(type: :atat, value: "@@", pos: start)
|
|
68
|
+
end
|
|
69
|
+
# Two-character operators, matched before their single-char prefixes.
|
|
70
|
+
if c == "=" && peek(1) == "="
|
|
71
|
+
@i += 2
|
|
72
|
+
return Token.new(type: :eqeq, value: "==", pos: start)
|
|
73
|
+
end
|
|
74
|
+
if c == "/" && peek(1) == "/"
|
|
75
|
+
@i += 2
|
|
76
|
+
return Token.new(type: :slashslash, value: "//", pos: start)
|
|
77
|
+
end
|
|
78
|
+
if c == "?" && peek(1) == "?"
|
|
79
|
+
@i += 2
|
|
80
|
+
return Token.new(type: :qq, value: "??", pos: start)
|
|
81
|
+
end
|
|
82
|
+
if c == "&" && peek(1) == "&"
|
|
83
|
+
@i += 2
|
|
84
|
+
return Token.new(type: :andand, value: "&&", pos: start)
|
|
85
|
+
end
|
|
86
|
+
if c == "|"
|
|
87
|
+
case peek(1)
|
|
88
|
+
when "|" then @i += 2; return Token.new(type: :oror, value: "||", pos: start)
|
|
89
|
+
when ":" then @i += 2; return Token.new(type: :pipemap, value: "|:", pos: start)
|
|
90
|
+
when "?" then @i += 2; return Token.new(type: :pipefilter, value: "|?", pos: start)
|
|
91
|
+
when "+" then @i += 2; return Token.new(type: :pipereduce, value: "|+", pos: start)
|
|
92
|
+
else @i += 1; return Token.new(type: :pipe, value: "|", pos: start)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
58
95
|
if c == "!"
|
|
59
96
|
@i += 1
|
|
60
97
|
return Token.new(type: :bang, value: "!", pos: start)
|
|
@@ -62,7 +99,7 @@ module Fusion
|
|
|
62
99
|
if c == '"'
|
|
63
100
|
return lex_string(start)
|
|
64
101
|
end
|
|
65
|
-
if digit?(c)
|
|
102
|
+
if digit?(c)
|
|
66
103
|
return lex_number(start)
|
|
67
104
|
end
|
|
68
105
|
if ident_start?(c)
|
|
@@ -143,7 +180,6 @@ module Fusion
|
|
|
143
180
|
|
|
144
181
|
def lex_number(start)
|
|
145
182
|
j = @i
|
|
146
|
-
j += 1 if @src[j] == "-"
|
|
147
183
|
j += 1 while j < @n && digit?(@src[j])
|
|
148
184
|
is_float = false
|
|
149
185
|
if @src[j] == "." && digit?(@src[j + 1])
|
|
@@ -176,6 +212,36 @@ module Fusion
|
|
|
176
212
|
end
|
|
177
213
|
end
|
|
178
214
|
|
|
215
|
+
# Path lexing — see `tokens`. Only called immediately after `@`/`@@`, with no
|
|
216
|
+
# whitespace skipped, so the path must be tight. Returns nil (position restored)
|
|
217
|
+
# when nothing path-like follows, i.e. a bare `@`/`@@`.
|
|
218
|
+
def try_lex_path
|
|
219
|
+
start = @i
|
|
220
|
+
buf = +""
|
|
221
|
+
while peek == "." && peek(1) == "." && peek(2) == "/"
|
|
222
|
+
buf << "../"
|
|
223
|
+
@i += 3
|
|
224
|
+
end
|
|
225
|
+
unless ident_start?(peek)
|
|
226
|
+
@i = start
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
buf << lex_ident_text
|
|
230
|
+
while peek == "/" && ident_start?(peek(1))
|
|
231
|
+
@i += 1
|
|
232
|
+
buf << "/" << lex_ident_text
|
|
233
|
+
end
|
|
234
|
+
Token.new(type: :path, value: buf, pos: start)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def lex_ident_text
|
|
238
|
+
j = @i
|
|
239
|
+
j += 1 while j < @n && ident_part?(@src[j])
|
|
240
|
+
text = @src[@i...j]
|
|
241
|
+
@i = j
|
|
242
|
+
text
|
|
243
|
+
end
|
|
244
|
+
|
|
179
245
|
def digit?(c) = c && c >= "0" && c <= "9"
|
|
180
246
|
def ident_start?(c) = c && (c =~ /[A-Za-z_]/)
|
|
181
247
|
def ident_part?(c) = c && (c =~ /[A-Za-z0-9_]/)
|
data/lib/fusion/parser.rb
CHANGED
|
@@ -22,16 +22,16 @@ module Fusion
|
|
|
22
22
|
|
|
23
23
|
# Parse a complete program. The lexer and parser report failures by raising
|
|
24
24
|
# ParseError; this single entry point rescues them and returns a standardized
|
|
25
|
-
# syntax_error value, so no caller ever sees a raw Ruby error. `
|
|
26
|
-
# syntax_error's
|
|
27
|
-
def self.parse_file(src,
|
|
25
|
+
# syntax_error value, so no caller ever sees a raw Ruby error. `site` is the
|
|
26
|
+
# syntax_error's `{origin:, file:}` context.
|
|
27
|
+
def self.parse_file(src, site:)
|
|
28
28
|
toks = Lexer.new(src).tokens
|
|
29
29
|
p = new(toks)
|
|
30
30
|
expr = p.parse_expr
|
|
31
31
|
p.expect(:eof)
|
|
32
32
|
expr
|
|
33
33
|
rescue ParseError => err
|
|
34
|
-
Interpreter::ErrorVal.
|
|
34
|
+
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.message)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Parse one REPL entry — a statement (`identifier "=" expr`) or a bare
|
|
@@ -39,14 +39,14 @@ module Fusion
|
|
|
39
39
|
# parse_file, a standardized syntax_error value instead of ever raising. The
|
|
40
40
|
# REPL uses the error/non-error distinction to tell "keep editing" (didn't
|
|
41
41
|
# parse yet) from "evaluate now" (a complete statement or expression).
|
|
42
|
-
def self.parse_repl(src,
|
|
42
|
+
def self.parse_repl(src, site:)
|
|
43
43
|
toks = Lexer.new(src).tokens
|
|
44
44
|
p = new(toks)
|
|
45
45
|
entry = p.parse_repl_entry
|
|
46
46
|
p.expect(:eof)
|
|
47
47
|
entry
|
|
48
48
|
rescue ParseError => err
|
|
49
|
-
Interpreter::ErrorVal.
|
|
49
|
+
Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.message)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# A leading `identifier =` marks a statement; anything else is an expression.
|
|
@@ -67,35 +67,136 @@ module Fusion
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def parse_expr
|
|
70
|
-
|
|
70
|
+
parse_or
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
# --- Operator sugar (reference §2.7) --------------------------------------
|
|
74
|
+
# The ladder below desugars every operator to a pipe into an `@OP.*` member
|
|
75
|
+
# (or, for the map-pipes, a stdlib call). Tightest to loosest:
|
|
76
|
+
# postfix · unary (! - / ~) · pipe (| |: |? |+) · * / % // · + - · ?? · == · && · ||
|
|
77
|
+
# `@OP.*` is a shadowable `:name` reference, so a local `@OP` reskins the operators.
|
|
78
|
+
|
|
79
|
+
def parse_or
|
|
80
|
+
operands = [parse_and]
|
|
81
|
+
while at?(:oror)
|
|
76
82
|
advance
|
|
77
|
-
|
|
78
|
-
left = Expression::Pipe.new(left: left, right: right)
|
|
83
|
+
operands << parse_and
|
|
79
84
|
end
|
|
80
|
-
|
|
85
|
+
fold_operator(operands, "or")
|
|
81
86
|
end
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
def parse_and
|
|
89
|
+
operands = [parse_equality]
|
|
90
|
+
while at?(:andand)
|
|
91
|
+
advance
|
|
92
|
+
operands << parse_equality
|
|
93
|
+
end
|
|
94
|
+
fold_operator(operands, "and")
|
|
95
|
+
end
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def parse_prefix
|
|
92
|
-
if at?(:bang)
|
|
97
|
+
def parse_equality
|
|
98
|
+
operands = [parse_ordering]
|
|
99
|
+
while at?(:eqeq)
|
|
93
100
|
advance
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
operands << parse_ordering
|
|
102
|
+
end
|
|
103
|
+
fold_operator(operands, "equal")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `??` (compare) is binary and left-associative — it does not fold.
|
|
107
|
+
def parse_ordering
|
|
108
|
+
node = parse_additive
|
|
109
|
+
while at?(:qq)
|
|
110
|
+
advance
|
|
111
|
+
node = pipe_operator([node, parse_additive], "compare")
|
|
112
|
+
end
|
|
113
|
+
node
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A run of `+`/`-` folds into one `@OP.sum`; each `-` term is negated.
|
|
117
|
+
def parse_additive
|
|
118
|
+
terms = [parse_multiplicative]
|
|
119
|
+
folded = false
|
|
120
|
+
while at?(:plus) || at?(:minus)
|
|
121
|
+
negated = at?(:minus)
|
|
122
|
+
advance
|
|
123
|
+
term = parse_multiplicative
|
|
124
|
+
terms << (negated ? negate(term) : term)
|
|
125
|
+
folded = true
|
|
126
|
+
end
|
|
127
|
+
folded ? pipe_operator(terms, "sum") : terms.first
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A run of `*`/`/` folds into one `@OP.product` (each `/` term inverted); `%`/`//`
|
|
131
|
+
# are binary and break the run. Standard left-to-right: `a * b % c` is `(a * b) % c`.
|
|
132
|
+
def parse_multiplicative
|
|
133
|
+
node = parse_pipe
|
|
134
|
+
run = nil # accumulating product-run terms, or nil
|
|
135
|
+
loop do
|
|
136
|
+
if at?(:star) || at?(:slash)
|
|
137
|
+
inverted = at?(:slash)
|
|
138
|
+
advance
|
|
139
|
+
term = parse_pipe
|
|
140
|
+
term = pipe_into(term, "invert") if inverted
|
|
141
|
+
if run
|
|
142
|
+
run << term
|
|
143
|
+
else
|
|
144
|
+
run = [node, term]
|
|
145
|
+
end
|
|
146
|
+
elsif at?(:percent) || at?(:slashslash)
|
|
147
|
+
node = pipe_operator(run, "product") if run
|
|
148
|
+
run = nil
|
|
149
|
+
op = at?(:percent) ? "modulo" : "quotient"
|
|
150
|
+
advance
|
|
151
|
+
node = pipe_operator([node, parse_pipe], op)
|
|
152
|
+
else
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
run ? pipe_operator(run, "product") : node
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def parse_pipe
|
|
160
|
+
node = parse_unary
|
|
161
|
+
loop do
|
|
162
|
+
if at?(:pipe)
|
|
163
|
+
advance
|
|
164
|
+
node = Expression::Pipe.new(left: node, right: parse_unary)
|
|
165
|
+
elsif at?(:pipemap) || at?(:pipefilter) || at?(:pipereduce)
|
|
166
|
+
target = { pipemap: "map", pipefilter: "filter", pipereduce: "reduce" }[peek.type]
|
|
167
|
+
advance
|
|
168
|
+
arg = Expression::ObjLit.new(pairs: [
|
|
169
|
+
KeyValuePair.new(key: "c", value: node),
|
|
170
|
+
KeyValuePair.new(key: "f", value: parse_unary),
|
|
171
|
+
])
|
|
172
|
+
node = Expression::Pipe.new(left: arg, right: name_ref(target))
|
|
96
173
|
else
|
|
97
|
-
|
|
174
|
+
break
|
|
98
175
|
end
|
|
176
|
+
end
|
|
177
|
+
node
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Tokens that can begin a unary operand — used to decide whether `!` has an
|
|
181
|
+
# operand or is the bare `!null`.
|
|
182
|
+
PRIMARY_STARTERS = %i[number string true_kw false_kw null_kw bang minus slash tilde
|
|
183
|
+
lbracket lbrace lparen ident at atat].freeze
|
|
184
|
+
|
|
185
|
+
# Unary prefixes: `!x` builds an error (bare `!` is `!null`); `-x` negates,
|
|
186
|
+
# `/x` inverts, `~x` is logical not. All bind tighter than `|`.
|
|
187
|
+
def parse_unary
|
|
188
|
+
if at?(:bang)
|
|
189
|
+
advance
|
|
190
|
+
PRIMARY_STARTERS.include?(peek.type) ? Expression::ErrLit.new(payload: parse_unary) : Expression::ErrLit.new(payload: nil)
|
|
191
|
+
elsif at?(:minus)
|
|
192
|
+
advance
|
|
193
|
+
negate(parse_unary)
|
|
194
|
+
elsif at?(:slash)
|
|
195
|
+
advance
|
|
196
|
+
pipe_into(parse_unary, "invert")
|
|
197
|
+
elsif at?(:tilde)
|
|
198
|
+
advance
|
|
199
|
+
pipe_into(parse_unary, "not")
|
|
99
200
|
else
|
|
100
201
|
parse_postfix
|
|
101
202
|
end
|
|
@@ -111,8 +212,15 @@ module Fusion
|
|
|
111
212
|
elsif at?(:lbracket)
|
|
112
213
|
advance
|
|
113
214
|
idx = parse_expr
|
|
114
|
-
|
|
115
|
-
|
|
215
|
+
if at?(:equals)
|
|
216
|
+
advance
|
|
217
|
+
value = parse_expr
|
|
218
|
+
expect(:rbracket)
|
|
219
|
+
node = Expression::IndexSet.new(obj: node, idx: idx, value: value)
|
|
220
|
+
else
|
|
221
|
+
expect(:rbracket)
|
|
222
|
+
node = Expression::Index.new(obj: node, idx: idx)
|
|
223
|
+
end
|
|
116
224
|
else
|
|
117
225
|
break
|
|
118
226
|
end
|
|
@@ -120,6 +228,36 @@ module Fusion
|
|
|
120
228
|
node
|
|
121
229
|
end
|
|
122
230
|
|
|
231
|
+
# --- Desugaring helpers ---------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def name_ref(path) = Expression::FileRef.new(variety: :name, path: path)
|
|
234
|
+
|
|
235
|
+
# `@OP.member`, a shadowable reference.
|
|
236
|
+
def op_member(member) = Expression::Member.new(obj: name_ref("OP"), key: member)
|
|
237
|
+
|
|
238
|
+
# `expr | @OP.member`
|
|
239
|
+
def pipe_into(expr, member) = Expression::Pipe.new(left: expr, right: op_member(member))
|
|
240
|
+
|
|
241
|
+
# `[operands...] | @OP.member`
|
|
242
|
+
def pipe_operator(operands, member)
|
|
243
|
+
arr = Expression::ArrLit.new(items: operands.map { |e| ArrayItem.new(value: e) })
|
|
244
|
+
Expression::Pipe.new(left: arr, right: op_member(member))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A single-operand run collapses to that operand; otherwise fold n-ary.
|
|
248
|
+
def fold_operator(operands, member)
|
|
249
|
+
operands.length == 1 ? operands.first : pipe_operator(operands, member)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# `-x`: a numeric literal folds to a negative literal; anything else negates.
|
|
253
|
+
def negate(expr)
|
|
254
|
+
if expr.is_a?(Expression::Lit) && expr.value.is_a?(Numeric)
|
|
255
|
+
Expression::Lit.new(value: -expr.value)
|
|
256
|
+
else
|
|
257
|
+
pipe_into(expr, "negate")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
123
261
|
def parse_primary
|
|
124
262
|
t = peek
|
|
125
263
|
case t.type
|
|
@@ -130,35 +268,34 @@ module Fusion
|
|
|
130
268
|
when :lparen then parse_function_or_group
|
|
131
269
|
when :ident then advance; Expression::Ident.new(name: t.value)
|
|
132
270
|
when :at then parse_fileref
|
|
271
|
+
when :atat then parse_superref
|
|
133
272
|
else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}"
|
|
134
273
|
end
|
|
135
274
|
end
|
|
136
275
|
|
|
276
|
+
# `@@` is super. Bare `@@` is super of the current file's own name; `@@name`
|
|
277
|
+
# (or a downward `@@dir/name`) is a *stable* reference to that name — it skips
|
|
278
|
+
# the sibling `name.fsn`, resolving builtin → stdlib, so a user's local shadow
|
|
279
|
+
# can't intercept it. `@@../…` is meaningless (super of an upward path) and
|
|
280
|
+
# falls through to a parse error.
|
|
281
|
+
def parse_superref
|
|
282
|
+
expect(:atat)
|
|
283
|
+
return Expression::FileRef.new(variety: :super, path: nil) unless at?(:path)
|
|
284
|
+
|
|
285
|
+
tok = advance
|
|
286
|
+
raise ParseError, "`@@` cannot take an upward path (at #{tok.pos})" if tok.value.include?("..")
|
|
287
|
+
Expression::FileRef.new(variety: :super_name, path: tok.value)
|
|
288
|
+
end
|
|
289
|
+
|
|
137
290
|
def parse_fileref
|
|
138
291
|
expect(:at)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
while at?(:dot) && peek(1)&.type == :dot
|
|
147
|
-
advance; advance # consume the two dots of ..
|
|
148
|
-
parts << ".."
|
|
149
|
-
expect(:slash)
|
|
150
|
-
has_dotdot = true
|
|
151
|
-
end
|
|
152
|
-
parts << expect(:ident).value
|
|
153
|
-
while at?(:slash)
|
|
154
|
-
advance
|
|
155
|
-
parts << expect(:ident).value
|
|
156
|
-
end
|
|
157
|
-
# A reference is eligible for builtin/stdlib fallback (:name) iff it does NOT
|
|
158
|
-
# contain "../". Downward paths like "dir/a" are still eligible; only "../"
|
|
159
|
-
# (escaping upward) forces pure file-path (:path) resolution.
|
|
160
|
-
bare = !has_dotdot
|
|
161
|
-
Expression::FileRef.new(variety: bare ? :name : :path, path: parts.join("/"))
|
|
292
|
+
return Expression::FileRef.new(variety: :self, path: nil) unless at?(:path)
|
|
293
|
+
|
|
294
|
+
# A reference is eligible for builtin/stdlib fallback (:name) iff it has no
|
|
295
|
+
# "../"; downward paths stay eligible, only "../" forces file-only (:path).
|
|
296
|
+
# The lexer produced the whole path as one tight token (see Lexer#try_lex_path).
|
|
297
|
+
path = advance.value
|
|
298
|
+
Expression::FileRef.new(variety: path.include?("..") ? :path : :name, path: path)
|
|
162
299
|
end
|
|
163
300
|
|
|
164
301
|
def parse_array
|
|
@@ -275,7 +412,7 @@ module Fusion
|
|
|
275
412
|
|
|
276
413
|
# Tokens that can begin a `guardedpat` (used to detect whether `!` is
|
|
277
414
|
# followed by a payload pattern or stands alone).
|
|
278
|
-
GUARDEDPAT_STARTERS = %i[number string true_kw false_kw null_kw
|
|
415
|
+
GUARDEDPAT_STARTERS = %i[number string true_kw false_kw null_kw minus
|
|
279
416
|
lbracket lbrace ident].freeze
|
|
280
417
|
|
|
281
418
|
def parse_errpat
|
|
@@ -305,6 +442,7 @@ module Fusion
|
|
|
305
442
|
case t.type
|
|
306
443
|
when :number, :string then advance; Pattern::PLit.new(value: t.value)
|
|
307
444
|
when :true_kw, :false_kw, :null_kw then advance; Pattern::PLit.new(value: t.value)
|
|
445
|
+
when :minus then advance; Pattern::PLit.new(value: -expect(:number).value) # negative literal
|
|
308
446
|
when :lbracket then parse_arraypat
|
|
309
447
|
when :lbrace then parse_objectpat
|
|
310
448
|
when :ident
|
data/lib/fusion/version.rb
CHANGED
data/stdlib/all.fsn
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Return true if every item satisfies the predicate "f" (a truthy result) — over an
|
|
2
|
+
# array's elements or an object's values. Input: {"f": predicate, "c": array-or-object}.
|
|
3
|
+
# Short-circuits: the first item whose predicate is falsey yields false without testing
|
|
4
|
+
# the rest.
|
|
5
|
+
(
|
|
6
|
+
{"f": _ ? @Function, "c": []} => true,
|
|
7
|
+
{"f": f ? @Function, "c": [x, ...rest]} => (x | f) | (
|
|
8
|
+
_ ? @falsey => false,
|
|
9
|
+
_ => {"f": f, "c": rest} | @,
|
|
10
|
+
),
|
|
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\": _ ? @Array}", "{\"f\": _ ? @Function, \"c\": _ ? @Object}"]},
|
|
13
|
+
)
|
data/stdlib/any.fsn
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# True if any item satisfies the predicate — over an array's elements or an object's
|
|
2
|
+
# values. Input: {"f": predicate, "c": array-or-object}. Short-circuits on the first
|
|
3
|
+
# truthy result.
|
|
4
|
+
(
|
|
5
|
+
{"f": _ ? @Function, "c": []} => false,
|
|
6
|
+
{"f": f ? @Function, "c": [x, ...rest]} => x | f | (
|
|
7
|
+
_ ? @falsey => {"f": f, "c": rest} | @,
|
|
8
|
+
_ => true,
|
|
9
|
+
),
|
|
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\": _ ? @Array}", "{\"f\": _ ? @Function, \"c\": _ ? @Object}"]},
|
|
12
|
+
)
|
data/stdlib/chars.fsn
ADDED
data/stdlib/compact.fsn
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Drop null entries from an array (by element) or an object (by value). Built on @filter.
|
|
2
|
+
(
|
|
3
|
+
xs ? @Array => {"f": (null => false, _ => true), "c": xs} | @filter,
|
|
4
|
+
obj ? @Object => {"f": (null => false, _ => true), "c": obj} | @filter,
|
|
5
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@compact", "status": 0, "input": x, "expected": ["_ ? @Array", "_ ? @Object"]}
|
|
6
|
+
)
|
data/stdlib/concat.fsn
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Concatenate two strings. Input: [a, b]. Built on @join with an empty separator.
|
|
2
|
+
(
|
|
3
|
+
[a ? @String, b ? @String] => [[a, b], ""] | @join,
|
|
4
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@concat", "status": 0, "input": x, "expected": ["[_ ? @String, _ ? @String]"]}
|
|
5
|
+
)
|
data/stdlib/falsey.fsn
ADDED
data/stdlib/filter.fsn
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Keep the array elements (or object values) for which f(x) is truthy. Input:
|
|
2
|
+
# {"f": predicate, "c": array-or-object}. Like @map, it is polymorphic on c.
|
|
3
|
+
(
|
|
4
|
+
{"f": _ ? @Function, "c": []} => [],
|
|
5
|
+
{"f": f ? @Function, "c": [x, ...rest]} => x | f | (
|
|
6
|
+
_ ? @falsey => {"f": f, "c": rest} | @,
|
|
7
|
+
_ => [x, ...({"f": f, "c": rest} | @)],
|
|
8
|
+
),
|
|
9
|
+
{"f": f ? @Function, "c": obj ? @Object} =>
|
|
10
|
+
{"f": (e => e | ([_, v] => v | f)), "c": {"f": (k => [k, obj[k]]), "c": obj | @keys} | @map} | @ | @toObject,
|
|
11
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@filter", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Array}", "{\"f\": _ ? @Function, \"c\": _ ? @Object}"]}
|
|
12
|
+
)
|
data/stdlib/flatten.fsn
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Recursively flatten nested arrays into one flat array.
|
|
2
|
+
(
|
|
3
|
+
[] => [],
|
|
4
|
+
[x ? @Array, ...rest] => [...(x | @), ...(rest | @)],
|
|
5
|
+
[x, ...rest] => [x, ...(rest | @)],
|
|
6
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@flatten", "status": 0, "input": x, "expected": ["_ ? @Array"]}
|
|
7
|
+
)
|
data/stdlib/gt.fsn
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Strictly greater: interprets an @OP.compare result. Use as `[a, b] | @OP.compare | @gt`.
|
|
2
|
+
# A partial order's compare may return null (incomparable); that propagates as null.
|
|
3
|
+
(
|
|
4
|
+
-1 => false,
|
|
5
|
+
0 => false,
|
|
6
|
+
1 => true,
|
|
7
|
+
null => null,
|
|
8
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@gt", "status": 0, "input": x, "expected": ["-1", "0", "1", "null"]},
|
|
9
|
+
)
|
data/stdlib/gte.fsn
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Greater-or-equal: interprets an @OP.compare result. Use as `[a, b] | @OP.compare | @gte`.
|
|
2
|
+
# A partial order's compare may return null (incomparable); that propagates as null.
|
|
3
|
+
(
|
|
4
|
+
-1 => false,
|
|
5
|
+
0 => true,
|
|
6
|
+
1 => true,
|
|
7
|
+
null => null,
|
|
8
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@gte", "status": 0, "input": x, "expected": ["-1", "0", "1", "null"]},
|
|
9
|
+
)
|
data/stdlib/lt.fsn
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Strictly less: interprets an @OP.compare result. Use as `[a, b] | @OP.compare | @lt`.
|
|
2
|
+
# A partial order's compare may return null (incomparable); that propagates as null.
|
|
3
|
+
(
|
|
4
|
+
-1 => true,
|
|
5
|
+
0 => false,
|
|
6
|
+
1 => false,
|
|
7
|
+
null => null,
|
|
8
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@lt", "status": 0, "input": x, "expected": ["-1", "0", "1", "null"]},
|
|
9
|
+
)
|
data/stdlib/lte.fsn
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Less-or-equal: interprets an @OP.compare result. Use as `[a, b] | @OP.compare | @lte`.
|
|
2
|
+
# A partial order's compare may return null (incomparable); that propagates as null.
|
|
3
|
+
(
|
|
4
|
+
-1 => true,
|
|
5
|
+
0 => true,
|
|
6
|
+
1 => false,
|
|
7
|
+
null => null,
|
|
8
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@lte", "status": 0, "input": x, "expected": ["-1", "0", "1", "null"]},
|
|
9
|
+
)
|
data/stdlib/map.fsn
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# Apply f to each element of an array, or to each value of an object (keeping the
|
|
2
|
+
# keys). Input: {"f": fn, "c": array-or-object}.
|
|
1
3
|
(
|
|
2
|
-
{"f": _, "
|
|
3
|
-
{"f": f, "
|
|
4
|
-
{"f":
|
|
5
|
-
x => !{"kind": "argument_error", "
|
|
4
|
+
{"f": _ ? @Function, "c": []} => [],
|
|
5
|
+
{"f": f ? @Function, "c": [x, ...rest]} => [x | f, ...({"f": f, "c": rest} | @)],
|
|
6
|
+
{"f": f ? @Function, "c": obj ? @Object} => {"f": (k => [k, obj[k] | f]), "c": obj | @keys} | @ | @toObject,
|
|
7
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@map", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Array}", "{\"f\": _ ? @Function, \"c\": _ ? @Object}"]}
|
|
6
8
|
)
|
data/stdlib/range.fsn
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(
|
|
2
2
|
0 => [],
|
|
3
|
-
n ? (m ? @Integer => [
|
|
4
|
-
x => !{"kind": "
|
|
3
|
+
n ? (m ? @Integer => [m, 0] | @OP.compare | @gt) => [...([n, -1] | @OP.sum | @), [n, -1] | @OP.sum],
|
|
4
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@range", "status": 0, "input": x, "expected": ["_ ? (m ? @Integer => [m, 0] | @OP.compare | @gte)"]}
|
|
5
5
|
)
|
data/stdlib/reduce.fsn
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Left fold over a NON-EMPTY array with no seed: combine the first two elements
|
|
2
|
+
# with f, then fold that result with the rest. Input: {"f": binaryFn, "c": array}.
|
|
3
|
+
# `f` receives the pair [acc, element]. An empty list is an error.
|
|
4
|
+
(
|
|
5
|
+
{"f": _ ? @Function, "c": [x]} => x,
|
|
6
|
+
{"f": f ? @Function, "c": [x, y, ...rest]} => {"f": f, "c": [[x, y] | f, ...rest]} | @,
|
|
7
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@reduce", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": [_, ...]}"]}
|
|
8
|
+
)
|
data/stdlib/sanitize.fsn
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
(
|
|
7
7
|
v ? @Function => "<function>",
|
|
8
8
|
v ? @NonFinite => [["<", v | @toString] | @concat, ">"] | @concat,
|
|
9
|
-
v ? @Array => {"f": @sanitize, "
|
|
10
|
-
v ? @Object => {"f": @sanitize, "
|
|
9
|
+
v ? @Array => {"f": @sanitize, "c": v} | @map,
|
|
10
|
+
v ? @Object => {"f": @sanitize, "c": v} | @map,
|
|
11
11
|
v => v
|
|
12
12
|
)
|