Class: Fusion::Parser
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
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, location:) ⇒ Object
Parse a complete program.
-
.parse_repl(src, location:) ⇒ 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
-
#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
p_array (reference.md §2.5).
- #parse_corepat ⇒ 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 of `pattern => expr`; we detect one by scanning for a top-level `=>` before the matching `)`.
- #parse_guardedpat ⇒ Object
-
#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_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.
-
#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_prefix ⇒ Object
‘!` is a prefix operator that constructs an error from its operand.
- #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).
-
#peek(o = 0) ⇒ Object
—- token helpers —-.
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, location:) ⇒ 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. ‘location` is the syntax_error’s “code X” / “code <inline>” context.
27 28 29 30 31 32 33 34 35 |
# File 'lib/fusion/parser.rb', line 27 def self.parse_file(src, location:) toks = Lexer.new(src).tokens p = new(toks) expr = p.parse_expr p.expect(:eof) expr rescue ParseError => err Interpreter::ErrorVal.internal(kind: "syntax_error", location: location, operation: "parsing", input: src, message: err.) end |
.parse_repl(src, location:) ⇒ 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, location:) toks = Lexer.new(src).tokens p = new(toks) entry = p.parse_repl_entry p.expect(:eof) entry rescue ParseError => err Interpreter::ErrorVal.internal(kind: "syntax_error", location: location, operation: "parsing", input: src, message: err.) end |
Instance Method Details
#advance ⇒ Object
397 |
# File 'lib/fusion/parser.rb', line 397 def advance = (@toks[@i].tap { @i += 1 }) |
#at?(type) ⇒ Boolean
396 |
# File 'lib/fusion/parser.rb', line 396 def at?(type) = peek.type == type |
#expect(type) ⇒ Object
398 399 400 401 402 |
# File 'lib/fusion/parser.rb', line 398 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 “)”?
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/fusion/parser.rb', line 242 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
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/fusion/parser.rb', line 164 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_guarded`s — 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.
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/fusion/parser.rb', line 328 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
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/fusion/parser.rb', line 303 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 :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_errpat ⇒ Object
281 282 283 284 285 286 287 288 |
# File 'lib/fusion/parser.rb', line 281 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_pipe end |
#parse_fileref ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/fusion/parser.rb', line 137 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 Expression::FileRef.new(variety: :self, path: 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 Expression::FileRef.new(variety: ? :name : :path, path: parts.join("/")) 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).
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/fusion/parser.rb', line 211 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
290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/fusion/parser.rb', line 290 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_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.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/fusion/parser.rb', line 183 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.
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/fusion/parser.rb', line 353 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_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.
272 273 274 |
# File 'lib/fusion/parser.rb', line 272 def parse_pattern at?(:bang) ? parse_errpat : parse_guardedpat end |
#parse_pattern_pair ⇒ Object
p_pair = string “:” p_guarded
388 389 390 391 392 |
# File 'lib/fusion/parser.rb', line 388 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.
381 382 383 384 385 |
# File 'lib/fusion/parser.rb', line 381 def parse_pattern_rest expect(:spread) name = at?(:ident) ? advance.value : nil PatternRest.new(name: name) end |
#parse_pipe ⇒ Object
73 74 75 76 77 78 79 80 81 |
# File 'lib/fusion/parser.rb', line 73 def parse_pipe left = parse_prefix while at?(:pipe) advance right = parse_prefix left = Expression::Pipe.new(left: left, right: right) end left end |
#parse_postfix ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/fusion/parser.rb', line 104 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 expect(:rbracket) node = Expression::Index.new(obj: node, idx: 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)`.
91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/fusion/parser.rb', line 91 def parse_prefix if at?(:bang) advance if PRIMARY_STARTERS.include?(peek.type) Expression::ErrLit.new(payload: parse_prefix) # allow !!x to nest else Expression::ErrLit.new(payload: nil) # bare ! -> !null end else parse_postfix end end |
#parse_primary ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/fusion/parser.rb', line 123 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 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 |
#peek(o = 0) ⇒ Object
—- token helpers —-
395 |
# File 'lib/fusion/parser.rb', line 395 def peek(o = 0) = @toks[@i + o] |