fusion-lang 0.0.1.alpha2 → 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 +11 -9
- data/Rakefile +8 -0
- data/docs/lang/design.md +289 -51
- data/docs/lang/implementation.md +279 -0
- data/docs/lang/roadmap.md +20 -36
- data/docs/user/explanation.md +5 -10
- data/docs/user/how-to-guides.md +145 -15
- data/docs/user/reference.md +365 -140
- data/docs/user/tutorial.md +22 -19
- data/examples/double.fsn +4 -1
- data/examples/factorial.fsn +6 -3
- data/examples/fizzbuzz.fsn +1 -4
- data/examples/gcd.fsn +9 -0
- data/examples/json_test.fsn +4 -0
- data/examples/matrix/OP.fsn +2 -0
- data/examples/matrix/average.fsn +2 -0
- data/examples/matrix/solve.fsn +2 -0
- data/examples/palindrome.fsn +1 -1
- data/exe/fusion +12 -12
- data/lib/fusion/ast.rb +76 -27
- data/lib/fusion/atom.rb +1 -1
- data/lib/fusion/cli/decoder.rb +13 -8
- data/lib/fusion/cli/encoder.rb +2 -2
- data/lib/fusion/cli/options.rb +134 -61
- data/lib/fusion/cli/parser.rb +4 -4
- data/lib/fusion/cli/repl.rb +32 -27
- data/lib/fusion/cli/serializer.rb +11 -11
- data/lib/fusion/cli.rb +120 -49
- data/lib/fusion/interpreter/builtins.rb +298 -160
- 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 +263 -98
- data/lib/fusion/lexer.rb +125 -37
- data/lib/fusion/parser.rb +245 -70
- data/lib/fusion/version.rb +3 -1
- data/lib/fusion.rb +0 -1
- data/stdlib/all.fsn +13 -0
- data/stdlib/any.fsn +12 -0
- data/stdlib/chars.fsn +5 -0
- data/stdlib/compact.fsn +5 -0
- data/stdlib/concat.fsn +6 -0
- data/stdlib/entries.fsn +6 -0
- data/stdlib/falsey.fsn +6 -0
- data/stdlib/filter.fsn +12 -0
- data/stdlib/flatten.fsn +7 -0
- data/stdlib/map.fsn +7 -4
- 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/reduce.fsn +8 -0
- data/stdlib/safe.fsn +7 -0
- data/stdlib/sanitize.fsn +2 -3
- data/stdlib/toObject.fsn +7 -0
- data/stdlib/truthy.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 +50 -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/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
|
-
rescue ParseError =>
|
|
34
|
-
Interpreter::ErrorVal.
|
|
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
|
|
@@ -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
|
-
rescue ParseError =>
|
|
49
|
-
Interpreter::ErrorVal.
|
|
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.
|
|
@@ -67,35 +67,149 @@ 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
|
+
# 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).
|
|
112
|
+
def parse_ordering
|
|
113
|
+
node = parse_additive
|
|
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
|
|
124
|
+
end
|
|
125
|
+
node
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A run of `+`/`-` folds into one `@OP.sum`; each `-` term is negated.
|
|
129
|
+
def parse_additive
|
|
130
|
+
terms = [parse_multiplicative]
|
|
131
|
+
folded = false
|
|
132
|
+
while at?(:plus) || at?(:minus)
|
|
133
|
+
negated = at?(:minus)
|
|
134
|
+
advance
|
|
135
|
+
term = parse_multiplicative
|
|
136
|
+
terms << (negated ? negate(term) : term)
|
|
137
|
+
folded = true
|
|
138
|
+
end
|
|
139
|
+
folded ? pipe_operator(terms, "sum") : terms.first
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A run of `*`/`/` folds into one `@OP.product` (each `/` term inverted); `%`/`//`
|
|
143
|
+
# are binary and break the run. Standard left-to-right: `a * b % c` is `(a * b) % c`.
|
|
144
|
+
def parse_multiplicative
|
|
145
|
+
node = parse_pipe
|
|
146
|
+
run = nil # accumulating product-run terms, or nil
|
|
147
|
+
loop do
|
|
148
|
+
if at?(:star) || at?(:slash)
|
|
149
|
+
inverted = at?(:slash)
|
|
150
|
+
advance
|
|
151
|
+
term = parse_pipe
|
|
152
|
+
term = pipe_into(term, "invert") if inverted
|
|
153
|
+
if run
|
|
154
|
+
run << term
|
|
155
|
+
else
|
|
156
|
+
run = [node, term]
|
|
157
|
+
end
|
|
158
|
+
elsif at?(:percent) || at?(:slashslash)
|
|
159
|
+
node = pipe_operator(run, "product") if run
|
|
160
|
+
run = nil
|
|
161
|
+
op = at?(:percent) ? "modulo" : "quotient"
|
|
162
|
+
advance
|
|
163
|
+
node = pipe_operator([node, parse_pipe], op)
|
|
96
164
|
else
|
|
97
|
-
|
|
165
|
+
break
|
|
98
166
|
end
|
|
167
|
+
end
|
|
168
|
+
run ? pipe_operator(run, "product") : node
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse_pipe
|
|
172
|
+
node = parse_unary
|
|
173
|
+
loop do
|
|
174
|
+
if at?(:pipe)
|
|
175
|
+
advance
|
|
176
|
+
node = Expression::Pipe.new(left: node, right: parse_unary)
|
|
177
|
+
elsif at?(:pipemap) || at?(:pipefilter) || at?(:pipereduce)
|
|
178
|
+
target = { pipemap: "map", pipefilter: "filter", pipereduce: "reduce" }[peek.type]
|
|
179
|
+
advance
|
|
180
|
+
arg = Expression::ObjLit.new(
|
|
181
|
+
pairs: [
|
|
182
|
+
KeyValuePair.new(key: "c", value: node),
|
|
183
|
+
KeyValuePair.new(key: "f", value: parse_unary),
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
node = Expression::Pipe.new(left: arg, right: name_ref(target))
|
|
187
|
+
else
|
|
188
|
+
break
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
node
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Tokens that can begin a unary operand — used to decide whether `!` has an
|
|
195
|
+
# operand or is the bare `!null`.
|
|
196
|
+
PRIMARY_STARTERS = [:number, :string, :true_kw, :false_kw, :null_kw, :bang, :minus, :slash, :tilde, :lbracket, :lbrace, :lparen, :ident, :at, :atat].freeze
|
|
197
|
+
|
|
198
|
+
# Unary prefixes: `!x` builds an error (bare `!` is `!null`); `-x` negates,
|
|
199
|
+
# `/x` inverts, `~x` is logical not. All bind tighter than `|`.
|
|
200
|
+
def parse_unary
|
|
201
|
+
if at?(:bang)
|
|
202
|
+
advance
|
|
203
|
+
PRIMARY_STARTERS.include?(peek.type) ? Expression::ErrLit.new(payload: parse_unary) : Expression::ErrLit.new(payload: nil)
|
|
204
|
+
elsif at?(:minus)
|
|
205
|
+
advance
|
|
206
|
+
negate(parse_unary)
|
|
207
|
+
elsif at?(:slash)
|
|
208
|
+
advance
|
|
209
|
+
pipe_into(parse_unary, "invert")
|
|
210
|
+
elsif at?(:tilde)
|
|
211
|
+
advance
|
|
212
|
+
pipe_into(parse_unary, "not")
|
|
99
213
|
else
|
|
100
214
|
parse_postfix
|
|
101
215
|
end
|
|
@@ -111,8 +225,15 @@ module Fusion
|
|
|
111
225
|
elsif at?(:lbracket)
|
|
112
226
|
advance
|
|
113
227
|
idx = parse_expr
|
|
114
|
-
|
|
115
|
-
|
|
228
|
+
if at?(:equals)
|
|
229
|
+
advance
|
|
230
|
+
value = parse_expr
|
|
231
|
+
expect(:rbracket)
|
|
232
|
+
node = Expression::IndexSet.new(obj: node, idx: idx, value: value)
|
|
233
|
+
else
|
|
234
|
+
expect(:rbracket)
|
|
235
|
+
node = Expression::Index.new(obj: node, idx: idx)
|
|
236
|
+
end
|
|
116
237
|
else
|
|
117
238
|
break
|
|
118
239
|
end
|
|
@@ -120,45 +241,78 @@ module Fusion
|
|
|
120
241
|
node
|
|
121
242
|
end
|
|
122
243
|
|
|
244
|
+
# --- Desugaring helpers ---------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def name_ref(path) = Expression::FileRef.new(variety: :name, path: path)
|
|
247
|
+
|
|
248
|
+
# `@OP.member`, a shadowable reference.
|
|
249
|
+
def op_member(member) = Expression::Member.new(obj: name_ref("OP"), key: member)
|
|
250
|
+
|
|
251
|
+
# `expr | @OP.member`
|
|
252
|
+
def pipe_into(expr, member) = Expression::Pipe.new(left: expr, right: op_member(member))
|
|
253
|
+
|
|
254
|
+
# `[operands...] | @OP.member`
|
|
255
|
+
def pipe_operator(operands, member)
|
|
256
|
+
arr = Expression::ArrLit.new(items: operands.map { |e| ArrayItem.new(value: e) })
|
|
257
|
+
Expression::Pipe.new(left: arr, right: op_member(member))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# A single-operand run collapses to that operand; otherwise fold n-ary.
|
|
261
|
+
def fold_operator(operands, member)
|
|
262
|
+
operands.length == 1 ? operands.first : pipe_operator(operands, member)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# `-x`: a numeric literal folds to a negative literal; anything else negates.
|
|
266
|
+
def negate(expr)
|
|
267
|
+
if expr.is_a?(Expression::Lit) && expr.value.is_a?(Numeric)
|
|
268
|
+
Expression::Lit.new(value: -expr.value)
|
|
269
|
+
else
|
|
270
|
+
pipe_into(expr, "negate")
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
123
274
|
def parse_primary
|
|
124
275
|
t = peek
|
|
125
276
|
case t.type
|
|
126
|
-
when :number, :string
|
|
127
|
-
|
|
277
|
+
when :number, :string, :true_kw, :false_kw, :null_kw
|
|
278
|
+
advance
|
|
279
|
+
Expression::Lit.new(value: t.value)
|
|
128
280
|
when :lbracket then parse_array
|
|
129
281
|
when :lbrace then parse_object
|
|
130
282
|
when :lparen then parse_function_or_group
|
|
131
|
-
when :ident
|
|
283
|
+
when :ident
|
|
284
|
+
advance
|
|
285
|
+
Expression::Ident.new(name: t.value)
|
|
132
286
|
when :at then parse_fileref
|
|
287
|
+
when :atat then parse_superref
|
|
133
288
|
else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}"
|
|
134
289
|
end
|
|
135
290
|
end
|
|
136
291
|
|
|
292
|
+
# `@@` is super. Bare `@@` is super of the current file's own name; `@@name`
|
|
293
|
+
# (or a downward `@@dir/name`) is a *stable* reference to that name — it skips
|
|
294
|
+
# the sibling `name.fsn`, resolving builtin → stdlib, so a user's local shadow
|
|
295
|
+
# can't intercept it. `@@../…` is meaningless (super of an upward path) and
|
|
296
|
+
# falls through to a parse error.
|
|
297
|
+
def parse_superref
|
|
298
|
+
expect(:atat)
|
|
299
|
+
return Expression::FileRef.new(variety: :super, path: nil) unless at?(:path)
|
|
300
|
+
|
|
301
|
+
tok = advance
|
|
302
|
+
raise ParseError, "`@@` cannot take an upward path (at #{tok.pos})" if tok.value.include?("..")
|
|
303
|
+
|
|
304
|
+
Expression::FileRef.new(variety: :super_name, path: tok.value)
|
|
305
|
+
end
|
|
306
|
+
|
|
137
307
|
def parse_fileref
|
|
138
308
|
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("/"))
|
|
309
|
+
return Expression::FileRef.new(variety: :self, path: nil) unless at?(:path)
|
|
310
|
+
|
|
311
|
+
# A reference is eligible for builtin/stdlib fallback (:name) iff it has no
|
|
312
|
+
# "../"; downward paths stay eligible, only "../" forces file-only (:path).
|
|
313
|
+
# The lexer produced the whole path as one tight token (see Lexer#try_lex_path).
|
|
314
|
+
path = advance.value
|
|
315
|
+
Expression::FileRef.new(variety: path.include?("..") ? :path : :name, path: path)
|
|
162
316
|
end
|
|
163
317
|
|
|
164
318
|
def parse_array
|
|
@@ -172,6 +326,7 @@ module Fusion
|
|
|
172
326
|
items << ArrayItem.new(value: parse_expr)
|
|
173
327
|
end
|
|
174
328
|
break unless at?(:comma)
|
|
329
|
+
|
|
175
330
|
advance
|
|
176
331
|
end
|
|
177
332
|
expect(:rbracket)
|
|
@@ -192,11 +347,13 @@ module Fusion
|
|
|
192
347
|
key_tok = expect(:string)
|
|
193
348
|
key = key_tok.value
|
|
194
349
|
raise ParseError, "duplicate key #{key.inspect} (at #{key_tok.pos})" if keys.include?(key)
|
|
350
|
+
|
|
195
351
|
keys << key
|
|
196
352
|
expect(:colon)
|
|
197
353
|
pairs << KeyValuePair.new(key: key, value: parse_expr)
|
|
198
354
|
end
|
|
199
355
|
break unless at?(:comma)
|
|
356
|
+
|
|
200
357
|
advance
|
|
201
358
|
end
|
|
202
359
|
expect(:rbrace)
|
|
@@ -221,12 +378,11 @@ module Fusion
|
|
|
221
378
|
expect(:arrow)
|
|
222
379
|
body = parse_expr
|
|
223
380
|
clauses << Clause.new(pattern: pat, body: body)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
end
|
|
381
|
+
|
|
382
|
+
break if !at?(:comma)
|
|
383
|
+
|
|
384
|
+
advance
|
|
385
|
+
break if at?(:rparen) # trailing comma
|
|
230
386
|
end
|
|
231
387
|
expect(:rparen)
|
|
232
388
|
Expression::FuncLit.new(clauses: clauses)
|
|
@@ -247,10 +403,11 @@ module Fusion
|
|
|
247
403
|
case t.type
|
|
248
404
|
when :lparen, :lbracket, :lbrace then depth += 1
|
|
249
405
|
when :rparen, :rbracket, :rbrace
|
|
250
|
-
return false if depth
|
|
406
|
+
return false if depth == 0 # hit our closing ) first
|
|
407
|
+
|
|
251
408
|
depth -= 1
|
|
252
409
|
when :arrow
|
|
253
|
-
return true if depth
|
|
410
|
+
return true if depth == 0
|
|
254
411
|
when :eof
|
|
255
412
|
return false
|
|
256
413
|
end
|
|
@@ -275,8 +432,7 @@ module Fusion
|
|
|
275
432
|
|
|
276
433
|
# Tokens that can begin a `guardedpat` (used to detect whether `!` is
|
|
277
434
|
# followed by a payload pattern or stands alone).
|
|
278
|
-
GUARDEDPAT_STARTERS =
|
|
279
|
-
lbracket lbrace ident].freeze
|
|
435
|
+
GUARDEDPAT_STARTERS = [:number, :string, :true_kw, :false_kw, :null_kw, :minus, :lbracket, :lbrace, :ident].freeze
|
|
280
436
|
|
|
281
437
|
def parse_errpat
|
|
282
438
|
expect(:bang)
|
|
@@ -303,8 +459,12 @@ module Fusion
|
|
|
303
459
|
def parse_corepat
|
|
304
460
|
t = peek
|
|
305
461
|
case t.type
|
|
306
|
-
when :number, :string
|
|
307
|
-
|
|
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
|
|
308
468
|
when :lbracket then parse_arraypat
|
|
309
469
|
when :lbrace then parse_objectpat
|
|
310
470
|
when :ident
|
|
@@ -335,12 +495,14 @@ module Fusion
|
|
|
335
495
|
advance
|
|
336
496
|
break if at?(:rbracket) # trailing comma
|
|
337
497
|
raise ParseError, "a pattern may contain at most one `...rest` (at #{peek.pos})" if at?(:spread)
|
|
498
|
+
|
|
338
499
|
items << PatternItem.new(pattern: parse_guardedpat)
|
|
339
500
|
end
|
|
340
501
|
break
|
|
341
502
|
end
|
|
342
503
|
items << PatternItem.new(pattern: parse_guardedpat)
|
|
343
504
|
break unless at?(:comma)
|
|
505
|
+
|
|
344
506
|
advance
|
|
345
507
|
end
|
|
346
508
|
expect(:rbracket)
|
|
@@ -361,14 +523,17 @@ module Fusion
|
|
|
361
523
|
unless at?(:rbrace)
|
|
362
524
|
raise ParseError, "in an object pattern, `...rest` must come last (at #{peek.pos})"
|
|
363
525
|
end
|
|
526
|
+
|
|
364
527
|
break
|
|
365
528
|
end
|
|
366
529
|
key_pos = peek.pos
|
|
367
530
|
pair = parse_pattern_pair
|
|
368
531
|
raise ParseError, "duplicate key #{pair.key.inspect} (at #{key_pos})" if keys.include?(pair.key)
|
|
532
|
+
|
|
369
533
|
keys << pair.key
|
|
370
534
|
pairs << pair
|
|
371
535
|
break unless at?(:comma)
|
|
536
|
+
|
|
372
537
|
advance
|
|
373
538
|
end
|
|
374
539
|
expect(:rbrace)
|
|
@@ -392,12 +557,22 @@ module Fusion
|
|
|
392
557
|
end
|
|
393
558
|
|
|
394
559
|
# ---- token helpers ----
|
|
395
|
-
def peek(o = 0)
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
|
|
398
572
|
def expect(type)
|
|
399
573
|
t = peek
|
|
400
574
|
raise ParseError, "Expected #{type} but got #{t.type} (#{t.value.inspect}) at #{t.pos}" unless t.type == type
|
|
575
|
+
|
|
401
576
|
advance
|
|
402
577
|
end
|
|
403
578
|
end
|
data/lib/fusion/version.rb
CHANGED
data/lib/fusion.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\": _ ? @Collection}"]},
|
|
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
|
+
_ ? @truthy => true,
|
|
8
|
+
_ => {"f": f, "c": rest} | @,
|
|
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\": _ ? @Collection}"]},
|
|
12
|
+
)
|
data/stdlib/chars.fsn
ADDED
data/stdlib/compact.fsn
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Drop null entries from an array (by element) or an object (by value). Built on @filter.
|
|
2
|
+
(
|
|
3
|
+
c ? @Collection => c |? (null => false, _ => true),
|
|
4
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@compact", "status": 0, "input": x, "expected": ["_ ? @Collection"]}
|
|
5
|
+
)
|
data/stdlib/concat.fsn
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Concatenate an array of strings, of any length. Built on @join with an empty
|
|
2
|
+
# separator.
|
|
3
|
+
(
|
|
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)"]}
|
|
6
|
+
)
|
data/stdlib/entries.fsn
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Destructure an object into an array of its [key, value] entries, in insertion
|
|
2
|
+
# order. The inverse of @toObject.
|
|
3
|
+
(
|
|
4
|
+
obj ? @Object => obj | @keys |: (k => [k, obj[k]]),
|
|
5
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@entries", "status": 0, "input": x, "expected": ["_ ? @Object"]}
|
|
6
|
+
)
|
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
|
+
# Reduce to array case
|
|
10
|
+
{"f": f ? @Function, "c": obj ? @Object} => {"f": ([_, v] => v | f), "c": obj | @entries} | @ | @toObject,
|
|
11
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@filter", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Collection}"]}
|
|
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/map.fsn
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
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
|
-
|
|
5
|
-
|
|
4
|
+
{"f": _ ? @Function, "c": []} => [],
|
|
5
|
+
{"f": f ? @Function, "c": [x, ...rest]} => [x | f, ...({"f": f, "c": rest} | @)],
|
|
6
|
+
# Reduce to array case
|
|
7
|
+
{"f": f ? @Function, "c": obj ? @Object} => {"f": ([k, v] => [k, v | f]), "c": obj | @entries} | @ | @toObject,
|
|
8
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@map", "status": 0, "input": x, "expected": ["{\"f\": _ ? @Function, \"c\": _ ? @Collection}"]}
|
|
6
9
|
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# True for a matrix: a non-empty array of equally sized vectors (@vector/Vector).
|
|
2
|
+
# A predicate: false for anything else, never an error.
|
|
3
|
+
(rows => [
|
|
4
|
+
rows | @Array,
|
|
5
|
+
rows | @size > 0,
|
|
6
|
+
{"c": rows, "f": @vector/Vector} | @all,
|
|
7
|
+
rows |: @size | @OP.equal,
|
|
8
|
+
] | @OP.and | @safe)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# The operators reskinned for matrices. Point a directory's OP.fsn at
|
|
2
|
+
# `@matrix/OP` to make the arithmetic sugar operate on matrices: `a + b`
|
|
3
|
+
# (elementwise sum), `-a` / `a - b` (elementwise negation), `a * b` (matrix
|
|
4
|
+
# product), `/a` (inverse), `a / b` (multiply by the inverse). `//` and `%`
|
|
5
|
+
# have no matrix meaning and always raise. The spread keeps every other member
|
|
6
|
+
# (equal, compare and its readers, the boolean operators) at its builtin
|
|
7
|
+
# meaning — this module's own files rely on that: they use the comparison and
|
|
8
|
+
# boolean sugar and plain `@OP` references freely, and reserve `@@OP` for the
|
|
9
|
+
# scalar versions of the arithmetic members this file overrides.
|
|
10
|
+
{
|
|
11
|
+
...@@,
|
|
12
|
+
"sum": @sum,
|
|
13
|
+
"negate": @negate,
|
|
14
|
+
"product": @product,
|
|
15
|
+
"invert": @invert,
|
|
16
|
+
"quotient": (x => !{"kind": "math_error", "origin": "stdlib", "operation": "@matrix/OP.quotient", "status": 0, "input": x, "message": "integer division is not defined for matrices"}),
|
|
17
|
+
"modulo": (x => !{"kind": "math_error", "origin": "stdlib", "operation": "@matrix/OP.modulo", "status": 0, "input": x, "message": "modulo is not defined for matrices"}),
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Add two equally sized matrices elementwise: zip the rows and add each pair
|
|
2
|
+
# through @vector/add. Input: [a, b].
|
|
3
|
+
(
|
|
4
|
+
[a, b] ? ([x ? @Matrix, y ? @Matrix] => [x, y] |: @dimensions | @OP.equal) =>
|
|
5
|
+
[a, b] | @zip |: @vector/add,
|
|
6
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@matrix/add", "status": 0, "input": x, "expected": ["_ ? ([a ? @matrix/Matrix, b ? @matrix/Matrix] => [a, b] |: @matrix/dimensions | @OP.equal)"]}
|
|
7
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Extract the i-th (0-based) column of a matrix as a vector. Input: [matrix, index].
|
|
2
|
+
(
|
|
3
|
+
[m, i] ? ([x ? @Matrix, n ? @Integer] => n >= 0 && n < (x | @dimensions).columns) =>
|
|
4
|
+
m |: (row => row[i]),
|
|
5
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@matrix/column", "status": 0, "input": x, "expected": ["_ ? ([m ? @matrix/Matrix, i ? @Integer] => i >= 0 && i < (m | @matrix/dimensions).columns)"]}
|
|
6
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Determinant of a square matrix, by Laplace expansion along the first row: the
|
|
2
|
+
# dot product of the first row with its cofactors (signed minor determinants).
|
|
3
|
+
(
|
|
4
|
+
m ? (sq ? @Matrix => sq | @dimensions | (d => d.rows == d.columns)) =>
|
|
5
|
+
m | (
|
|
6
|
+
[[a]] => a,
|
|
7
|
+
[firstRow, ...] =>
|
|
8
|
+
[
|
|
9
|
+
firstRow,
|
|
10
|
+
(m | @dimensions).columns | @range |: (j =>
|
|
11
|
+
[[-1, j] | @math.pow, [m, 0, j] | @minor | @] | @@OP.product
|
|
12
|
+
),
|
|
13
|
+
] | @vector/dot
|
|
14
|
+
),
|
|
15
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@matrix/determinant", "status": 0, "input": x, "expected": ["_ ? (sq ? @matrix/Matrix => sq | @matrix/dimensions | (d => d.rows == d.columns))"]}
|
|
16
|
+
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# The dimensions of a matrix, as {"rows": _, "columns": _}.
|
|
2
|
+
(
|
|
3
|
+
m ? @Matrix => {"rows": m | @size, "columns": m[0] | @size},
|
|
4
|
+
x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@matrix/dimensions", "status": 0, "input": x, "expected": ["_ ? @matrix/Matrix"]}
|
|
5
|
+
)
|