-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.go
More file actions
3337 lines (3196 loc) · 116 KB
/
Copy pathparser.go
File metadata and controls
3337 lines (3196 loc) · 116 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
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
344
345
346
347
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Package parser builds an AST from tokens: recursive descent for statements,
// Pratt (precedence-climbing) for expressions, and a scope stack to resolve the
// classic local-variable-vs-method-call ambiguity (plan-rbgo.md §10).
//
// The scope stack is what lets `foo` mean a variable read when `foo` was
// assigned earlier in the same def, and a (possibly command-style) method call
// otherwise — exactly MRI's rule.
package parser
import (
"fmt"
"math/big"
"strconv"
"strings"
"github.com/go-ruby-parser/parser/ast"
"github.com/go-ruby-parser/parser/lexer"
"github.com/go-ruby-parser/parser/token"
)
type parseError struct{ msg string }
func (e parseError) Error() string { return e.msg }
// scope tracks declared locals. A hard scope is a method/class/module/top-level
// boundary that local lookup does not cross; a soft scope (a block) chains to
// its enclosing scope, so a block sees and can assign the enclosing locals.
type scope struct {
locals map[string]bool
hard bool
// Implicit block-parameter tracking (numbered params _1.._9 and `it`),
// only meaningful for a block scope that declared no explicit |params|.
explicitParams bool
maxNum int // highest _N referenced in the block body (0 = none)
usedIt bool // bare `it` referenced in the block body
}
func newScope(hard bool) *scope { return &scope{locals: map[string]bool{}, hard: hard} }
// numberedParam returns N for a numbered block parameter name "_1".."_9", or 0.
func numberedParam(name string) int {
if len(name) == 2 && name[0] == '_' && name[1] >= '1' && name[1] <= '9' {
return int(name[1] - '0')
}
return 0
}
// implicitParamScope returns the innermost scope if it is a block that may host
// implicit numbered/`it` parameters (a soft scope with no explicit |params|),
// or nil. Implicit parameters bind to the innermost block and never cross a
// method/class boundary.
func (p *Parser) implicitParamScope() *scope {
s := p.scope()
if s.hard || s.explicitParams {
return nil
}
return s
}
// finishImplicitParams resolves the parameter list of a freshly-parsed block.
// With explicit params it returns them unchanged; otherwise it synthesises the
// numbered (_1.._maxNum) or `it` parameters its body referenced. A body may not
// mix the two forms.
func (p *Parser) finishImplicitParams(s *scope, explicit []string) []string {
if len(explicit) > 0 {
return explicit
}
if s.maxNum > 0 && s.usedIt {
p.fail("`it` is not allowed together with numbered parameters")
}
if s.maxNum > 0 {
// Numbered parameters may not nest: an enclosing block (up to the nearest
// method/class boundary) that also uses them is a Ruby SyntaxError.
for i := len(p.scopes) - 2; i >= 0; i-- {
if p.scopes[i].maxNum > 0 {
p.fail("numbered parameter is already used in outer block")
}
if p.scopes[i].hard {
break
}
}
names := make([]string, s.maxNum)
for i := range names {
names[i] = "_" + string(rune('1'+i))
}
return names
}
if s.usedIt {
return []string{"it"}
}
return explicit
}
// Parser holds parsing state.
type Parser struct {
toks []token.Token
pos int
scopes []*scope
// noDo suppresses `do…end` block attachment while parsing a while/until
// condition, so the `do` there belongs to the loop, not to a call in the
// condition.
noDo bool
// patternDepth > 0 while parsing a pattern atom, where a top-level `|` is the
// alternation separator rather than the bitwise-or operator.
patternDepth int
// noPipe suppresses treating `|` as the bitwise-or operator while parsing the
// default expression of an optional block parameter inside a `|...|` list, so
// the `|` there closes the parameter list rather than continuing the default.
noPipe bool
// noMasgn suppresses multiple-assignment detection while parsing a parameter
// default (`def f(a = c(1), b = nil)`), so the comma separating parameters is
// not mistaken for a masgn target separator (`c(1), b = nil`).
noMasgn bool
// noRescueMod suppresses the trailing modifier-`rescue` while parsing the
// arguments of a paren-less command call, so the `rescue` binds to the whole
// command rather than to its last argument: `raise "x" rescue 42` is
// `(raise "x") rescue 42`, not `raise("x" rescue 42)`. It is cleared inside any
// nested delimited context (a parenthesised group, a `(…)`/`[…]` argument list,
// a `{…}` hash), where an inner modifier-`rescue` is again allowed.
noRescueMod bool
// bracketDepth > 0 while parsing a parenthesised call-argument list or a hash
// literal, where newlines are insignificant. A `key:` whose value sits on the
// next line (`f(`⏎` k:`⏎` v)`) is then a continued pair, not a value-omitted
// shorthand; at top level (depth 0) a newline after `key:` ends the command.
bracketDepth int
}
// parseHook, when non-nil, runs at the start of Parse. It exists only so a
// white-box test can inject a non-parseError panic and exercise the recover's
// internal-error path; it is nil in normal operation.
var parseHook func()
// Parse lexes and parses src into a Program. It never panics: a malformed input
// yields a parse error, and any unexpected internal panic is also surfaced as a
// parse error rather than propagating to the caller.
func Parse(src string) (prog *ast.Program, err error) {
toks := lexer.New(src).Tokenize()
p := &Parser{toks: toks, scopes: []*scope{newScope(true)}}
defer func() {
if r := recover(); r != nil {
if pe, ok := r.(parseError); ok {
prog, err = nil, pe
return
}
// An internal bug (e.g. an unexpected type assertion or index) must not
// crash the caller: report it as a parse error so the parser's contract
// of never panicking holds for every input.
prog, err = nil, parseError{msg: fmt.Sprintf("internal parser error: %v", r)}
}
}()
if parseHook != nil {
parseHook()
}
body := p.parseStatements(map[token.Type]bool{})
p.expect(token.EOF)
return &ast.Program{Body: body}, nil
}
// --- token cursor ---
// The cursor clamps at the trailing EOF: once exhausted, cur/peekTok keep
// returning EOF and advance is a no-op, so a parser that over-reads on malformed
// input (e.g. an unterminated string interpolation) fails cleanly via expect
// instead of indexing past the slice and panicking.
func (p *Parser) cur() token.Token {
if p.pos >= len(p.toks) {
return p.toks[len(p.toks)-1] // the EOF token
}
return p.toks[p.pos]
}
// peekTok returns the token after the cursor. Every caller first checks the
// cursor is a specific non-EOF token, and the trailing EOF is always present, so
// the cursor is at most the second-to-last token and pos+1 stays in range.
func (p *Parser) peekTok() token.Token { return p.toks[p.pos+1] }
func (p *Parser) advance() token.Token {
t := p.cur()
if p.pos < len(p.toks) {
p.pos++
}
return t
}
func (p *Parser) is(tt token.Type) bool { return p.cur().Type == tt }
func (p *Parser) accept(tt token.Type) bool {
if p.is(tt) {
p.advance()
return true
}
return false
}
func (p *Parser) expect(tt token.Type) token.Token {
if !p.is(tt) {
p.fail("expected %s, got %q (%s)", tt, p.cur().Lit, p.cur().Type)
}
return p.advance()
}
// fail never returns; the ast.Node result lets primary parsers write
// `return p.fail(...)` without an unreachable trailing return.
func (p *Parser) fail(format string, args ...any) ast.Node {
t := p.cur()
panic(parseError{msg: fmt.Sprintf("parse error at line %d: %s", t.Line, fmt.Sprintf(format, args...))})
}
func (p *Parser) skipNewlines() {
for p.is(token.NEWLINE) {
p.advance()
}
}
// firstSignificantIs reports whether the first non-NEWLINE token at or after the
// cursor is of type tt, without consuming anything. The token stream always ends
// in a (non-NEWLINE) EOF, so the scan always finds a significant token.
func (p *Parser) firstSignificantIs(tt token.Type) bool {
i := p.pos
for i < len(p.toks) && p.toks[i].Type == token.NEWLINE {
i++
}
return i < len(p.toks) && p.toks[i].Type == tt
}
// --- scope ---
func (p *Parser) scope() *scope { return p.scopes[len(p.scopes)-1] }
func (p *Parser) pushScope() { p.scopes = append(p.scopes, newScope(true)) }
func (p *Parser) pushBlockScope() { p.scopes = append(p.scopes, newScope(false)) }
func (p *Parser) popScope() { p.scopes = p.scopes[:len(p.scopes)-1] }
func (p *Parser) declareLocal(n string) { p.scope().locals[n] = true }
// isLocal reports whether n is a visible local: it searches the scope chain but
// does not cross a hard (method/class/module/top-level) boundary, while block
// scopes (soft) chain to their enclosing scope.
func (p *Parser) isLocal(n string) bool {
for i := len(p.scopes) - 1; i >= 0; i-- {
if p.scopes[i].locals[n] {
return true
}
if p.scopes[i].hard {
break
}
}
return false
}
// barewordValue turns a bare name into the node it denotes: a local-variable
// reference when the name is a visible local, otherwise a no-arg method call on
// self. Used for `{x:}` hash shorthand.
func (p *Parser) barewordValue(name string) ast.Node {
if p.isLocal(name) {
return &ast.VarRef{Name: name}
}
return &ast.Call{Name: name}
}
// --- statements ---
var (
bodyEnd = map[token.Type]bool{token.END: true}
braceBlockEnd = map[token.Type]bool{token.RBRACE: true}
beginBodyEnd = map[token.Type]bool{token.RESCUE: true, token.ELSE: true, token.ENSURE: true, token.END: true}
caseBodyEnd = map[token.Type]bool{token.WHEN: true, token.ELSE: true, token.END: true}
inBodyEnd = map[token.Type]bool{token.IN: true, token.ELSE: true, token.END: true}
ifBodyEnd = map[token.Type]bool{token.END: true, token.ELSE: true, token.ELSIF: true}
// rangeHiEnds marks tokens that cannot begin a range's high endpoint, making
// the range endless (`1..`, `arr[2..]`).
rangeHiEnds = map[token.Type]bool{token.RBRACKET: true, token.RPAREN: true, token.RBRACE: true, token.NEWLINE: true, token.EOF: true, token.COMMA: true, token.END: true, token.THEN: true, token.DO: true}
)
func (p *Parser) parseStatements(stop map[token.Type]bool) []ast.Node {
// A statement body is a fresh expression context: the masgn-suppression set
// while parsing an enclosing command-argument / parameter-default does not
// reach into a nested body (`p begin; a, b = z; end` is a real masgn). Clear it
// for the duration and restore on exit.
savedMasgn := p.noMasgn
p.noMasgn = false
// A nested statement body (a parenthesised group, an interpolation, a keyword
// block) is a fresh context: an inner modifier-`rescue` there is not the one
// that binds to an enclosing command call, so re-enable it.
savedRescue := p.noRescueMod
p.noRescueMod = false
defer func() { p.noMasgn = savedMasgn; p.noRescueMod = savedRescue }()
var body []ast.Node
for {
p.skipNewlines()
if p.is(token.EOF) || stop[p.cur().Type] {
break
}
body = append(body, p.parseStatement())
// Statements are separated by newlines/semicolons; the lexer emits both
// as NEWLINE. A terminator or EOF may follow directly.
if !p.is(token.NEWLINE) && !p.is(token.EOF) && !stop[p.cur().Type] {
p.fail("unexpected %q after statement", p.cur().Lit)
}
}
return body
}
func (p *Parser) parseStatement() ast.Node {
switch p.cur().Type {
case token.DEF:
// A method definition may carry a trailing modifier (`def f; …; end if c`)
// and is occasionally chained (`def f; end.tap { … }`), so feed it through
// the postfix/modifier machinery rather than returning it raw.
return p.applyModifiers(p.parsePostfixTail(p.parseDef()))
case token.CLASS:
return p.applyModifiers(p.parsePostfixTail(p.parseClass()))
case token.MODULE:
return p.applyModifiers(p.parsePostfixTail(p.parseModule()))
case token.RETURN:
return p.applyModifiers(p.parseReturn())
case token.BREAK:
return p.applyModifiers(p.parseBreak())
case token.NEXT:
return p.applyModifiers(p.parseNext())
case token.RETRY:
p.advance()
return p.applyModifiers(&ast.Retry{})
case token.ALIAS:
return p.applyModifiers(p.parseAlias())
case token.UNDEF:
return p.applyModifiers(p.parseUndef())
default:
return p.applyModifiers(p.parseOneLineMatch(p.parseKeywordLogical()))
}
}
// parseKeywordLogical parses the low-precedence keyword operators `and`, `or`,
// and the prefix `not`. They bind looser than `=` (and everything else except
// the trailing if/unless/while/until modifiers), so they sit between an
// assignment and a statement. `and`/`or` are left-associative and desugar to the
// `&&`/`||` BinaryExpr nodes; `not` desugars to a `!` UnaryExpr. (`p(1 and 2)`
// is itself invalid Ruby, so this layer only appears in statement/assignment
// positions, never inside a paren-less argument.)
func (p *Parser) parseKeywordLogical() ast.Node {
left := p.parseExprOrAssign()
for p.is(token.AND) || p.is(token.OR) {
op := "&&"
if p.is(token.OR) {
op = "||"
}
p.advance()
left = &ast.BinaryExpr{Op: op, Left: left, Right: p.parseKeywordOperand()}
}
return left
}
// parseKeywordOperand parses an operand of `and`/`or`. Besides an ordinary
// expression (a leading `not` is handled by parseExprOrAssign) it accepts the
// jump keywords `return`/`break`/`next` (`a or return`, `x = 1 and break`),
// which are valid in this position in MRI.
func (p *Parser) parseKeywordOperand() ast.Node {
switch p.cur().Type {
case token.RETURN:
return p.parseReturn()
case token.BREAK:
return p.parseBreak()
case token.NEXT:
return p.parseNext()
}
return p.parseExprOrAssign()
}
// parseOneLineMatch wraps a statement-level expression in a one-line pattern
// match when followed by `=> pattern` (rightward assignment) or `in pattern`
// (boolean test). `=` binds tighter than these, so `x = v in P` is `(x=v) in P`.
func (p *Parser) parseOneLineMatch(subject ast.Node) ast.Node {
switch {
case p.accept(token.HASHROCKET):
return &ast.MatchPattern{Subject: subject, Pattern: p.parsePattern()}
case p.accept(token.IN):
return &ast.MatchPattern{Subject: subject, Pattern: p.parsePattern(), Bool: true}
}
return subject
}
// applyModifiers wraps a statement in trailing `if/unless/while/until` modifiers
// (`puts x if cond`, `return unless ok`) and the modifier `rescue`. It is used by
// the keyword-statement paths (`def`, `class`, `return`, …) whose head is parsed
// before the ordinary expression machinery would apply a modifier `rescue`; an
// ordinary-expression statement has its `rescue` consumed earlier (in
// withRescueModifier), so none remains here. `rescue` binds tighter than the
// conditional modifiers, matching MRI (`def…end rescue nil if c` is
// `(def…end rescue nil) if c`), which the source-order left-to-right wrapping of
// this loop reproduces.
func (p *Parser) applyModifiers(node ast.Node) ast.Node {
for {
switch p.cur().Type {
case token.RESCUE:
p.advance()
fallback := p.parseTernary()
node = &ast.Begin{Body: []ast.Node{node}, Rescues: []ast.RescueClause{{Body: []ast.Node{fallback}}}}
case token.IF:
p.advance()
node = &ast.If{Cond: p.parseCond(), Then: []ast.Node{node}}
case token.UNLESS:
p.advance()
node = &ast.If{Cond: not(p.parseCond()), Then: []ast.Node{node}}
case token.WHILE:
p.advance()
node = &ast.While{Cond: p.parseCond(), Body: []ast.Node{node}}
case token.UNTIL:
p.advance()
node = &ast.While{Cond: not(p.parseCond()), Body: []ast.Node{node}}
default:
return node
}
}
}
func not(n ast.Node) ast.Node { return &ast.UnaryExpr{Op: "!", Operand: n} }
// parseConstPath parses a constant path in a name/superclass position: a bare
// constant (`Foo`), a scope-resolution path (`Foo::Bar::Baz`), or a leading-`::`
// path (`::Foo`, `::Foo::Bar`). It returns the trailing segment name and, when
// the path is more than a bare constant, the full *ScopedConst node (else nil).
func (p *Parser) parseConstPath() (name string, path ast.Node) {
var node ast.Node
if p.accept(token.SCOPE) { // leading `::Name`
name = p.expect(token.CONST).Lit
node = &ast.ScopedConst{Name: name, Global: true}
} else if p.is(token.CONST) {
name = p.advance().Lit
node = &ast.ConstRef{Name: name}
} else {
// An expression receiver before `::CONST` names a dynamically-scoped class
// or module: `class self.class::Foo`, `class obj::Bar`. parsePostfix reads
// the whole `recv::CONST` chain and yields a ScopedConst whose Name is the
// trailing segment, exactly the path node we want.
expr := p.parsePostfix()
sc, ok := expr.(*ast.ScopedConst)
if !ok {
p.fail("expected a class/module path")
}
return sc.Name, sc
}
scoped := node // becomes the *ScopedConst once a `::CONST` segment is seen
for p.is(token.SCOPE) && p.peekTok().Type == token.CONST {
p.advance() // ::
name = p.advance().Lit
scoped = &ast.ScopedConst{Recv: scoped, Name: name}
}
if _, ok := scoped.(*ast.ConstRef); ok {
return name, nil // a bare constant: no path node
}
return name, scoped
}
func (p *Parser) parseClass() ast.Node {
p.expect(token.CLASS)
// `class << target` opens target's singleton (metaclass). A SHOVEL here is
// the singleton-class form, not a constant path.
if p.accept(token.SHOVEL) {
target := p.parseTernary()
p.pushScope() // the singleton-class body has its own local scope
body := p.parseBodyWithRescue()
p.popScope()
p.expect(token.END)
return &ast.SingletonClassDef{Target: target, Body: body}
}
name, path := p.parseConstPath()
super := ""
var superExpr ast.Node
if p.accept(token.LT) {
// The superclass is any expression (`class A < Base`, `class A < self`,
// `class A < Struct.new(:x)`, `class A < ns::Base`). A bare constant keeps
// the plain-name Super form; anything else is recorded as SuperExpr.
sup := p.parseTernary()
if c, ok := sup.(*ast.ConstRef); ok {
super = c.Name
} else {
superExpr = sup
}
}
p.pushScope() // a class body has its own local scope
body := p.parseBodyWithRescue()
p.popScope()
p.expect(token.END)
return &ast.ClassDef{Name: name, NamePath: path, Super: super, SuperExpr: superExpr, Body: body}
}
func (p *Parser) parseModule() ast.Node {
p.expect(token.MODULE)
name, path := p.parseConstPath()
p.pushScope() // a module body has its own local scope
body := p.parseBodyWithRescue()
p.popScope()
p.expect(token.END)
return &ast.ModuleDef{Name: name, NamePath: path, Body: body}
}
// isDefRecvStart reports whether tt can begin an explicit `def` receiver
// (`def self.x`, `def obj.x`, `def Const.x`, `def @ivar.x`, `def @@c.x`,
// `def $g.x`).
func isDefRecvStart(tt token.Type) bool {
switch tt {
case token.SELF, token.IDENT, token.CONST, token.IVAR, token.CVAR, token.GVAR:
return true
}
return false
}
func (p *Parser) parseDef() ast.Node {
p.expect(token.DEF)
singleton := false
var recv ast.Node
// A parenthesised singleton receiver: def (expr).foo. MRI evaluates the paren
// group to an arbitrary object and defines a singleton method on it, so the
// receiver may be any single expression — a local (`def (obj).m`), a constant
// (`def (String).m`), a method call (`def (foo.bar).m`), `(self)`, a ternary,
// an `and`/`or`/assignment, etc. This emits the same MethodDef shape as the
// bare `def obj.foo` form (Recv set, Singleton false) so the compiler lowers it
// with no special case. A `(` right after `def` can only open this form (a
// method name is never a paren group). MRI requires exactly one expression in
// the group (a compound `def (a; b).m` is a syntax error), so a receiver that
// is not a single statement is rejected here too.
if p.is(token.LPAREN) {
p.advance() // (
stmts := p.parseStatements(map[token.Type]bool{token.RPAREN: true})
if len(stmts) != 1 {
p.fail("singleton def receiver must be a single expression")
}
recv = stmts[0]
p.expect(token.RPAREN)
p.expect(token.DOT)
}
// A receiver before the method name: def self.foo / def obj.foo / def Const.foo
// / def @ivar.foo / def $g.foo. The kind guard keeps peekTok in range (the
// receiver is always a single name token followed by a dot).
if recv == nil && isDefRecvStart(p.cur().Type) && p.peekTok().Type == token.DOT {
switch p.cur().Type {
case token.SELF:
p.advance() // self
singleton = true
case token.IDENT: // def obj.foo — singleton method on a local's object
recv = &ast.VarRef{Name: p.advance().Lit}
case token.CONST: // def Const.foo — class/module method
recv = &ast.ConstRef{Name: p.advance().Lit}
case token.IVAR: // def @ivar.foo — singleton method on an ivar's object
recv = &ast.IvarRef{Name: p.advance().Lit}
case token.CVAR:
recv = &ast.CVarRef{Name: p.advance().Lit}
case token.GVAR:
recv = &ast.GVarRef{Name: p.advance().Lit}
}
if recv != nil || singleton {
p.advance() // .
}
}
name, ok := p.parseDefName()
if !ok {
p.fail("expected method name after def")
}
p.pushScope() // params (and their defaults) live in the method scope
var params []string
var defaults []ast.Node
var kwParams []ast.KwParam
var prepends []ast.Node
var kwRest, blockParam string
forward := false
splat := -1
if p.accept(token.LPAREN) {
params, defaults, prepends, splat, kwParams, kwRest, blockParam, forward = p.parseDefParams(token.RPAREN)
p.expect(token.RPAREN)
} else if (p.is(token.IDENT) || p.is(token.LABEL) || p.is(token.AMPER) || p.is(token.DOTDOTDOT) ||
p.is(token.STAR) || p.is(token.POW) || p.is(token.LPAREN)) && !p.is(token.NEWLINE) {
// paren-less params: def foo a, b / def foo a:, b: 2 / def foo &blk /
// def foo *rest / def foo **opts / def foo (a, b), c
params, defaults, prepends, splat, kwParams, kwRest, blockParam, forward = p.parseDefParams(token.NEWLINE)
}
// Endless method definition: def name(params) = expr (no body/end).
if p.accept(token.ASSIGN) {
body := []ast.Node{p.parseExprOrAssign()}
if len(prepends) > 0 {
body = append(prepends, body...)
}
p.popScope()
return &ast.MethodDef{Name: name, Params: params, Defaults: defaults, SplatIndex: splat, KwParams: kwParams, KwRest: kwRest, BlockParam: blockParam, Singleton: singleton, Recv: recv, Forward: forward, Body: body}
}
// A method body may carry rescue/else/ensure clauses without an explicit begin.
body := p.parseBodyWithRescue()
// A destructuring parameter `(a, b)` expands to a multiple-assignment from a
// synthetic positional, prepended to the body (the same shape parseBlockParams
// uses for block destructuring).
if len(prepends) > 0 {
body = append(prepends, body...)
}
p.popScope()
p.expect(token.END)
return &ast.MethodDef{Name: name, Params: params, Defaults: defaults, SplatIndex: splat, KwParams: kwParams, KwRest: kwRest, BlockParam: blockParam, Singleton: singleton, Recv: recv, Forward: forward, Body: body}
}
// parseDefName reads the name in a `def`: an identifier/constant, an operator
// method (`<=>`, `<`, `==`, `+`, `<<`, …), or the index methods `[]` / `[]=`.
func (p *Parser) parseDefName() (string, bool) {
switch p.cur().Type {
case token.IDENT:
name := p.advance().Lit
// Setter method: def name=(value) — the '=' hugs the name (no space).
if p.is(token.ASSIGN) && !p.cur().SpaceBefore {
p.advance()
name += "="
}
return name, true
case token.PLUS, token.MINUS, token.TILDE, token.BANG:
// Binary `+`/`-` and the unary methods `~`/`!`, plus the unary-operator
// methods `+@`/`-@`/`~@`/`!@` whose `@` hugs the operator.
name := p.advance().Lit
// `+@`/`-@`/`~@`/`!@`: a bare `@` (no following name) hugs the operator. The
// lexer yields it as an ILLEGAL "@" token (bare `@` is not a valid ivar).
if p.cur().Lit == "@" && !p.cur().SpaceBefore {
p.advance()
name += "@"
}
return name, true
case token.CONST,
token.SPACESHIP, token.LT, token.GT, token.LE, token.GE,
token.EQ, token.EQQ, token.NEQ, token.MATCH, token.SHOVEL, token.RSHIFT,
token.STAR, token.POW, token.SLASH, token.PERCENT,
token.AMPER, token.PIPE, token.CARET:
return p.advance().Lit, true
case token.LBRACKET:
p.advance()
p.expect(token.RBRACKET)
if p.accept(token.ASSIGN) {
return "[]=", true
}
return "[]", true
case token.XSTRING:
// The backtick method name `def \`(cmd); end` (lexed as an empty XSTRING).
p.advance()
return "`", true
}
// A keyword used as a method name: def do / def then / def in / def class …
// Ruby permits any reserved word in def-name position, including the setter
// form `def ensure=(v)` where a `=` hugs the keyword name.
if _, isKeyword := token.Keywords[p.cur().Lit]; isKeyword {
name := p.advance().Lit
if p.is(token.ASSIGN) && !p.cur().SpaceBefore {
p.advance()
name += "="
}
return name, true
}
return "", false
}
// parseDefParams parses a method's parameter list, each optionally `name =
// default`. Each parameter is declared before its (and later) defaults are
// parsed, so a default may reference earlier parameters. defaults is parallel to
// params, nil for a required parameter.
func (p *Parser) parseDefParams(until token.Type) (params []string, defaults, prepends []ast.Node, splat int, kwParams []ast.KwParam, kwRest, blockParam string, forward bool) {
splat = -1
group := 0
// A parenthesised parameter list may span several lines, with newlines after
// the open paren and around the separating commas; a paren-less list (until ==
// NEWLINE) must not skip newlines, as one terminates it.
paren := until == token.RPAREN
if paren {
p.skipNewlines()
}
if p.is(until) || p.is(token.NEWLINE) {
return params, defaults, prepends, splat, kwParams, kwRest, blockParam, forward
}
for {
if paren {
p.skipNewlines()
if p.is(until) { // a trailing comma before the close paren
break
}
}
if p.is(token.LPAREN) { // destructuring positional param: `((a, b), c)`
outer, chained := p.parseDestructureParam(&group)
// The synthetic positional carrying the destructured value is recorded as
// the parameter (so arity is correct); the unpacking MultiAssigns run at
// the top of the body. Inner (nested) unpacks come first so each
// synthetic is bound before the outer unpack reads it.
params = append(params, outer.Values[0].(*ast.VarRef).Name)
defaults = append(defaults, nil)
// The outer unpack binds the inner synthetics first, then the chained
// inner unpacks read them.
prepends = append(prepends, outer)
for _, c := range chained {
prepends = append(prepends, c)
}
if !p.accept(token.COMMA) {
break
}
continue
}
if p.accept(token.DOTDOTDOT) { // `...` argument-forwarding param (always last)
forward = true
break
}
if p.accept(token.AMPER) { // &block param, or anonymous & (always last)
// `def f(&)` — an anonymous block param (Ruby 3.1+), forwardable as `&`.
// It is recorded with the sentinel name "&" (no Ruby local can be named
// that). A named &block declares the local.
if p.is(token.IDENT) {
blockParam = p.advance().Lit
p.declareLocal(blockParam)
} else {
blockParam = "&"
}
break
}
if p.accept(token.POW) { // **rest keyword-splat, anonymous **, or **nil
// `def f(**)` — anonymous double-splat (Ruby 3.2+), sentinel name "**".
// `def f(**nil)` — explicitly no keyword args, recorded as "nil".
switch {
case p.is(token.IDENT):
kwRest = p.advance().Lit
p.declareLocal(kwRest)
case p.is(token.NIL):
p.advance()
kwRest = "nil"
default:
kwRest = "**"
}
// A double-splat may be followed by a &block param, so do not break;
// consume a separating comma and continue, otherwise stop.
if !p.accept(token.COMMA) {
break
}
continue
}
if p.is(token.LABEL) { // keyword param: `a:` (required) or `a: default`
name := p.advance().Lit
p.declareLocal(name)
var def ast.Node
if !p.is(token.COMMA) && !p.is(until) && !p.is(token.NEWLINE) {
def = p.parseParamDefault()
}
kwParams = append(kwParams, ast.KwParam{Name: name, Default: def})
if !p.accept(token.COMMA) {
break
}
continue
}
if p.accept(token.STAR) { // *rest splat param, or anonymous *
splat = len(params)
// `def f(*)` / `def f(a, *)` — anonymous splat (sentinel name "*").
if p.is(token.IDENT) {
params = append(params, p.advance().Lit)
p.declareLocal(params[splat])
} else {
params = append(params, "*")
}
defaults = append(defaults, nil)
if !p.accept(token.COMMA) {
break
}
continue
}
name := p.expect(token.IDENT).Lit
params = append(params, name)
// A parameter is not yet in scope while its own default expression is
// parsed (MRI's rule), so a default may call a same-named method —
// `def f(secret = secret("x"))` is the secret() *method*, not a self
// reference. Declare the local only after the default is consumed; earlier
// parameters are already declared and so visible to this default.
if p.accept(token.ASSIGN) {
defaults = append(defaults, p.parseParamDefault())
} else {
defaults = append(defaults, nil)
}
p.declareLocal(name)
if !p.accept(token.COMMA) {
break
}
}
if paren {
p.skipNewlines() // tolerate a newline before the close paren
}
return params, defaults, prepends, splat, kwParams, kwRest, blockParam, forward
}
// parseDestructureParam parses a parenthesised destructuring positional
// parameter — `(a, b)`, `((a, b), c)`, `(a, *b, c)` — returning a MultiAssign
// that unpacks a synthetic positional ("(0)", "(1)", …) into the named locals,
// to be prepended to the method body. It mirrors the block-destructuring shape
// in parseBlockParams and supports one level of further nesting.
func (p *Parser) parseDestructureParam(group *int) (outer *ast.MultiAssign, chained []*ast.MultiAssign) {
p.expect(token.LPAREN)
var names []string
gsplat := -1
for {
switch {
case p.accept(token.STAR):
gsplat = len(names)
if p.is(token.IDENT) {
n := p.advance().Lit
names = append(names, n)
p.declareLocal(n)
} else {
names = append(names, "*")
}
case p.is(token.LPAREN):
// A further-nested destructure: this outer unpack binds it into its own
// synthetic local; a chained MultiAssign then unpacks that synthetic.
inner, innerChained := p.parseDestructureParam(group)
names = append(names, inner.Values[0].(*ast.VarRef).Name)
chained = append(chained, inner)
chained = append(chained, innerChained...)
default:
n := p.expect(token.IDENT).Lit
names = append(names, n)
p.declareLocal(n)
}
if !p.accept(token.COMMA) {
break
}
}
p.expect(token.RPAREN)
syn := "(" + strconv.Itoa(*group) + ")"
*group++
p.declareLocal(syn)
return &ast.MultiAssign{Names: names, SplatIndex: gsplat, Values: []ast.Node{&ast.VarRef{Name: syn}}}, chained
}
// parseParamDefault parses a parameter's default-value expression with masgn
// detection suppressed, so the comma that separates parameters is not mistaken
// for a multiple-assignment target separator (`def f(a = c(1), b = nil)`).
func (p *Parser) parseParamDefault() ast.Node {
saved := p.noMasgn
p.noMasgn = true
def := p.parseExprOrAssign()
p.noMasgn = saved
return def
}
// parseKwArgValue parses the value of a `key: value` keyword argument with masgn
// detection suppressed, so an assignment value does not swallow the comma before
// the next argument (`f(a: x = 1, b: y = 2)` is two pairs, not one masgn value).
func (p *Parser) parseKwArgValue() ast.Node {
saved := p.noMasgn
p.noMasgn = true
v := p.parseExprOrAssign()
p.noMasgn = saved
return v
}
// parseCond parses an if/unless/while/until condition, where the low-precedence
// keyword operators `and`/`or`/`not` are permitted (`if a and b`, `while x or
// y`, `unless not done`).
func (p *Parser) parseCond() ast.Node {
// A condition may itself be a one-line pattern match: `if node in Foo[...]`,
// `while x => p`. Wrap the logical expression so a trailing `in`/`=>` pattern
// is consumed here too, not only at statement level. (A `case`'s `in` uses a
// separate path and never reaches parseCond.)
return p.parseOneLineMatch(p.parseKeywordLogical())
}
func (p *Parser) parseIf() ast.Node {
p.expect(token.IF)
cond := p.parseCond()
p.accept(token.THEN)
then := p.parseStatements(ifBodyEnd)
node := &ast.If{Cond: cond, Then: then}
for p.is(token.ELSIF) {
p.advance()
c := p.parseCond()
p.accept(token.THEN)
b := p.parseStatements(ifBodyEnd)
node.Elsifs = append(node.Elsifs, ast.Elsif{Cond: c, Body: b})
}
if p.accept(token.ELSE) {
node.Else = p.parseStatements(bodyEnd)
}
p.expect(token.END)
return node
}
// parseUnless desugars `unless c ... else ... end` to `if !c ... else ... end`.
func (p *Parser) parseUnless() ast.Node {
p.expect(token.UNLESS)
cond := p.parseCond()
p.accept(token.THEN)
then := p.parseStatements(ifBodyEnd)
node := &ast.If{Cond: not(cond), Then: then}
if p.accept(token.ELSE) {
node.Else = p.parseStatements(bodyEnd)
}
p.expect(token.END)
return node
}
func (p *Parser) parseWhile() ast.Node {
p.expect(token.WHILE)
cond := p.parseLoopCond()
p.accept(token.DO)
body := p.parseStatements(bodyEnd)
p.expect(token.END)
return &ast.While{Cond: cond, Body: body}
}
// parseLoopCond parses a while/until condition with `do…end` attachment
// suppressed, so a trailing `do` is the loop's, not a call block's.
func (p *Parser) parseLoopCond() ast.Node {
saved := p.noDo
p.noDo = true
cond := p.parseCond()
p.noDo = saved
return cond
}
// parseUntil desugars `until c ... end` to `while !c ... end`.
func (p *Parser) parseUntil() ast.Node {
p.expect(token.UNTIL)
cond := p.parseLoopCond()
p.accept(token.DO)
body := p.parseStatements(bodyEnd)
p.expect(token.END)
return &ast.While{Cond: not(cond), Body: body}
}
// parseFor parses `for VAR[, VAR…] in ITER [do] ... end`. The loop variables are
// plain names (one or more, comma-separated) that — unlike block parameters —
// are declared in the enclosing scope and outlive the loop, so they are recorded
// as locals here. The iterator expression is parsed with `do…end` attachment
// suppressed so a trailing `do` belongs to the loop, not to a call within it.
func (p *Parser) parseFor() ast.Node {
p.expect(token.FOR)
var vars []string
for {
name := p.expect(token.IDENT).Lit
vars = append(vars, name)
p.declareLocal(name)
if !p.accept(token.COMMA) {
break
}
}
p.expect(token.IN)
iter := p.parseLoopCond()
p.accept(token.DO)
body := p.parseStatements(bodyEnd)
p.expect(token.END)
return &ast.For{Vars: vars, Iter: iter, Body: body}
}
func (p *Parser) parseReturn() ast.Node {
p.expect(token.RETURN)
// A value-less `return` ends at a terminator, a body/block close, or a
// trailing modifier (`return if c`, `return unless c`) — same rule as
// break/next. Without this, `return unless x` would parse `unless x … end`
// as the return value and swallow the matching `end`.
if p.atStatementEnd() {
return &ast.Return{}
}
first := p.parseExprOrAssign()
if !p.is(token.COMMA) {
return &ast.Return{Value: first}
}
// `return a, b, …` returns an array of the values.
elems := []ast.Node{first}
for p.accept(token.COMMA) {
elems = append(elems, p.parseExprOrAssign())
}
return &ast.Return{Value: &ast.ArrayLit{Elems: elems}}
}
// parseAlias parses `alias NewName OldName`. Each name is a method name (a bare
// identifier/constant/keyword/operator) or a symbol, or — for global aliasing —
// a global variable. The two names are separated by whitespace, not a comma.
func (p *Parser) parseAlias() ast.Node {
p.expect(token.ALIAS)
return &ast.Alias{NewName: p.parseFitem(), OldName: p.parseFitem()}
}
// parseUndef parses `undef name [, name…]`, removing the named methods.
func (p *Parser) parseUndef() ast.Node {
p.expect(token.UNDEF)
names := []string{p.parseFitem()}
for p.accept(token.COMMA) {
names = append(names, p.parseFitem())
}
return &ast.Undef{Names: names}
}
// parseFitem reads one method-name item for alias/undef: a symbol (`:foo`,
// `:==`), a global variable (`$x`, alias only), or a bare method name — an
// identifier, a constant, a reserved word, or an operator (`==`, `<=>`, `[]`).
func (p *Parser) parseFitem() string {
switch p.cur().Type {
case token.SYMBOL, token.GVAR:
return p.advance().Lit
}
if name, ok := p.parseDefName(); ok {
return name
}
p.fail("expected a method name")
return ""
}
func (p *Parser) parseBreak() ast.Node {
p.expect(token.BREAK)
if p.atStatementEnd() {