fusion-lang 0.0.1.alpha1 → 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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -6
  3. data/Rakefile +9 -0
  4. data/docs/lang/design.md +418 -28
  5. data/docs/lang/implementation.md +238 -0
  6. data/docs/lang/roadmap.md +20 -57
  7. data/docs/user/explanation.md +5 -10
  8. data/docs/user/how-to-guides.md +62 -23
  9. data/docs/user/reference.md +596 -168
  10. data/docs/user/tutorial.md +32 -29
  11. data/examples/double.fsn +1 -1
  12. data/examples/ends.fsn +4 -0
  13. data/examples/factorial.fsn +2 -2
  14. data/examples/fizzbuzz.fsn +1 -4
  15. data/examples/json_test.fsn +4 -0
  16. data/examples/palindrome.fsn +2 -1
  17. data/exe/fusion +17 -44
  18. data/lib/fusion/ast.rb +97 -0
  19. data/lib/fusion/atom.rb +17 -0
  20. data/lib/fusion/cli/decoder.rb +84 -0
  21. data/lib/fusion/cli/encoder.rb +28 -0
  22. data/lib/fusion/cli/options.rb +212 -0
  23. data/lib/fusion/cli/parser.rb +38 -0
  24. data/lib/fusion/cli/repl.rb +78 -0
  25. data/lib/fusion/cli/serializer.rb +70 -0
  26. data/lib/fusion/cli.rb +207 -0
  27. data/lib/fusion/interpreter/builtins.rb +465 -0
  28. data/lib/fusion/interpreter/env.rb +89 -0
  29. data/lib/fusion/interpreter/error_val.rb +71 -0
  30. data/lib/fusion/interpreter/func.rb +22 -0
  31. data/lib/fusion/interpreter/native_func.rb +22 -0
  32. data/lib/fusion/interpreter/thunk.rb +53 -0
  33. data/lib/fusion/interpreter.rb +752 -0
  34. data/lib/fusion/lexer.rb +249 -0
  35. data/lib/fusion/null.rb +9 -0
  36. data/lib/fusion/parser.rb +542 -0
  37. data/lib/fusion/token.rb +22 -0
  38. data/lib/fusion/typed_data.rb +23 -0
  39. data/lib/fusion/version.rb +1 -1
  40. data/lib/fusion/wire_pair.rb +11 -0
  41. data/lib/fusion.rb +11 -1122
  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 +6 -0
  46. data/stdlib/concat.fsn +5 -0
  47. data/stdlib/falsey.fsn +6 -0
  48. data/stdlib/filter.fsn +12 -0
  49. data/stdlib/flatten.fsn +7 -0
  50. data/stdlib/gt.fsn +9 -0
  51. data/stdlib/gte.fsn +9 -0
  52. data/stdlib/lt.fsn +9 -0
  53. data/stdlib/lte.fsn +9 -0
  54. data/stdlib/map.fsn +6 -2
  55. data/stdlib/range.fsn +2 -1
  56. data/stdlib/reduce.fsn +8 -0
  57. data/stdlib/sanitize.fsn +12 -0
  58. data/stdlib/truthy.fsn +7 -0
  59. metadata +41 -2
  60. data/stdlib/math/square.fsn +0 -1
@@ -0,0 +1,542 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Transformation ===
4
+ #
5
+ # Recursive descent parser following the EBNF
6
+ #
7
+ # Input: Array<Token>
8
+ # Output: AST::Expression
9
+
10
+ require_relative "token"
11
+ require_relative "ast"
12
+ require_relative "interpreter/error_val"
13
+
14
+ module Fusion
15
+ class Parser
16
+ include AST
17
+
18
+ def initialize(tokens)
19
+ @toks = tokens
20
+ @i = 0
21
+ end
22
+
23
+ # Parse a complete program. The lexer and parser report failures by raising
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. `site` is the
26
+ # syntax_error's `{origin:, file:}` context.
27
+ def self.parse_file(src, site:)
28
+ toks = Lexer.new(src).tokens
29
+ p = new(toks)
30
+ expr = p.parse_expr
31
+ p.expect(:eof)
32
+ expr
33
+ rescue ParseError => err
34
+ Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.message)
35
+ end
36
+
37
+ # Parse one REPL entry — a statement (`identifier "=" expr`) or a bare
38
+ # expression — returning an AST::Statement::Assignment / AST::Expression, or, like
39
+ # parse_file, a standardized syntax_error value instead of ever raising. The
40
+ # REPL uses the error/non-error distinction to tell "keep editing" (didn't
41
+ # parse yet) from "evaluate now" (a complete statement or expression).
42
+ def self.parse_repl(src, site:)
43
+ toks = Lexer.new(src).tokens
44
+ p = new(toks)
45
+ entry = p.parse_repl_entry
46
+ p.expect(:eof)
47
+ entry
48
+ rescue ParseError => err
49
+ Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.message)
50
+ end
51
+
52
+ # A leading `identifier =` marks a statement; anything else is an expression.
53
+ # (A bare identifier is itself a valid expression, so the `=` is the decider.)
54
+ def parse_repl_entry
55
+ if at?(:ident) && peek(1)&.type == :equals
56
+ parse_statement
57
+ else
58
+ parse_expr
59
+ end
60
+ end
61
+
62
+ # statement = identifier "=" expr (REPL only; files contain one expr)
63
+ def parse_statement
64
+ name = expect(:ident).value
65
+ expect(:equals)
66
+ AST::Statement::Assignment.new(name: name, expression: parse_expr)
67
+ end
68
+
69
+ def parse_expr
70
+ parse_or
71
+ end
72
+
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)
82
+ advance
83
+ operands << parse_and
84
+ end
85
+ fold_operator(operands, "or")
86
+ end
87
+
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
96
+
97
+ def parse_equality
98
+ operands = [parse_ordering]
99
+ while at?(:eqeq)
100
+ advance
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))
173
+ else
174
+ break
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")
200
+ else
201
+ parse_postfix
202
+ end
203
+ end
204
+
205
+ def parse_postfix
206
+ node = parse_primary
207
+ loop do
208
+ if at?(:dot)
209
+ advance
210
+ key = expect(:ident).value
211
+ node = Expression::Member.new(obj: node, key: key)
212
+ elsif at?(:lbracket)
213
+ advance
214
+ idx = parse_expr
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
224
+ else
225
+ break
226
+ end
227
+ end
228
+ node
229
+ end
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
+
261
+ def parse_primary
262
+ t = peek
263
+ case t.type
264
+ when :number, :string then advance; Expression::Lit.new(value: t.value)
265
+ when :true_kw, :false_kw, :null_kw then advance; Expression::Lit.new(value: t.value)
266
+ when :lbracket then parse_array
267
+ when :lbrace then parse_object
268
+ when :lparen then parse_function_or_group
269
+ when :ident then advance; Expression::Ident.new(name: t.value)
270
+ when :at then parse_fileref
271
+ when :atat then parse_superref
272
+ else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}"
273
+ end
274
+ end
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
+
290
+ def parse_fileref
291
+ expect(:at)
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)
299
+ end
300
+
301
+ def parse_array
302
+ expect(:lbracket)
303
+ items = []
304
+ until at?(:rbracket)
305
+ if at?(:spread)
306
+ advance
307
+ items << ArraySpread.new(value: parse_expr)
308
+ else
309
+ items << ArrayItem.new(value: parse_expr)
310
+ end
311
+ break unless at?(:comma)
312
+ advance
313
+ end
314
+ expect(:rbracket)
315
+ Expression::ArrLit.new(items: items)
316
+ end
317
+
318
+ # Fixed keys must be distinct (the ObjLit data rule); a repeat is a clean
319
+ # syntax_error. Keys arriving via `...spread` are dynamic and not checked.
320
+ def parse_object
321
+ expect(:lbrace)
322
+ pairs = []
323
+ keys = []
324
+ until at?(:rbrace)
325
+ if at?(:spread)
326
+ advance
327
+ pairs << ObjectSpread.new(value: parse_expr)
328
+ else
329
+ key_tok = expect(:string)
330
+ key = key_tok.value
331
+ raise ParseError, "duplicate key #{key.inspect} (at #{key_tok.pos})" if keys.include?(key)
332
+ keys << key
333
+ expect(:colon)
334
+ pairs << KeyValuePair.new(key: key, value: parse_expr)
335
+ end
336
+ break unless at?(:comma)
337
+ advance
338
+ end
339
+ expect(:rbrace)
340
+ Expression::ObjLit.new(pairs: pairs)
341
+ end
342
+
343
+ # A "(" begins a grouped expression, a function literal, or — when empty —
344
+ # the clause-less function `()`. A function is a comma-separated list of
345
+ # `pattern => expr`; we detect one by scanning for a top-level `=>` before the
346
+ # matching `)`. `()` matches nothing (so it yields null for any normal input
347
+ # and propagates errors).
348
+ def parse_function_or_group
349
+ expect(:lparen)
350
+ if at?(:rparen)
351
+ advance
352
+ return Expression::FuncLit.new(clauses: [])
353
+ end
354
+ if looks_like_function?
355
+ clauses = []
356
+ loop do
357
+ pat = parse_pattern
358
+ expect(:arrow)
359
+ body = parse_expr
360
+ clauses << Clause.new(pattern: pat, body: body)
361
+ if at?(:comma)
362
+ advance
363
+ break if at?(:rparen) # trailing comma
364
+ else
365
+ break
366
+ end
367
+ end
368
+ expect(:rparen)
369
+ Expression::FuncLit.new(clauses: clauses)
370
+ else
371
+ e = parse_expr
372
+ expect(:rparen)
373
+ e
374
+ end
375
+ end
376
+
377
+ # Look ahead from current position (just after "(") to decide if this is a
378
+ # function literal: is there a top-level "=>" before the matching ")"?
379
+ def looks_like_function?
380
+ depth = 0
381
+ j = @i
382
+ while j < @toks.length
383
+ t = @toks[j]
384
+ case t.type
385
+ when :lparen, :lbracket, :lbrace then depth += 1
386
+ when :rparen, :rbracket, :rbrace
387
+ return false if depth.zero? # hit our closing ) first
388
+ depth -= 1
389
+ when :arrow
390
+ return true if depth.zero?
391
+ when :eof
392
+ return false
393
+ end
394
+ j += 1
395
+ end
396
+ false
397
+ end
398
+
399
+ # ---- Patterns ----
400
+ # ---- Pattern grammar (mirrors reference.md §2.5 EBNF) ------------------
401
+ # pattern = p_error | p_guarded
402
+ # p_error = "!" | "!" p_guarded
403
+ # p_guarded = p_core [ "?" predicate ]
404
+ # p_core = p_literal | p_bind | p_wildcard | p_array | p_object
405
+ # Note: `p_core` does NOT include p_error. The "no nested !pat" property
406
+ # falls out of the grammar shape — `p_error` is only reachable from `pattern`
407
+ # (a clause's top level), never from inside arrays, objects, or another
408
+ # error's payload. No flag-threading is needed.
409
+ def parse_pattern
410
+ at?(:bang) ? parse_errpat : parse_guardedpat
411
+ end
412
+
413
+ # Tokens that can begin a `guardedpat` (used to detect whether `!` is
414
+ # followed by a payload pattern or stands alone).
415
+ GUARDEDPAT_STARTERS = %i[number string true_kw false_kw null_kw minus
416
+ lbracket lbrace ident].freeze
417
+
418
+ def parse_errpat
419
+ expect(:bang)
420
+ if GUARDEDPAT_STARTERS.include?(peek.type)
421
+ Pattern::PErr.new(inner: parse_guardedpat) # "!" guardedpat
422
+ else
423
+ Pattern::PErr.new(inner: Pattern::PWild.new(dummy: nil)) # bare "!" — matches any error, binds nothing
424
+ end
425
+ end
426
+
427
+ def parse_guardedpat
428
+ inner = parse_corepat
429
+ if at?(:question)
430
+ advance
431
+ # A predicate is a full pipe so it may chain functions: `a ? b | c` tests
432
+ # `a | b | c`. It stops at `=>`, `,`, `]`, `}`, `)` like any expression.
433
+ pred = parse_pipe
434
+ Pattern::PGuard.new(inner: inner, pred_expr: pred)
435
+ else
436
+ inner
437
+ end
438
+ end
439
+
440
+ def parse_corepat
441
+ t = peek
442
+ case t.type
443
+ when :number, :string then advance; Pattern::PLit.new(value: t.value)
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
446
+ when :lbracket then parse_arraypat
447
+ when :lbrace then parse_objectpat
448
+ when :ident
449
+ advance
450
+ t.value == "_" ? Pattern::PWild.new(dummy: nil) : Pattern::PBind.new(name: t.value)
451
+ when :bang
452
+ # `!pat` is only valid as a clause's top-level pattern, never inside an
453
+ # array element, object member, or error payload.
454
+ raise ParseError, "`!pat` may only appear as a clause's top-level pattern (at #{t.pos})"
455
+ else
456
+ raise ParseError, "Unexpected token in pattern: #{t.type} at #{t.pos}"
457
+ end
458
+ end
459
+
460
+ # p_array (reference.md §2.5). Items are `p_guarded`s — never error patterns.
461
+ # The grammar's two arms (with / without a rest) become two phases: the loop
462
+ # parses leading items up to an optional single `...rest`; once a rest is
463
+ # consumed, the inner loop parses trailing items only, so a second `...` lands
464
+ # in `parse_guardedpat` as an unexpected token. There is no `seen_rest` flag —
465
+ # "at most one rest" is enforced by the shape of the loop.
466
+ def parse_arraypat
467
+ expect(:lbracket)
468
+ items = []
469
+ until at?(:rbracket)
470
+ if at?(:spread)
471
+ items << parse_pattern_rest
472
+ while at?(:comma)
473
+ advance
474
+ break if at?(:rbracket) # trailing comma
475
+ raise ParseError, "a pattern may contain at most one `...rest` (at #{peek.pos})" if at?(:spread)
476
+ items << PatternItem.new(pattern: parse_guardedpat)
477
+ end
478
+ break
479
+ end
480
+ items << PatternItem.new(pattern: parse_guardedpat)
481
+ break unless at?(:comma)
482
+ advance
483
+ end
484
+ expect(:rbracket)
485
+ Pattern::PArr.new(items: items)
486
+ end
487
+
488
+ # p_object (reference.md §2.5). Leading pairs up to an optional single
489
+ # `...rest`, which must come last — only a trailing comma may follow it. Keys
490
+ # must be distinct (the PObj data rule); a repeat is a clean syntax_error.
491
+ def parse_objectpat
492
+ expect(:lbrace)
493
+ pairs = []
494
+ keys = []
495
+ until at?(:rbrace)
496
+ if at?(:spread)
497
+ pairs << parse_pattern_rest
498
+ advance if at?(:comma) && peek(1)&.type == :rbrace # trailing comma
499
+ unless at?(:rbrace)
500
+ raise ParseError, "in an object pattern, `...rest` must come last (at #{peek.pos})"
501
+ end
502
+ break
503
+ end
504
+ key_pos = peek.pos
505
+ pair = parse_pattern_pair
506
+ raise ParseError, "duplicate key #{pair.key.inspect} (at #{key_pos})" if keys.include?(pair.key)
507
+ keys << pair.key
508
+ pairs << pair
509
+ break unless at?(:comma)
510
+ advance
511
+ end
512
+ expect(:rbrace)
513
+ Pattern::PObj.new(pairs: pairs)
514
+ end
515
+
516
+ # p_rest = "..." [ identifier ] — the single rest binder, shared by array and
517
+ # object patterns. Callers parse it only at a rest position and then continue
518
+ # with items/pairs only, which is what holds a pattern to one rest.
519
+ def parse_pattern_rest
520
+ expect(:spread)
521
+ name = at?(:ident) ? advance.value : nil
522
+ PatternRest.new(name: name)
523
+ end
524
+
525
+ # p_pair = string ":" p_guarded
526
+ def parse_pattern_pair
527
+ key = expect(:string).value
528
+ expect(:colon)
529
+ PatternPair.new(key: key, pattern: parse_guardedpat)
530
+ end
531
+
532
+ # ---- token helpers ----
533
+ def peek(o = 0) = @toks[@i + o]
534
+ def at?(type) = peek.type == type
535
+ def advance = (@toks[@i].tap { @i += 1 })
536
+ def expect(type)
537
+ t = peek
538
+ raise ParseError, "Expected #{type} but got #{t.type} (#{t.value.inspect}) at #{t.pos}" unless t.type == type
539
+ advance
540
+ end
541
+ end
542
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Data Structure ===
4
+ #
5
+ # Array<Token> is
6
+ # - output of the lexer
7
+ # - input of the parser
8
+
9
+ require_relative "typed_data"
10
+ require_relative "atom"
11
+
12
+ module Fusion
13
+ # type: one of the lexer's token-type symbols (:number, :ident, :lparen, ...).
14
+ # value: the token's payload — a scalar for literals/keywords, the matched
15
+ # text for punctuation/identifiers, or nil for :eof.
16
+ # pos: the token's source offset.
17
+ Token = TypedData.define(
18
+ type: Symbol,
19
+ value: ->(v) { Atom === v || v.nil? },
20
+ pos: Integer,
21
+ )
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Utility ===
4
+ #
5
+ # Ruby's `Data` with `===` type per attribute. A type is anything `===`-able:
6
+ # a class (`Integer`), a regexp (`Identifier`), a marker module, or — for
7
+ # anything composite (unions, typed arrays, enums, "optional") — a `proc`.
8
+ # Mini-clone of gem `literal`.
9
+
10
+ module TypedData
11
+ def self.define(**schema)
12
+ Data.define(*schema.keys) do
13
+ define_method(:initialize) do |**kwargs|
14
+ kwargs.each_key do |key|
15
+ unless schema[key] === kwargs[key]
16
+ raise TypeError, "#{key}: expected #{schema[key]}, got #{kwargs[key].inspect} (#{kwargs[key].class})"
17
+ end
18
+ end
19
+ super(**kwargs)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module Fusion
2
- VERSION = "0.0.1.alpha1"
2
+ VERSION = "0.0.1"
3
3
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # === Value ===
4
+ #
5
+ # The combination of status code and value (JSON string)
6
+
7
+ require_relative "typed_data"
8
+
9
+ module Fusion
10
+ WirePair = TypedData.define(status: ->(v) { Integer === v && [0, 1].include?(v) }, data: String)
11
+ end