Class: Fusion::Parser
Constant Summary collapse
- PRIMARY_STARTERS =
Tokens that can begin a unary operand — used to decide whether
!has an operand or is the bare!null. %i[number string true_kw false_kw null_kw bang minus slash tilde lbracket lbrace lparen ident at atat].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 minus lbracket lbrace ident].freeze
Constants included from AST
AST::ArrayItem, AST::ArraySpread, AST::Clause, AST::Identifier, AST::KeyValuePair, AST::ObjectSpread, AST::PatternItem, AST::PatternPair, AST::PatternRest
Class Method Summary collapse
-
.parse_file(src, site:) ⇒ Object
Parse a complete program.
-
.parse_repl(src, site:) ⇒ Object
Parse one REPL entry — a statement (
identifier "=" expr) or a bare expression — returning an AST::Statement::Assignment / AST::Expression, or, like parse_file, a standardized syntax_error value instead of ever raising.
Instance Method Summary collapse
- #advance ⇒ Object
- #at?(type) ⇒ Boolean
- #expect(type) ⇒ Object
-
#fold_operator(operands, member) ⇒ Object
A single-operand run collapses to that operand; otherwise fold n-ary.
-
#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 ")"?.
-
#name_ref(path) ⇒ Object
--- Desugaring helpers ---------------------------------------------------.
-
#negate(expr) ⇒ Object
-x: a numeric literal folds to a negative literal; anything else negates. -
#op_member(member) ⇒ Object
@OP.member, a shadowable reference. -
#parse_additive ⇒ Object
A run of
+/-folds into one@OP.sum; each-term is negated. - #parse_and ⇒ Object
- #parse_array ⇒ Object
-
#parse_arraypat ⇒ Object
p_array (reference.md §2.5).
- #parse_corepat ⇒ Object
- #parse_equality ⇒ Object
- #parse_errpat ⇒ Object
- #parse_expr ⇒ Object
- #parse_fileref ⇒ Object
-
#parse_function_or_group ⇒ Object
A "(" begins a grouped expression, a function literal, or — when empty — the clause-less function
(). A function is a comma-separated list ofpattern => expr; we detect one by scanning for a top-level=>before the matching). - #parse_guardedpat ⇒ Object
-
#parse_multiplicative ⇒ Object
A run of
*//folds into one@OP.product(each/term inverted);%///are binary and break the run. -
#parse_object ⇒ Object
Fixed keys must be distinct (the ObjLit data rule); a repeat is a clean syntax_error.
-
#parse_objectpat ⇒ Object
p_object (reference.md §2.5).
-
#parse_or ⇒ Object
--- Operator sugar (reference §2.7) -------------------------------------- The ladder below desugars every operator to a pipe into an
@OP.*member (or, for the map-pipes, a stdlib call). -
#parse_ordering ⇒ Object
??(compare) is binary and left-associative — it does not fold. -
#parse_pattern ⇒ Object
---- Patterns ---- ---- Pattern grammar (mirrors reference.md §2.5 EBNF) ------------------ pattern = p_error | p_guarded p_error = "!" | "!" p_guarded p_guarded = p_core [ "?" predicate ] p_core = p_literal | p_bind | p_wildcard | p_array | p_object Note:
p_coredoes NOT include p_error. -
#parse_pattern_pair ⇒ Object
p_pair = string ":" p_guarded.
-
#parse_pattern_rest ⇒ Object
p_rest = "..." [ identifier ] — the single rest binder, shared by array and object patterns.
- #parse_pipe ⇒ Object
- #parse_postfix ⇒ Object
- #parse_primary ⇒ Object
-
#parse_repl_entry ⇒ Object
A leading
identifier =marks a statement; anything else is an expression. -
#parse_statement ⇒ Object
statement = identifier "=" expr (REPL only; files contain one expr).
-
#parse_superref ⇒ Object
@@is super. -
#parse_unary ⇒ Object
Unary prefixes:
!xbuilds an error (bare!is!null);-xnegates,/xinverts,~xis logical not. -
#peek(o = 0) ⇒ Object
---- token helpers ----.
-
#pipe_into(expr, member) ⇒ Object
expr | @OP.member. -
#pipe_operator(operands, member) ⇒ Object
[operands...] | @OP.member.
Constructor Details
#initialize(tokens) ⇒ Parser
Returns a new instance of Parser.
18 19 20 21 |
# File 'lib/fusion/parser.rb', line 18 def initialize(tokens) @toks = tokens @i = 0 end |
Class Method Details
.parse_file(src, site:) ⇒ Object
Parse a complete program. The lexer and parser report failures by raising
ParseError; this single entry point rescues them and returns a standardized
syntax_error value, so no caller ever sees a raw Ruby error. site is the
syntax_error's {origin:, file:} context.
27 28 29 30 31 32 33 34 35 |
# File 'lib/fusion/parser.rb', line 27 def self.parse_file(src, site:) toks = Lexer.new(src).tokens p = new(toks) expr = p.parse_expr p.expect(:eof) expr rescue ParseError => err Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.) end |
.parse_repl(src, site:) ⇒ Object
Parse one REPL entry — a statement (identifier "=" expr) or a bare
expression — returning an AST::Statement::Assignment / AST::Expression, or, like
parse_file, a standardized syntax_error value instead of ever raising. The
REPL uses the error/non-error distinction to tell "keep editing" (didn't
parse yet) from "evaluate now" (a complete statement or expression).
42 43 44 45 46 47 48 49 50 |
# File 'lib/fusion/parser.rb', line 42 def self.parse_repl(src, site:) toks = Lexer.new(src).tokens p = new(toks) entry = p.parse_repl_entry p.expect(:eof) entry rescue ParseError => err Interpreter::ErrorVal.from_runtime(kind: "syntax_error", **site, operation: "parsing code", input: src, message: err.) end |
Instance Method Details
#advance ⇒ Object
535 |
# File 'lib/fusion/parser.rb', line 535 def advance = (@toks[@i].tap { @i += 1 }) |
#at?(type) ⇒ Boolean
534 |
# File 'lib/fusion/parser.rb', line 534 def at?(type) = peek.type == type |
#expect(type) ⇒ Object
536 537 538 539 540 |
# File 'lib/fusion/parser.rb', line 536 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 |
#fold_operator(operands, member) ⇒ Object
A single-operand run collapses to that operand; otherwise fold n-ary.
248 249 250 |
# File 'lib/fusion/parser.rb', line 248 def fold_operator(operands, member) operands.length == 1 ? operands.first : pipe_operator(operands, member) 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 ")"?
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/fusion/parser.rb', line 379 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 |
#name_ref(path) ⇒ Object
--- Desugaring helpers ---------------------------------------------------
233 |
# File 'lib/fusion/parser.rb', line 233 def name_ref(path) = Expression::FileRef.new(variety: :name, path: path) |
#negate(expr) ⇒ Object
-x: a numeric literal folds to a negative literal; anything else negates.
253 254 255 256 257 258 259 |
# File 'lib/fusion/parser.rb', line 253 def negate(expr) if expr.is_a?(Expression::Lit) && expr.value.is_a?(Numeric) Expression::Lit.new(value: -expr.value) else pipe_into(expr, "negate") end end |
#op_member(member) ⇒ Object
@OP.member, a shadowable reference.
236 |
# File 'lib/fusion/parser.rb', line 236 def op_member(member) = Expression::Member.new(obj: name_ref("OP"), key: member) |
#parse_additive ⇒ Object
A run of +/- folds into one @OP.sum; each - term is negated.
117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/fusion/parser.rb', line 117 def parse_additive terms = [parse_multiplicative] folded = false while at?(:plus) || at?(:minus) negated = at?(:minus) advance term = parse_multiplicative terms << (negated ? negate(term) : term) folded = true end folded ? pipe_operator(terms, "sum") : terms.first end |
#parse_and ⇒ Object
88 89 90 91 92 93 94 95 |
# File 'lib/fusion/parser.rb', line 88 def parse_and operands = [parse_equality] while at?(:andand) advance operands << parse_equality end fold_operator(operands, "and") end |
#parse_array ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/fusion/parser.rb', line 301 def parse_array expect(:lbracket) items = [] until at?(:rbracket) if at?(:spread) advance items << ArraySpread.new(value: parse_expr) else items << ArrayItem.new(value: parse_expr) end break unless at?(:comma) advance end expect(:rbracket) Expression::ArrLit.new(items: items) end |
#parse_arraypat ⇒ Object
p_array (reference.md §2.5). Items are p_guardeds — never error patterns.
The grammar's two arms (with / without a rest) become two phases: the loop
parses leading items up to an optional single ...rest; once a rest is
consumed, the inner loop parses trailing items only, so a second ... lands
in parse_guardedpat as an unexpected token. There is no seen_rest flag —
"at most one rest" is enforced by the shape of the loop.
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 |
# File 'lib/fusion/parser.rb', line 466 def parse_arraypat expect(:lbracket) items = [] until at?(:rbracket) if at?(:spread) items << parse_pattern_rest while at?(:comma) advance break if at?(:rbracket) # trailing comma raise ParseError, "a pattern may contain at most one `...rest` (at #{peek.pos})" if at?(:spread) items << PatternItem.new(pattern: parse_guardedpat) end break end items << PatternItem.new(pattern: parse_guardedpat) break unless at?(:comma) advance end expect(:rbracket) Pattern::PArr.new(items: items) end |
#parse_corepat ⇒ Object
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 |
# File 'lib/fusion/parser.rb', line 440 def parse_corepat t = peek case t.type when :number, :string then advance; Pattern::PLit.new(value: t.value) when :true_kw, :false_kw, :null_kw then advance; Pattern::PLit.new(value: t.value) when :minus then advance; Pattern::PLit.new(value: -expect(:number).value) # negative literal when :lbracket then parse_arraypat when :lbrace then parse_objectpat when :ident advance t.value == "_" ? Pattern::PWild.new(dummy: nil) : Pattern::PBind.new(name: 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_equality ⇒ Object
97 98 99 100 101 102 103 104 |
# File 'lib/fusion/parser.rb', line 97 def parse_equality operands = [parse_ordering] while at?(:eqeq) advance operands << parse_ordering end fold_operator(operands, "equal") end |
#parse_errpat ⇒ Object
418 419 420 421 422 423 424 425 |
# File 'lib/fusion/parser.rb', line 418 def parse_errpat expect(:bang) if GUARDEDPAT_STARTERS.include?(peek.type) Pattern::PErr.new(inner: parse_guardedpat) # "!" guardedpat else Pattern::PErr.new(inner: Pattern::PWild.new(dummy: nil)) # bare "!" — matches any error, binds nothing end end |
#parse_expr ⇒ Object
69 70 71 |
# File 'lib/fusion/parser.rb', line 69 def parse_expr parse_or end |
#parse_fileref ⇒ Object
290 291 292 293 294 295 296 297 298 299 |
# File 'lib/fusion/parser.rb', line 290 def parse_fileref expect(:at) return Expression::FileRef.new(variety: :self, path: nil) unless at?(:path) # A reference is eligible for builtin/stdlib fallback (:name) iff it has no # "../"; downward paths stay eligible, only "../" forces file-only (:path). # The lexer produced the whole path as one tight token (see Lexer#try_lex_path). path = advance.value Expression::FileRef.new(variety: path.include?("..") ? :path : :name, path: path) end |
#parse_function_or_group ⇒ Object
A "(" begins a grouped expression, a function literal, or — when empty —
the clause-less function (). A function is a comma-separated list of
pattern => expr; we detect one by scanning for a top-level => before the
matching ). () matches nothing (so it yields null for any normal input
and propagates errors).
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/fusion/parser.rb', line 348 def parse_function_or_group expect(:lparen) if at?(:rparen) advance return Expression::FuncLit.new(clauses: []) end if looks_like_function? clauses = [] loop do pat = parse_pattern expect(:arrow) body = parse_expr clauses << Clause.new(pattern: pat, body: body) if at?(:comma) advance break if at?(:rparen) # trailing comma else break end end expect(:rparen) Expression::FuncLit.new(clauses: clauses) else e = parse_expr expect(:rparen) e end end |
#parse_guardedpat ⇒ Object
427 428 429 430 431 432 433 434 435 436 437 438 |
# File 'lib/fusion/parser.rb', line 427 def parse_guardedpat inner = parse_corepat if at?(:question) advance # A predicate is a full pipe so it may chain functions: `a ? b | c` tests # `a | b | c`. It stops at `=>`, `,`, `]`, `}`, `)` like any expression. pred = parse_pipe Pattern::PGuard.new(inner: inner, pred_expr: pred) else inner end end |
#parse_multiplicative ⇒ Object
A run of *// folds into one @OP.product (each / term inverted); %///
are binary and break the run. Standard left-to-right: a * b % c is (a * b) % c.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/fusion/parser.rb', line 132 def parse_multiplicative node = parse_pipe run = nil # accumulating product-run terms, or nil loop do if at?(:star) || at?(:slash) inverted = at?(:slash) advance term = parse_pipe term = pipe_into(term, "invert") if inverted if run run << term else run = [node, term] end elsif at?(:percent) || at?(:slashslash) node = pipe_operator(run, "product") if run run = nil op = at?(:percent) ? "modulo" : "quotient" advance node = pipe_operator([node, parse_pipe], op) else break end end run ? pipe_operator(run, "product") : node end |
#parse_object ⇒ Object
Fixed keys must be distinct (the ObjLit data rule); a repeat is a clean
syntax_error. Keys arriving via ...spread are dynamic and not checked.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/fusion/parser.rb', line 320 def parse_object expect(:lbrace) pairs = [] keys = [] until at?(:rbrace) if at?(:spread) advance pairs << ObjectSpread.new(value: parse_expr) else key_tok = expect(:string) key = key_tok.value raise ParseError, "duplicate key #{key.inspect} (at #{key_tok.pos})" if keys.include?(key) keys << key expect(:colon) pairs << KeyValuePair.new(key: key, value: parse_expr) end break unless at?(:comma) advance end expect(:rbrace) Expression::ObjLit.new(pairs: pairs) end |
#parse_objectpat ⇒ Object
p_object (reference.md §2.5). Leading pairs up to an optional single
...rest, which must come last — only a trailing comma may follow it. Keys
must be distinct (the PObj data rule); a repeat is a clean syntax_error.
491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 |
# File 'lib/fusion/parser.rb', line 491 def parse_objectpat expect(:lbrace) pairs = [] keys = [] until at?(:rbrace) if at?(:spread) pairs << parse_pattern_rest advance if at?(:comma) && peek(1)&.type == :rbrace # trailing comma unless at?(:rbrace) raise ParseError, "in an object pattern, `...rest` must come last (at #{peek.pos})" end break end key_pos = peek.pos pair = parse_pattern_pair raise ParseError, "duplicate key #{pair.key.inspect} (at #{key_pos})" if keys.include?(pair.key) keys << pair.key pairs << pair break unless at?(:comma) advance end expect(:rbrace) Pattern::PObj.new(pairs: pairs) end |
#parse_or ⇒ Object
--- Operator sugar (reference §2.7) --------------------------------------
The ladder below desugars every operator to a pipe into an @OP.* member
(or, for the map-pipes, a stdlib call). Tightest to loosest:
postfix · unary (! - / ~) · pipe (| |: |? |+) · * / % // · + - · ?? · == · && · ||
@OP.* is a shadowable :name reference, so a local @OP reskins the operators.
79 80 81 82 83 84 85 86 |
# File 'lib/fusion/parser.rb', line 79 def parse_or operands = [parse_and] while at?(:oror) advance operands << parse_and end fold_operator(operands, "or") end |
#parse_ordering ⇒ Object
?? (compare) is binary and left-associative — it does not fold.
107 108 109 110 111 112 113 114 |
# File 'lib/fusion/parser.rb', line 107 def parse_ordering node = parse_additive while at?(:qq) advance node = pipe_operator([node, parse_additive], "compare") end node end |
#parse_pattern ⇒ Object
---- Patterns ----
---- Pattern grammar (mirrors reference.md §2.5 EBNF) ------------------
pattern = p_error | p_guarded
p_error = "!" | "!" p_guarded
p_guarded = p_core [ "?" predicate ]
p_core = p_literal | p_bind | p_wildcard | p_array | p_object
Note: p_core does NOT include p_error. The "no nested !pat" property
falls out of the grammar shape — p_error 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.
409 410 411 |
# File 'lib/fusion/parser.rb', line 409 def parse_pattern at?(:bang) ? parse_errpat : parse_guardedpat end |
#parse_pattern_pair ⇒ Object
p_pair = string ":" p_guarded
526 527 528 529 530 |
# File 'lib/fusion/parser.rb', line 526 def parse_pattern_pair key = expect(:string).value expect(:colon) PatternPair.new(key: key, pattern: parse_guardedpat) end |
#parse_pattern_rest ⇒ Object
p_rest = "..." [ identifier ] — the single rest binder, shared by array and object patterns. Callers parse it only at a rest position and then continue with items/pairs only, which is what holds a pattern to one rest.
519 520 521 522 523 |
# File 'lib/fusion/parser.rb', line 519 def parse_pattern_rest expect(:spread) name = at?(:ident) ? advance.value : nil PatternRest.new(name: name) end |
#parse_pipe ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/fusion/parser.rb', line 159 def parse_pipe node = parse_unary loop do if at?(:pipe) advance node = Expression::Pipe.new(left: node, right: parse_unary) elsif at?(:pipemap) || at?(:pipefilter) || at?(:pipereduce) target = { pipemap: "map", pipefilter: "filter", pipereduce: "reduce" }[peek.type] advance arg = Expression::ObjLit.new(pairs: [ KeyValuePair.new(key: "c", value: node), KeyValuePair.new(key: "f", value: parse_unary), ]) node = Expression::Pipe.new(left: arg, right: name_ref(target)) else break end end node end |
#parse_postfix ⇒ Object
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/fusion/parser.rb', line 205 def parse_postfix node = parse_primary loop do if at?(:dot) advance key = expect(:ident).value node = Expression::Member.new(obj: node, key: key) elsif at?(:lbracket) advance idx = parse_expr if at?(:equals) advance value = parse_expr expect(:rbracket) node = Expression::IndexSet.new(obj: node, idx: idx, value: value) else expect(:rbracket) node = Expression::Index.new(obj: node, idx: idx) end else break end end node end |
#parse_primary ⇒ Object
261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/fusion/parser.rb', line 261 def parse_primary t = peek case t.type when :number, :string then advance; Expression::Lit.new(value: t.value) when :true_kw, :false_kw, :null_kw then advance; Expression::Lit.new(value: t.value) when :lbracket then parse_array when :lbrace then parse_object when :lparen then parse_function_or_group when :ident then advance; Expression::Ident.new(name: t.value) when :at then parse_fileref when :atat then parse_superref else raise ParseError, "Unexpected token #{t.type} (#{t.value.inspect}) at #{t.pos}" end end |
#parse_repl_entry ⇒ Object
A leading identifier = marks a statement; anything else is an expression.
(A bare identifier is itself a valid expression, so the = is the decider.)
54 55 56 57 58 59 60 |
# File 'lib/fusion/parser.rb', line 54 def parse_repl_entry if at?(:ident) && peek(1)&.type == :equals parse_statement else parse_expr end end |
#parse_statement ⇒ Object
statement = identifier "=" expr (REPL only; files contain one expr)
63 64 65 66 67 |
# File 'lib/fusion/parser.rb', line 63 def parse_statement name = expect(:ident).value expect(:equals) AST::Statement::Assignment.new(name: name, expression: parse_expr) end |
#parse_superref ⇒ Object
@@ is super. Bare @@ is super of the current file's own name; @@name
(or a downward @@dir/name) is a stable reference to that name — it skips
the sibling name.fsn, resolving builtin → stdlib, so a user's local shadow
can't intercept it. @@../… is meaningless (super of an upward path) and
falls through to a parse error.
281 282 283 284 285 286 287 288 |
# File 'lib/fusion/parser.rb', line 281 def parse_superref expect(:atat) return Expression::FileRef.new(variety: :super, path: nil) unless at?(:path) tok = advance raise ParseError, "`@@` cannot take an upward path (at #{tok.pos})" if tok.value.include?("..") Expression::FileRef.new(variety: :super_name, path: tok.value) end |
#parse_unary ⇒ Object
Unary prefixes: !x builds an error (bare ! is !null); -x negates,
/x inverts, ~x is logical not. All bind tighter than |.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/fusion/parser.rb', line 187 def parse_unary if at?(:bang) advance PRIMARY_STARTERS.include?(peek.type) ? Expression::ErrLit.new(payload: parse_unary) : Expression::ErrLit.new(payload: nil) elsif at?(:minus) advance negate(parse_unary) elsif at?(:slash) advance pipe_into(parse_unary, "invert") elsif at?(:tilde) advance pipe_into(parse_unary, "not") else parse_postfix end end |
#peek(o = 0) ⇒ Object
---- token helpers ----
533 |
# File 'lib/fusion/parser.rb', line 533 def peek(o = 0) = @toks[@i + o] |
#pipe_into(expr, member) ⇒ Object
expr | @OP.member
239 |
# File 'lib/fusion/parser.rb', line 239 def pipe_into(expr, member) = Expression::Pipe.new(left: expr, right: op_member(member)) |
#pipe_operator(operands, member) ⇒ Object
[operands...] | @OP.member
242 243 244 245 |
# File 'lib/fusion/parser.rb', line 242 def pipe_operator(operands, member) arr = Expression::ArrLit.new(items: operands.map { |e| ArrayItem.new(value: e) }) Expression::Pipe.new(left: arr, right: op_member(member)) end |