Class: Fusion::Parser
- Inherits:
-
Object
- Object
- Fusion::Parser
- 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
- #advance ⇒ Object
- #at?(type) ⇒ Boolean
- #expect(type) ⇒ Object
-
#initialize(tokens) ⇒ Parser
constructor
A new instance of Parser.
-
#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 “)”?.
- #parse_array ⇒ Object
- #parse_arraypat ⇒ Object
- #parse_corepat ⇒ Object
- #parse_errpat ⇒ Object
- #parse_expr ⇒ Object
- #parse_fileref ⇒ Object
-
#parse_function_or_group ⇒ Object
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.
- #parse_guardedpat ⇒ Object
- #parse_object ⇒ Object
- #parse_objectpat ⇒ Object
-
#parse_pattern ⇒ Object
—- 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.
- #parse_pipe ⇒ Object
- #parse_postfix ⇒ Object
-
#parse_prefix ⇒ Object
‘!` is a prefix operator that constructs an error from its operand.
- #parse_primary ⇒ Object
-
#peek(o = 0) ⇒ Object
—- token helpers —-.
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
Instance Method Details
#advance ⇒ Object
533 |
# File 'lib/fusion.rb', line 533 def advance = (@toks[@i].tap { @i += 1 }) |
#at?(type) ⇒ Boolean
532 |
# File 'lib/fusion.rb', line 532 def at?(type) = peek.type == type |
#expect(type) ⇒ Object
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 “)”?
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_array ⇒ Object
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_arraypat ⇒ Object
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_corepat ⇒ Object
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_errpat ⇒ Object
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_expr ⇒ Object
252 |
# File 'lib/fusion.rb', line 252 def parse_expr = parse_pipe |
#parse_fileref ⇒ Object
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. = !has_dotdot FileRef.new( ? :name : :path, parts.join("/")) end |
#parse_function_or_group ⇒ Object
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_guardedpat ⇒ Object
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_object ⇒ Object
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_objectpat ⇒ Object
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_pattern ⇒ Object
—- 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_pipe ⇒ Object
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_postfix ⇒ Object
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_prefix ⇒ Object
‘!` 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_primary ⇒ Object
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] |