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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +24 -0
  3. data/.simplecov +11 -0
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +11 -9
  6. data/Rakefile +8 -0
  7. data/docs/lang/design.md +289 -51
  8. data/docs/lang/implementation.md +279 -0
  9. data/docs/lang/roadmap.md +20 -36
  10. data/docs/user/explanation.md +5 -10
  11. data/docs/user/how-to-guides.md +145 -15
  12. data/docs/user/reference.md +365 -140
  13. data/docs/user/tutorial.md +22 -19
  14. data/examples/double.fsn +4 -1
  15. data/examples/factorial.fsn +6 -3
  16. data/examples/fizzbuzz.fsn +1 -4
  17. data/examples/gcd.fsn +9 -0
  18. data/examples/json_test.fsn +4 -0
  19. data/examples/matrix/OP.fsn +2 -0
  20. data/examples/matrix/average.fsn +2 -0
  21. data/examples/matrix/solve.fsn +2 -0
  22. data/examples/palindrome.fsn +1 -1
  23. data/exe/fusion +12 -12
  24. data/lib/fusion/ast.rb +76 -27
  25. data/lib/fusion/atom.rb +1 -1
  26. data/lib/fusion/cli/decoder.rb +13 -8
  27. data/lib/fusion/cli/encoder.rb +2 -2
  28. data/lib/fusion/cli/options.rb +134 -61
  29. data/lib/fusion/cli/parser.rb +4 -4
  30. data/lib/fusion/cli/repl.rb +32 -27
  31. data/lib/fusion/cli/serializer.rb +11 -11
  32. data/lib/fusion/cli.rb +120 -49
  33. data/lib/fusion/interpreter/builtins.rb +298 -160
  34. data/lib/fusion/interpreter/env.rb +42 -12
  35. data/lib/fusion/interpreter/error_val.rb +42 -20
  36. data/lib/fusion/interpreter/thunk.rb +53 -0
  37. data/lib/fusion/interpreter.rb +263 -98
  38. data/lib/fusion/lexer.rb +125 -37
  39. data/lib/fusion/parser.rb +245 -70
  40. data/lib/fusion/version.rb +3 -1
  41. data/lib/fusion.rb +0 -1
  42. data/stdlib/all.fsn +13 -0
  43. data/stdlib/any.fsn +12 -0
  44. data/stdlib/chars.fsn +5 -0
  45. data/stdlib/compact.fsn +5 -0
  46. data/stdlib/concat.fsn +6 -0
  47. data/stdlib/entries.fsn +6 -0
  48. data/stdlib/falsey.fsn +6 -0
  49. data/stdlib/filter.fsn +12 -0
  50. data/stdlib/flatten.fsn +7 -0
  51. data/stdlib/map.fsn +7 -4
  52. data/stdlib/matrix/Matrix.fsn +8 -0
  53. data/stdlib/matrix/OP.fsn +18 -0
  54. data/stdlib/matrix/add.fsn +7 -0
  55. data/stdlib/matrix/column.fsn +6 -0
  56. data/stdlib/matrix/determinant.fsn +16 -0
  57. data/stdlib/matrix/dimensions.fsn +5 -0
  58. data/stdlib/matrix/identity.fsn +6 -0
  59. data/stdlib/matrix/invert.fsn +26 -0
  60. data/stdlib/matrix/minor.fsn +15 -0
  61. data/stdlib/matrix/multiply.fsn +10 -0
  62. data/stdlib/matrix/negate.fsn +5 -0
  63. data/stdlib/matrix/product.fsn +11 -0
  64. data/stdlib/matrix/rotate.fsn +10 -0
  65. data/stdlib/matrix/row.fsn +6 -0
  66. data/stdlib/matrix/scale.fsn +6 -0
  67. data/stdlib/matrix/subtract.fsn +7 -0
  68. data/stdlib/matrix/sum.fsn +14 -0
  69. data/stdlib/matrix/transpose.fsn +5 -0
  70. data/stdlib/range.fsn +2 -2
  71. data/stdlib/reduce.fsn +8 -0
  72. data/stdlib/safe.fsn +7 -0
  73. data/stdlib/sanitize.fsn +2 -3
  74. data/stdlib/toObject.fsn +7 -0
  75. data/stdlib/truthy.fsn +7 -0
  76. data/stdlib/vector/Vector.fsn +7 -0
  77. data/stdlib/vector/add.fsn +6 -0
  78. data/stdlib/vector/cross.fsn +6 -0
  79. data/stdlib/vector/dot.fsn +6 -0
  80. data/stdlib/vector/norm.fsn +6 -0
  81. data/stdlib/vector/scale.fsn +5 -0
  82. data/stdlib/vector/subtract.fsn +6 -0
  83. data/stdlib/zip.fsn +6 -0
  84. metadata +50 -4
  85. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  86. data/stdlib/mapValues.fsn +0 -5
  87. 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. `location` is the
26
- # syntax_error's "code X" / "code <inline>" context.
27
- def self.parse_file(src, location:)
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 => err
34
- Interpreter::ErrorVal.internal(kind: "syntax_error", location: location, operation: "parsing", input: src, message: err.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
@@ -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, location:)
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 => err
49
- Interpreter::ErrorVal.internal(kind: "syntax_error", location: location, operation: "parsing", input: src, message: err.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.
@@ -67,35 +67,149 @@ module Fusion
67
67
  end
68
68
 
69
69
  def parse_expr
70
- parse_pipe
70
+ parse_or
71
71
  end
72
72
 
73
- def parse_pipe
74
- left = parse_prefix
75
- while at?(:pipe)
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
- right = parse_prefix
78
- left = Expression::Pipe.new(left: left, right: right)
83
+ operands << parse_and
79
84
  end
80
- left
85
+ fold_operator(operands, "or")
81
86
  end
82
87
 
83
- # Tokens that can begin a primary expression (used by parse_prefix to decide
84
- # whether `!` is followed by an operand).
85
- PRIMARY_STARTERS = %i[number string true_kw false_kw null_kw bang
86
- lbracket lbrace lparen ident at].freeze
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
- # `!` is a prefix operator that constructs an error from its operand. A bare
89
- # `!` (no operand follows) is shorthand for `!null`. Binds tighter than `|`
90
- # so `!x | f` is `(!x) | f`; looser than postfix so `!x.foo` is `!(x.foo)`.
91
- def parse_prefix
92
- if at?(:bang)
97
+ def parse_equality
98
+ operands = [parse_ordering]
99
+ while at?(:eqeq)
93
100
  advance
94
- if PRIMARY_STARTERS.include?(peek.type)
95
- Expression::ErrLit.new(payload: parse_prefix) # allow !!x to nest
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
- Expression::ErrLit.new(payload: nil) # bare ! -> !null
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
- expect(:rbracket)
115
- node = Expression::Index.new(obj: node, idx: idx)
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 then advance; Expression::Lit.new(value: t.value)
127
- when :true_kw, :false_kw, :null_kw then advance; Expression::Lit.new(value: t.value)
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 then advance; Expression::Ident.new(name: t.value)
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
- # Bare "@" = current file: not followed by something that can begin a path.
140
- nxt = peek
141
- starts_path = (nxt.type == :ident) || (nxt.type == :dot && peek(1)&.type == :dot)
142
- return Expression::FileRef.new(variety: :self, path: nil) unless starts_path
143
- # refpath: { "../" } segment { "/" segment }
144
- parts = []
145
- has_dotdot = false
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
- if at?(:comma)
225
- advance
226
- break if at?(:rparen) # trailing comma
227
- else
228
- break
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.zero? # hit our closing ) first
406
+ return false if depth == 0 # hit our closing ) first
407
+
251
408
  depth -= 1
252
409
  when :arrow
253
- return true if depth.zero?
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 = %i[number string true_kw false_kw null_kw
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 then advance; Pattern::PLit.new(value: t.value)
307
- when :true_kw, :false_kw, :null_kw then advance; Pattern::PLit.new(value: t.value)
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) = @toks[@i + o]
396
- def at?(type) = peek.type == type
397
- def advance = (@toks[@i].tap { @i += 1 })
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fusion
2
- VERSION = "0.0.1.alpha2"
4
+ VERSION = "0.0.2"
3
5
  end
data/lib/fusion.rb CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env ruby
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # Fusion — a proof-of-concept interpreter.
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
@@ -0,0 +1,5 @@
1
+ # Split a string into its characters. Built on @split with an empty separator.
2
+ (
3
+ s ? @String => [s, ""] | @split,
4
+ x => !{"kind": "argument_error", "origin": "stdlib", "operation": "@chars", "status": 0, "input": x, "expected": ["_ ? @String"]}
5
+ )
@@ -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
+ )
@@ -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
@@ -0,0 +1,6 @@
1
+ # True for false and null — Fusion's own falsiness, decided by pattern matching.
2
+ (
3
+ false => true,
4
+ null => true,
5
+ _ => false,
6
+ )
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
+ )
@@ -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": _, "xs": []} => [],
3
- {"f": f, "xs": [x, ...rest]} => [x | f, ...({"f": f, "xs": rest} | @map)],
4
- {"f": _, "xs": xs} => !{"kind": "type_error", "location": "stdlib map.fsn", "operation": "map", "input": xs | @sanitize, "message": "expected an array"},
5
- x => !{"kind": "argument_error", "location": "stdlib map.fsn", "operation": "map", "input": x | @sanitize, "message": "expected {\"f\": _, \"xs\": _}"}
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
+ )