Class: Fusion::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/fusion.rb

Overview

PARSER (recursive descent following the EBNF)

Constant Summary collapse

PRIMARY_STARTERS =

Tokens that can begin a primary expression (used by parse_prefix to decide whether ‘!` is followed by an operand).

%i[number string true_kw false_kw null_kw bang
lbracket lbrace lparen ident at].freeze
GUARDEDPAT_STARTERS =

Tokens that can begin a ‘guardedpat` (used to detect whether `!` is followed by a payload pattern or stands alone).

%i[number string true_kw false_kw null_kw
lbracket lbrace ident].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tokens) ⇒ Parser

Returns a new instance of Parser.



239
240
241
242
# File 'lib/fusion.rb', line 239

def initialize(tokens)
  @toks = tokens
  @i = 0
end

Class Method Details

.parse_file(src) ⇒ Object



244
245
246
247
248
249
250
# File 'lib/fusion.rb', line 244

def self.parse_file(src)
  toks = Lexer.new(src).tokens
  p = new(toks)
  expr = p.parse_expr
  p.expect(:eof)
  expr
end

Instance Method Details

#advanceObject



533
# File 'lib/fusion.rb', line 533

def advance = (@toks[@i].tap { @i += 1 })

#at?(type) ⇒ Boolean

Returns:

  • (Boolean)


532
# File 'lib/fusion.rb', line 532

def at?(type) = peek.type == type

#expect(type) ⇒ Object

Raises:



534
535
536
537
538
# File 'lib/fusion.rb', line 534

def expect(type)
  t = peek
  raise ParseError, "Expected #{type} but got #{t.type} (#{t.value.inspect}) at #{t.pos}" unless t.type == type
  advance
end

#looks_like_function?Boolean

Look ahead from current position (just after “(”) to decide if this is a function literal: is there a top-level “=>” before the matching “)”?

Returns:

  • (Boolean)


412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/fusion.rb', line 412

def looks_like_function?
  depth = 0
  j = @i
  while j < @toks.length
    t = @toks[j]
    case t.type
    when :lparen, :lbracket, :lbrace then depth += 1
    when :rparen, :rbracket, :rbrace
      return false if depth.zero? # hit our closing ) first
      depth -= 1
    when :arrow
      return true if depth.zero?
    when :eof
      return false
    end
    j += 1
  end
  false
end

#parse_arrayObject



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/fusion.rb', line 345

def parse_array
  expect(:lbracket)
  elems = []
  until at?(:rbracket)
    if at?(:spread)
      advance
      elems << [:spread, parse_expr]
    else
      elems << [:item, parse_expr]
    end
    break unless at?(:comma)
    advance
  end
  expect(:rbracket)
  ArrLit.new(elems)
end

#parse_arraypatObject



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/fusion.rb', line 490

def parse_arraypat
  # Array elements are `guardedpat`s — they cannot be error patterns.
  expect(:lbracket)
  elems = []
  until at?(:rbracket)
    if at?(:spread)
      advance
      name = at?(:ident) ? advance.value : nil
      elems << [:rest, name]
    else
      elems << [:pat, parse_guardedpat]
    end
    break unless at?(:comma)
    advance
  end
  expect(:rbracket)
  PArr.new(elems)
end

#parse_corepatObject



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/fusion.rb', line 471

def parse_corepat
  t = peek
  case t.type
  when :number, :string then advance; PLit.new(t.value)
  when :true_kw, :false_kw, :null_kw then advance; PLit.new(t.value)
  when :lbracket then parse_arraypat
  when :lbrace then parse_objectpat
  when :ident
    advance
    t.value == "_" ? PWild.new(nil) : PBind.new(t.value)
  when :bang
    # `!pat` is only valid as a clause's top-level pattern, never inside an
    # array element, object member, or error payload.
    raise ParseError, "`!pat` may only appear as a clause's top-level pattern (at #{t.pos})"
  else
    raise ParseError, "Unexpected token in pattern: #{t.type} at #{t.pos}"
  end
end

#parse_errpatObject



451
452
453
454
455
456
457
458
# File 'lib/fusion.rb', line 451

def parse_errpat
  expect(:bang)
  if GUARDEDPAT_STARTERS.include?(peek.type)
    PErr.new(parse_guardedpat)            # "!" guardedpat
  else
    PErr.new(PWild.new(nil))               # bare "!" — matches any error, binds nothing
  end
end

#parse_exprObject



252
# File 'lib/fusion.rb', line 252

def parse_expr = parse_pipe

#parse_filerefObject



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/fusion.rb', line 318

def parse_fileref
  expect(:at)
  # Bare "@" = current file: not followed by something that can begin a path.
  nxt = peek
  starts_path = (nxt.type == :ident) || (nxt.type == :dot && peek(1)&.type == :dot)
  return FileRef.new(:self, nil) unless starts_path
  # refpath: { "../" } segment { "/" segment }
  parts = []
  has_dotdot = false
  while at?(:dot) && peek(1)&.type == :dot
    advance; advance # consume the two dots of ..
    parts << ".."
    expect(:slash)
    has_dotdot = true
  end
  parts << expect(:ident).value
  while at?(:slash)
    advance
    parts << expect(:ident).value
  end
  # A reference is eligible for builtin/stdlib fallback (:name) iff it does NOT
  # contain "../". Downward paths like "dir/a" are still eligible; only "../"
  # (escaping upward) forces pure file-path (:path) resolution.
  bare = !has_dotdot
  FileRef.new(bare ? :name : :path, parts.join("/"))
end

#parse_function_or_groupObject

A “(” can begin either a grouped expression or a function literal. Distinguish by trying to parse a clause: a function is a comma-separated list of ‘pattern => expr`. We detect a function by scanning for `=>` before the matching `)` at depth 0.



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/fusion.rb', line 385

def parse_function_or_group
  expect(:lparen)
  if looks_like_function?
    clauses = []
    loop do
      pat = parse_pattern
      expect(:arrow)
      body = parse_expr
      clauses << [pat, body]
      if at?(:comma)
        advance
        break if at?(:rparen) # trailing comma
      else
        break
      end
    end
    expect(:rparen)
    FuncLit.new(clauses)
  else
    e = parse_expr
    expect(:rparen)
    e
  end
end

#parse_guardedpatObject



460
461
462
463
464
465
466
467
468
469
# File 'lib/fusion.rb', line 460

def parse_guardedpat
  inner = parse_corepat
  if at?(:question)
    advance
    pred = parse_prefix
    PGuard.new(inner, pred)
  else
    inner
  end
end

#parse_objectObject



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/fusion.rb', line 362

def parse_object
  expect(:lbrace)
  members = []
  until at?(:rbrace)
    if at?(:spread)
      advance
      members << [:spread, parse_expr]
    else
      key = expect(:string).value
      expect(:colon)
      members << [:kv, key, parse_expr]
    end
    break unless at?(:comma)
    advance
  end
  expect(:rbrace)
  ObjLit.new(members)
end

#parse_objectpatObject



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/fusion.rb', line 509

def parse_objectpat
  # Object members are `guardedpat`s — they cannot be error patterns.
  expect(:lbrace)
  members = []
  until at?(:rbrace)
    if at?(:spread)
      advance
      name = at?(:ident) ? advance.value : nil
      members << [:rest, name]
    else
      key = expect(:string).value
      expect(:colon)
      members << [:kv, key, parse_guardedpat]
    end
    break unless at?(:comma)
    advance
  end
  expect(:rbrace)
  PObj.new(members)
end

#parse_patternObject

—- Patterns —- —- Pattern grammar (mirrors reference.md §2.5 EBNF) ——————

pattern    = errpat | guardedpat
errpat     = "!" | "!" guardedpat
guardedpat = corepat [ "?" predicate ]
corepat    = literalpat | bindpat | wildcard | arraypat | objectpat

Note: ‘corepat` does NOT include errpat. The “no nested !pat” property falls out of the grammar shape — `errpat` is only reachable from `pattern` (a clause’s top level), never from inside arrays, objects, or another error’s payload. No flag-threading is needed.



442
443
444
# File 'lib/fusion.rb', line 442

def parse_pattern
  at?(:bang) ? parse_errpat : parse_guardedpat
end

#parse_pipeObject



254
255
256
257
258
259
260
261
262
# File 'lib/fusion.rb', line 254

def parse_pipe
  left = parse_prefix
  while at?(:pipe)
    advance
    right = parse_prefix
    left = Pipe.new(left, right)
  end
  left
end

#parse_postfixObject



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/fusion.rb', line 285

def parse_postfix
  node = parse_primary
  loop do
    if at?(:dot)
      advance
      key = expect(:ident).value
      node = Member.new(node, key)
    elsif at?(:lbracket)
      advance
      idx = parse_expr
      expect(:rbracket)
      node = Index.new(node, idx)
    else
      break
    end
  end
  node
end

#parse_prefixObject

‘!` is a prefix operator that constructs an error from its operand. A bare `!` (no operand follows) is shorthand for `!null`. Binds tighter than `|` so `!x | f` is `(!x) | f`; looser than postfix so `!x.foo` is `!(x.foo)`.



272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/fusion.rb', line 272

def parse_prefix
  if at?(:bang)
    advance
    if PRIMARY_STARTERS.include?(peek.type)
      ErrLit.new(parse_prefix)            # allow !!x to nest
    else
      ErrLit.new(nil)                     # bare ! -> !null
    end
  else
    parse_postfix
  end
end

#parse_primaryObject



304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/fusion.rb', line 304

def parse_primary
  t = peek
  case t.type
  when :number, :string then advance; Lit.new(t.value)
  when :true_kw, :false_kw, :null_kw then advance; Lit.new(t.value)
  when :lbracket then parse_array
  when :lbrace then parse_object
  when :lparen then parse_function_or_group
  when :ident then advance; Ident.new(t.value)
  when :at then parse_fileref
  else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}"
  end
end

#peek(o = 0) ⇒ Object

—- token helpers —-



531
# File 'lib/fusion.rb', line 531

def peek(o = 0) = @toks[@i + o]