Skip to content

Commit 297d07b

Browse files
authored
Merge pull request #51 from ruby-syntax-tree/pattern-matching
Pattern matching
2 parents f8abd8e + 0ed62b1 commit 297d07b

File tree

9 files changed

+210
-40
lines changed

9 files changed

+210
-40
lines changed

Gemfile

-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,3 @@
33
source "https://rubygems.org"
44

55
gemspec
6-
7-
gem "benchmark-ips"
8-
gem "parser"
9-
gem "ruby_parser"
10-
gem "stackprof"

Gemfile.lock

-12
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,15 @@ PATH
66
GEM
77
remote: https://rubygems.org/
88
specs:
9-
ast (2.4.2)
10-
benchmark-ips (2.10.0)
119
docile (1.4.0)
1210
minitest (5.15.0)
13-
parser (3.1.2.0)
14-
ast (~> 2.4.1)
1511
rake (13.0.6)
16-
ruby_parser (3.19.1)
17-
sexp_processor (~> 4.16)
18-
sexp_processor (4.16.0)
1912
simplecov (0.21.2)
2013
docile (~> 1.1)
2114
simplecov-html (~> 0.11)
2215
simplecov_json_formatter (~> 0.1)
2316
simplecov-html (0.12.3)
2417
simplecov_json_formatter (0.1.3)
25-
stackprof (0.2.19)
2618

2719
PLATFORMS
2820
arm64-darwin-21
@@ -32,14 +24,10 @@ PLATFORMS
3224
x86_64-linux
3325

3426
DEPENDENCIES
35-
benchmark-ips
3627
bundler
3728
minitest
38-
parser
3929
rake
40-
ruby_parser
4130
simplecov
42-
stackprof
4331
syntax_tree!
4432

4533
BUNDLED WITH

bin/bench

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
require "bundler/setup"
5-
require "benchmark/ips"
4+
require "bundler/inline"
65

7-
require_relative "../lib/syntax_tree"
8-
require "ruby_parser"
9-
require "parser/current"
6+
gemfile do
7+
source "https://rubygems.org"
8+
gem "benchmark-ips"
9+
gem "parser", require: "parser/current"
10+
gem "ruby_parser"
11+
end
12+
13+
$:.unshift(File.expand_path("../lib", __dir__))
14+
require "syntax_tree"
1015

1116
def compare(filepath)
1217
prefix = "#{File.expand_path("..", __dir__)}/"
@@ -30,7 +35,7 @@ filepaths = ARGV
3035
if filepaths.empty?
3136
filepaths = [
3237
File.expand_path("bench", __dir__),
33-
File.expand_path("../lib/syntax_tree.rb", __dir__)
38+
File.expand_path("../lib/syntax_tree/node.rb", __dir__)
3439
]
3540
end
3641

bin/profile

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
require "bundler/setup"
5-
require "stackprof"
4+
require "bundler/inline"
65

7-
filepath = File.expand_path("../lib/syntax_tree", __dir__)
8-
require_relative filepath
6+
gemfile do
7+
source "https://rubygems.org"
8+
gem "stackprof"
9+
end
10+
11+
$:.unshift(File.expand_path("../lib", __dir__))
12+
require "syntax_tree"
913

1014
GC.disable
1115

1216
StackProf.run(mode: :cpu, out: "tmp/profile.dump", raw: true) do
13-
SyntaxTree.format(File.read("#{filepath}.rb"))
17+
filepath = File.expand_path("../lib/syntax_tree/node.rb", __dir__)
18+
SyntaxTree.format(File.read(filepath))
1419
end
1520

1621
GC.enable
1722

18-
`bundle exec stackprof --d3-flamegraph tmp/profile.dump > tmp/flamegraph.html`
19-
puts "open tmp/flamegraph.html"
23+
File.open("tmp/flamegraph.html", "w") do |file|
24+
report = Marshal.load(IO.binread("tmp/profile.dump"))
25+
StackProf::Report.new(report).print_d3_flamegraph(file)
26+
end
27+
28+
`open tmp/flamegraph.html`

lib/syntax_tree/node.rb

+35-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ def to(other)
3535
)
3636
end
3737

38+
def deconstruct
39+
[start_line, start_char, start_column, end_line, end_char, end_column]
40+
end
41+
42+
def deconstruct_keys(keys)
43+
{
44+
start_line: start_line,
45+
start_char: start_char,
46+
start_column: start_column,
47+
end_line: end_line,
48+
end_char: end_char,
49+
end_column: end_column
50+
}
51+
end
52+
3853
def self.token(line:, char:, column:, size:)
3954
new(
4055
start_line: line,
@@ -4334,16 +4349,20 @@ class Heredoc < Node
43344349
# [String] the ending of the heredoc
43354350
attr_reader :ending
43364351

4352+
# [Integer] how far to dedent the heredoc
4353+
attr_reader :dedent
4354+
43374355
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
43384356
# heredoc string literal
43394357
attr_reader :parts
43404358

43414359
# [Array[ Comment | EmbDoc ]] the comments attached to this node
43424360
attr_reader :comments
43434361

4344-
def initialize(beginning:, ending: nil, parts: [], location:, comments: [])
4362+
def initialize(beginning:, ending: nil, dedent: 0, parts: [], location:, comments: [])
43454363
@beginning = beginning
43464364
@ending = ending
4365+
@dedent = dedent
43474366
@parts = parts
43484367
@location = location
43494368
@comments = comments
@@ -4538,7 +4557,12 @@ def format(q)
45384557
parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
45394558

45404559
contents = -> do
4541-
q.seplist(parts) { |part| q.format(part, stackable: false) }
4560+
q.group { q.seplist(parts) { |part| q.format(part, stackable: false) } }
4561+
4562+
# If there isn't a constant, and there's a blank keyword_rest, then we
4563+
# have an plain ** that needs to have a `then` after it in order to
4564+
# parse correctly on the next parse.
4565+
q.text(" then") if !constant && keyword_rest && keyword_rest.value.nil?
45424566
end
45434567

45444568
if constant
@@ -5594,11 +5618,17 @@ class MLHSParen < Node
55945618
# [MLHS | MLHSParen] the contents inside of the parentheses
55955619
attr_reader :contents
55965620

5621+
# [boolean] whether or not there is a trailing comma at the end of this
5622+
# list, which impacts destructuring. It's an attr_accessor so that while
5623+
# the syntax tree is being built it can be set by its parent node
5624+
attr_accessor :comma
5625+
55975626
# [Array[ Comment | EmbDoc ]] the comments attached to this node
55985627
attr_reader :comments
55995628

5600-
def initialize(contents:, location:, comments: [])
5629+
def initialize(contents:, comma: false, location:, comments: [])
56015630
@contents = contents
5631+
@comma = comma
56025632
@location = location
56035633
@comments = comments
56045634
end
@@ -5622,13 +5652,15 @@ def format(q)
56225652

56235653
if parent.is_a?(MAssign) || parent.is_a?(MLHSParen)
56245654
q.format(contents)
5655+
q.text(",") if comma
56255656
else
56265657
q.group(0, "(", ")") do
56275658
q.indent do
56285659
q.breakable("")
56295660
q.format(contents)
56305661
end
56315662

5663+
q.text(",") if comma
56325664
q.breakable("")
56335665
end
56345666
end

lib/syntax_tree/parser.rb

+76-7
Original file line numberDiff line numberDiff line change
@@ -516,12 +516,46 @@ def on_array(contents)
516516
def on_aryptn(constant, requireds, rest, posts)
517517
parts = [constant, *requireds, rest, *posts].compact
518518

519+
# If there aren't any parts (no constant, no positional arguments), then
520+
# we're matching an empty array. In this case, we're going to look for the
521+
# left and right brackets explicitly. Otherwise, we'll just use the bounds
522+
# of the various parts.
523+
location =
524+
if parts.empty?
525+
find_token(LBracket).location.to(find_token(RBracket).location)
526+
else
527+
parts[0].location.to(parts[-1].location)
528+
end
529+
530+
# If there's the optional then keyword, then we'll delete that and use it
531+
# as the end bounds of the location.
532+
if token = find_token(Kw, "then", consume: false)
533+
tokens.delete(token)
534+
location = location.to(token.location)
535+
end
536+
537+
# If there is a plain *, then we're going to fix up the location of it
538+
# here because it currently doesn't have anything to use for its precise
539+
# location. If we hit a comma, then we've gone too far.
540+
if rest.is_a?(VarField) && rest.value.nil?
541+
tokens.rindex do |token|
542+
case token
543+
in Op[value: "*"]
544+
rest = VarField.new(value: nil, location: token.location)
545+
break
546+
in Comma
547+
break
548+
else
549+
end
550+
end
551+
end
552+
519553
AryPtn.new(
520554
constant: constant,
521555
requireds: requireds || [],
522556
rest: rest,
523557
posts: posts || [],
524-
location: parts[0].location.to(parts[-1].location)
558+
location: location
525559
)
526560
end
527561

@@ -1373,15 +1407,35 @@ def on_float(value)
13731407
# VarField right
13741408
# ) -> FndPtn
13751409
def on_fndptn(constant, left, values, right)
1376-
beginning = constant || find_token(LBracket)
1377-
ending = find_token(RBracket)
1410+
# The opening of this find pattern is either going to be a left bracket, a
1411+
# right left parenthesis, or the left splat. We're going to use this to
1412+
# determine how to find the closing of the pattern, as well as determining
1413+
# the location of the node.
1414+
opening =
1415+
find_token(LBracket, consume: false) ||
1416+
find_token(LParen, consume: false) ||
1417+
left
1418+
1419+
# The closing is based on the opening, which is either the matched
1420+
# punctuation or the right splat.
1421+
closing =
1422+
case opening
1423+
in LBracket
1424+
tokens.delete(opening)
1425+
find_token(RBracket)
1426+
in LParen
1427+
tokens.delete(opening)
1428+
find_token(RParen)
1429+
else
1430+
right
1431+
end
13781432

13791433
FndPtn.new(
13801434
constant: constant,
13811435
left: left,
13821436
values: values,
13831437
right: right,
1384-
location: beginning.location.to(ending.location)
1438+
location: (constant || opening).location.to(closing.location)
13851439
)
13861440
end
13871441

@@ -1468,6 +1522,7 @@ def on_heredoc_dedent(string, width)
14681522
@heredocs[-1] = Heredoc.new(
14691523
beginning: heredoc.beginning,
14701524
ending: heredoc.ending,
1525+
dedent: width,
14711526
parts: string.parts,
14721527
location: heredoc.location
14731528
)
@@ -1481,6 +1536,7 @@ def on_heredoc_end(value)
14811536
@heredocs[-1] = Heredoc.new(
14821537
beginning: heredoc.beginning,
14831538
ending: value.chomp,
1539+
dedent: heredoc.dedent,
14841540
parts: heredoc.parts,
14851541
location:
14861542
Location.new(
@@ -1501,12 +1557,23 @@ def on_heredoc_end(value)
15011557
# (nil | VarField) keyword_rest
15021558
# ) -> HshPtn
15031559
def on_hshptn(constant, keywords, keyword_rest)
1560+
# Create an artificial VarField if we find an extra ** on the end
1561+
if !keyword_rest && (token = find_token(Op, "**", consume: false))
1562+
tokens.delete(token)
1563+
keyword_rest = VarField.new(value: nil, location: token.location)
1564+
end
1565+
1566+
# Delete the optional then keyword
1567+
if token = find_token(Kw, "then", consume: false)
1568+
tokens.delete(token)
1569+
end
1570+
15041571
parts = [constant, *keywords&.flatten(1), keyword_rest].compact
15051572
location =
1506-
if parts.empty?
1507-
find_token(LBrace).location.to(find_token(RBrace).location)
1508-
else
1573+
if parts.any?
15091574
parts[0].location.to(parts[-1].location)
1575+
else
1576+
find_token(LBrace).location.to(find_token(RBrace).location)
15101577
end
15111578

15121579
HshPtn.new(
@@ -2638,6 +2705,7 @@ def on_string_literal(string)
26382705
Heredoc.new(
26392706
beginning: heredoc.beginning,
26402707
ending: heredoc.ending,
2708+
dedent: heredoc.dedent,
26412709
parts: string.parts,
26422710
location: heredoc.location
26432711
)
@@ -3190,6 +3258,7 @@ def on_xstring_literal(xstring)
31903258
Heredoc.new(
31913259
beginning: heredoc.beginning,
31923260
ending: heredoc.ending,
3261+
dedent: heredoc.dedent,
31933262
parts: xstring.parts,
31943263
location: heredoc.location
31953264
)

0 commit comments

Comments
 (0)