patm 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1adf094454c70d6f4147b4c44e134a0204274e6c
4
- data.tar.gz: 93e09c5195a23d9dfd2ac2e317841c4a6cb80afc
3
+ metadata.gz: a6f5f8bcb3f5f3031bac6f117418b1d3bc917e8a
4
+ data.tar.gz: 56f203d9076b6905894ffb752954c7caa41fcca6
5
5
  SHA512:
6
- metadata.gz: 80ada62d20ae8926cf97f27c120e70896e3f0d916e38058221afa60967b5a6701cb2ca8d1dd35c8807d104f616bddc62bd032f64e27857d67c2821e5a7aae2a9
7
- data.tar.gz: 824f33ef73d6561932e620ca7e657c74ad2767301b32ede0c47677fc4a50010277a603e419b17967273d86320288e9a08788c47595ed3179f332c6d9cddeb09c
6
+ metadata.gz: 7f01fa35d647fea7658029071682a77dea11e7da92968087c383028f13476ba7a7745a7f04093457c1a45beb92bc1328fab4c9feb464f55550ef3df08b38bb40
7
+ data.tar.gz: 9ac471509e902916d52693fc7fe0bac69fb84106b8a539bcbd1f6a6657c34150e6214e73d0555b28c32ac6038bbdfaa7f40073160327ac024a9be16c2d7d1bdd
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  Gemfile.lock
2
+ *.gem
data/README.md CHANGED
@@ -79,6 +79,11 @@ end
79
79
 
80
80
  ## Changes
81
81
 
82
+ ### 0.1.0
83
+
84
+ - Faster matching with pattern compilation
85
+ - Fix StackOverflow bug for `[Patm.or()]`
86
+
82
87
  ### 0.0.1
83
88
 
84
89
  - Initial release
@@ -0,0 +1,99 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'patm.rb')
2
+ require 'benchmark'
3
+
4
+ P = Patm
5
+
6
+ def match_with_case(obj)
7
+ _1 = P._1
8
+ _2 = P._2
9
+ _3 = P._3
10
+ _4 = P._4
11
+ _xs = P::ARRAY_REST
12
+
13
+ case obj
14
+ when m = P.match([:assign, [:var_field, [:@ident, _1, [_2, _3]]], _4])
15
+ :as
16
+ when m = P.match([:massign, _1, _2])
17
+ :mas
18
+ when m = P.match([:var_ref, [:@ident, _1, [_2, _3]]])
19
+ :vr
20
+ when m = P.match([:params, _1, _2, _3, _xs])
21
+ :par
22
+ when m = P.match(P.or(nil, true, false, Numeric, String, Symbol, []))
23
+ :ignore
24
+ else
25
+ :else
26
+ end
27
+ end
28
+
29
+ @ruledef = lambda do|r|
30
+ _1 = P._1
31
+ _2 = P._2
32
+ _3 = P._3
33
+ _4 = P._4
34
+ _xs = P::ARRAY_REST
35
+
36
+ r.on [:assign, [:var_field, [:@ident, _1, [_2, _3]]], _4] do|m|
37
+ :as
38
+ end
39
+ r.on [:massign, _1, _2] do|m|
40
+ :mas
41
+ end
42
+ r.on [:var_ref, [:@ident, _1, [_2, _3]]] do|m|
43
+ :vr
44
+ end
45
+ r.on [:params, _1, _2, _3, _xs] do|m|
46
+ :par
47
+ end
48
+ r.on P.or(nil, true, false, Numeric, String, Symbol, []) do|m|
49
+ :ignore
50
+ end
51
+ r.else do|obj|
52
+ :else
53
+ end
54
+ end
55
+
56
+ @rule = P::Rule.new(&@ruledef)
57
+ @compiled_rule = P::Rule.new(true, &@ruledef)
58
+
59
+ def match_with_rule(obj)
60
+ @rule.apply(obj)
61
+ end
62
+
63
+ def match_with_compiled_rule(obj)
64
+ @compiled_rule.apply(obj)
65
+ end
66
+
67
+
68
+ VALUES = [
69
+ [ [:assign, [:var_field, [:@ident, 10, [20, 30]]], false], :as ],
70
+ [ [:massign, 1, 2], :mas],
71
+ [ [:var_ref, [:@ident, "x", [1, 2]]], :vr ],
72
+ [ [:params, 10, 20, 30, 40, 50, 60, 70, 80], :par ],
73
+ [ nil, :ignore ],
74
+ [ 100, :ignore ],
75
+ [ [1,2,3], :else],
76
+ ]
77
+
78
+
79
+ def bm(&b)
80
+ 10000.times do
81
+ VALUES.each do|obj, expected|
82
+ actual = b.call obj
83
+ raise "e:#{expected.inspect} a:#{actual.inspect}" unless actual == expected
84
+ end
85
+ end
86
+ end
87
+
88
+ Benchmark.bm(15) do|b|
89
+ b.report("case-when") { bm {|obj| match_with_case(obj) } }
90
+ b.report("rule") { bm {|obj| match_with_rule(obj) } }
91
+ b.report("compiled-rule") { bm {|obj| match_with_compiled_rule(obj) } }
92
+ end
93
+
94
+
95
+ # [:assign, [:var_field, [:@ident, _1, [_2, _3]]], _4] do|m|
96
+ # [:massign, _1, _2] do|m|
97
+ # [:var_ref, [:@ident, _1, [_2, _3]]] do|m|
98
+ # [:params, _1, _2, _3, _xs] do|m|
99
+ # p.or(nil, true, false, Numeric, String, Symbol, []) do|m|
data/lib/patm.rb CHANGED
@@ -11,20 +11,67 @@ module Patm
11
11
  when Pattern
12
12
  plain
13
13
  when Array
14
- if plain.last.is_a?(Pattern) && plain.last.rest?
15
- Arr.new(plain[0..-2].map{|p| build_from(p) }, plain.last)
16
- else
17
- Arr.new(plain.map{|p| build_from(p) })
18
- end
14
+ build_from_array(plain)
19
15
  else
20
16
  Obj.new(plain)
21
17
  end
22
18
  end
23
19
 
20
+ def self.build_from_array(array)
21
+ array = array.map{|a| build_from(a)}
22
+ rest_index = array.index(&:rest?)
23
+ if rest_index
24
+ head = array[0...rest_index]
25
+ rest = array[rest_index]
26
+ tail = array[(rest_index+1)..-1]
27
+ Arr.new(head, rest, tail)
28
+ else
29
+ Arr.new(array)
30
+ end
31
+ end
32
+
24
33
  def &(rhs)
25
34
  And.new([self, rhs])
26
35
  end
27
36
 
37
+ def compile
38
+ src, context, _ = self.compile_internal(0)
39
+
40
+ Compiled.new(self.inspect, src, context)
41
+ end
42
+
43
+ # free_index:Numeric -> [src, context, free_index]
44
+ # variables: _ctx, _match, _obj
45
+ def compile_internal(free_index, target_name = "_obj")
46
+ [
47
+ "_ctx[#{free_index}].execute(_match, #{target_name})",
48
+ [self],
49
+ free_index + 1
50
+ ]
51
+ end
52
+
53
+ class Compiled < self
54
+ def initialize(desc, src, context)
55
+ @desc = desc
56
+ @context = context
57
+ singleton_class = class <<self; self; end
58
+ @src = <<-RUBY
59
+ def execute(_match, _obj)
60
+ _ctx = @context
61
+ #{src}
62
+ end
63
+ RUBY
64
+ singleton_class.class_eval(@src)
65
+ end
66
+
67
+ attr_reader :src
68
+
69
+ def compile_internal(free_index, target_name = "_obj")
70
+ raise "Already compiled"
71
+ end
72
+ def inspect; "<compiled>#{@desc}"; end
73
+ end
74
+
28
75
  class Arr < self
29
76
  def initialize(head, rest = nil, tail = [])
30
77
  @head = head
@@ -45,7 +92,55 @@ module Patm
45
92
  (!@rest || @rest.execute(mmatch, obj[@head.size..-(@tail.size+1)]))
46
93
  end
47
94
 
48
- def inspect; (@head + [@rest] + @tail).inspect; end
95
+ def inspect
96
+ if @rest
97
+ (@head + [@rest] + @tail).inspect
98
+ else
99
+ (@head + @tail).inspect
100
+ end
101
+ end
102
+
103
+ def compile_internal(free_index, target_name = "_obj")
104
+ i = free_index
105
+ srcs = []
106
+ ctxs = []
107
+
108
+ srcs << "#{target_name}.is_a?(::Array)"
109
+
110
+ size_min = @head.size + @tail.size
111
+ if @rest
112
+ srcs << "#{target_name}.size >= #{size_min}"
113
+ else
114
+ srcs << "#{target_name}.size == #{size_min}"
115
+ end
116
+
117
+ elm_target_name = "#{target_name}_elm"
118
+ @head.each_with_index do|h, hi|
119
+ s, c, i = h.compile_internal(i, elm_target_name)
120
+ srcs << "(#{elm_target_name} = #{target_name}[#{hi}]; #{s})"
121
+ ctxs << c
122
+ end
123
+
124
+ srcs << "(#{target_name}_t = #{target_name}[(-#{@tail.size})..-1]; true)"
125
+ @tail.each_with_index do|t, ti|
126
+ s, c, i = t.compile_internal(i, elm_target_name)
127
+ srcs << "(#{elm_target_name} = #{target_name}_t[#{ti}]; #{s})"
128
+ ctxs << c
129
+ end
130
+
131
+ if @rest
132
+ tname = "#{target_name}_r"
133
+ s, c, i = @rest.compile_internal(i, tname)
134
+ srcs << "(#{tname} = #{target_name}[#{@head.size}..-(#{@tail.size+1})];#{s})"
135
+ ctxs << c
136
+ end
137
+
138
+ [
139
+ srcs.map{|s| "(#{s})"}.join(" &&\n"),
140
+ ctxs.flatten(1),
141
+ i
142
+ ]
143
+ end
49
144
  end
50
145
 
51
146
  class ArrRest < self
@@ -56,6 +151,13 @@ module Patm
56
151
  true
57
152
  end
58
153
  def inspect; "..."; end
154
+ def compile_internal(free_index, target_name = "_obj")
155
+ [
156
+ "true",
157
+ [],
158
+ free_index
159
+ ]
160
+ end
59
161
  end
60
162
 
61
163
  class Obj < self
@@ -70,11 +172,26 @@ module Patm
70
172
  def inspect
71
173
  "OBJ(#{@obj.inspect})"
72
174
  end
175
+
176
+ def compile_internal(free_index, target_name = "_obj")
177
+ [
178
+ "_ctx[#{free_index}] === #{target_name}",
179
+ [@obj],
180
+ free_index + 1,
181
+ ]
182
+ end
73
183
  end
74
184
 
75
185
  class Any < self
76
186
  def execute(match, obj); true; end
77
187
  def inspect; 'ANY'; end
188
+ def compile_internal(free_index, target_name = "_obj")
189
+ [
190
+ "true",
191
+ [],
192
+ free_index
193
+ ]
194
+ end
78
195
  end
79
196
 
80
197
  class Group < self
@@ -87,11 +204,41 @@ module Patm
87
204
  true
88
205
  end
89
206
  def inspect; "GROUP(#{@index})"; end
207
+ def compile_internal(free_index, target_name = "_obj")
208
+ [
209
+ "_match[#{@index}] = #{target_name}; true",
210
+ [],
211
+ free_index
212
+ ]
213
+ end
90
214
  end
91
215
 
92
- class Or < self
93
- def initialize(pats)
216
+ class LogicalOp < self
217
+ def initialize(pats, op_str)
94
218
  @pats = pats
219
+ @op_str = op_str
220
+ end
221
+ def compile_internal(free_index, target_name = "_obj")
222
+ srcs = []
223
+ i = free_index
224
+ ctxs = []
225
+ @pats.each do|pat|
226
+ s, c, i = pat.compile_internal(i, target_name)
227
+ srcs << s
228
+ ctxs << c
229
+ end
230
+
231
+ [
232
+ srcs.map{|s| "(#{s})" }.join(" #{@op_str}\n"),
233
+ ctxs.flatten(1),
234
+ i
235
+ ]
236
+ end
237
+ end
238
+
239
+ class Or < LogicalOp
240
+ def initialize(pats)
241
+ super(pats, '||')
95
242
  end
96
243
  def execute(mmatch, obj)
97
244
  @pats.any? do|pat|
@@ -99,16 +246,16 @@ module Patm
99
246
  end
100
247
  end
101
248
  def rest?
102
- @pats.any?(&rest?)
249
+ @pats.any?(&:rest?)
103
250
  end
104
251
  def inspect
105
252
  "OR(#{@pats.map(&:inspect).join(',')})"
106
253
  end
107
254
  end
108
255
 
109
- class And <self
256
+ class And < LogicalOp
110
257
  def initialize(pats)
111
- @pats = pats
258
+ super(pats, '&&')
112
259
  end
113
260
  def execute(mmatch, obj)
114
261
  @pats.all? do|pat|
@@ -129,18 +276,27 @@ module Patm
129
276
  ARRAY_REST = Pattern::ArrRest.new
130
277
 
131
278
  class Rule
132
- def initialize(&block)
279
+ def initialize(compile = false, &block)
280
+ @compile = compile
133
281
  # { Pattern => Proc }
134
282
  @rules = []
135
283
  block[self]
136
284
  end
137
285
 
138
286
  def on(pat, &block)
139
- @rules << [Pattern.build_from(pat), block]
287
+ if @compile
288
+ @rules << [Pattern.build_from(pat).compile, block]
289
+ else
290
+ @rules << [Pattern.build_from(pat), block]
291
+ end
140
292
  end
141
293
 
142
294
  def else(&block)
143
- @rules << [ANY, lambda {|m,o| block[o] }]
295
+ if @compile
296
+ @rules << [ANY.compile, lambda {|m,o| block[o] }]
297
+ else
298
+ @rules << [ANY, lambda {|m,o| block[o] }]
299
+ end
144
300
  end
145
301
 
146
302
  def apply(obj)
@@ -155,11 +311,12 @@ module Patm
155
311
  end
156
312
 
157
313
  class RuleCache
158
- def initialize
314
+ def initialize(compile = false)
315
+ @compile = compile
159
316
  @rules = {}
160
317
  end
161
318
  def match(rule_name, obj, &rule)
162
- (@rules[rule_name] ||= ::Patm::Rule.new(&rule)).apply(obj)
319
+ (@rules[rule_name] ||= ::Patm::Rule.new(@compile, &rule)).apply(obj)
163
320
  end
164
321
  end
165
322
 
@@ -206,6 +363,7 @@ module Patm
206
363
  end
207
364
 
208
365
  def self.match_array(head, rest_spat = nil, tail = [])
366
+ # TODO: deprecated
209
367
  Match.new(
210
368
  Pattern::Arr.new(
211
369
  head.map{|e| Pattern.build_from(e)},
data/patm.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.platform = Gem::Platform::RUBY
3
3
  s.name = 'patm'
4
- s.version = '0.0.1'
4
+ s.version = '0.1.0'
5
5
  s.summary = 'PATtern Matching library'
6
6
  s.description = 'Pattern matching library for plain data structure'
7
7
  s.required_ruby_version = '>= 1.9.0'
data/spec/patm_spec.rb CHANGED
@@ -22,16 +22,16 @@ module PatmHelper
22
22
 
23
23
  def match; @match; end
24
24
 
25
- def and_capture(g1, g2 = nil, g3 = nil)
25
+ def and_capture(g1, g2 = nil, g3 = nil, g4 = nil)
26
26
  these_matches(
27
- self, _capture(self, g1, g2, g3)
27
+ self, _capture(self, g1, g2, g3, g4)
28
28
  )
29
29
  end
30
30
  end
31
31
 
32
- matcher :_capture do|m, g1, g2, g3|
32
+ matcher :_capture do|m, g1, g2, g3, g4|
33
33
  match do|_|
34
- [m.match[1], m.match[2], m.match[3]] == [g1, g2, g3]
34
+ [m.match[1], m.match[2], m.match[3], m.match[4]] == [g1, g2, g3, g4]
35
35
  end
36
36
  end
37
37
 
@@ -60,6 +60,17 @@ describe "Usage:" do
60
60
  r.apply([1, 2, 3]).should == [2, 3]
61
61
  end
62
62
 
63
+ it 'with predefined Rule(compiled)' do
64
+ p = Patm
65
+ r = p::Rule.new(true) do|r|
66
+ r.on [1, p._1, p._2] do|m|
67
+ [m._1, m._2]
68
+ end
69
+ r.else {|obj| [] }
70
+ end
71
+ r.apply([1, 2, 3]).should == [2, 3]
72
+ end
73
+
63
74
  it 'with RuleCache' do
64
75
  p = Patm
65
76
  rs = p::RuleCache.new
@@ -74,6 +85,21 @@ describe "Usage:" do
74
85
 
75
86
  rs.match(:pattern_1, [1, 3, 5]) {|r| fail "should not reach here" }.should == [3, 5]
76
87
  end
88
+
89
+ it 'with RuleCache(compiled)' do
90
+ p = Patm
91
+ rs = p::RuleCache.new(true)
92
+
93
+ rs.match(:pattern_1, [1, 2, 3]) do|r|
94
+ r.on [1, p._1, p._2] do|m|
95
+ [m._1, m._2]
96
+ end
97
+ r.else {|obj| [] }
98
+ end
99
+ .should == [2, 3]
100
+
101
+ rs.match(:pattern_1, [1, 3, 5]) {|r| fail "should not reach here" }.should == [3, 5]
102
+ end
77
103
  end
78
104
 
79
105
  describe Patm::Pattern do
@@ -83,6 +109,10 @@ describe Patm::Pattern do
83
109
  subject { Patm::Pattern.build_from(plain) }
84
110
  instance_eval(&b)
85
111
  end
112
+ context "pattern '#{plain.inspect}'(Compiled)" do
113
+ subject { Patm::Pattern.build_from(plain).compile }
114
+ instance_eval(&b)
115
+ end
86
116
  end
87
117
 
88
118
  pattern 1 do
@@ -125,6 +155,10 @@ describe Patm::Pattern do
125
155
  it { should match_to('x').and_capture('x') }
126
156
  end
127
157
 
158
+ pattern Patm._1 & Patm._2 do
159
+ it { should match_to(1).and_capture(1, 1) }
160
+ end
161
+
128
162
  pattern [0, Patm._1, Patm._2] do
129
163
  it { should match_to([0, 1, 2]).and_capture(1, 2) }
130
164
  it { should_not match_to(['x', 1, 2]).and_capture(1, 2) }
@@ -140,4 +174,25 @@ describe Patm::Pattern do
140
174
  it { should match_to([0, 1]).and_capture([]) }
141
175
  it { should match_to([0, 1, 2, 3]).and_capture([2, 3]) }
142
176
  end
177
+
178
+ pattern [0, 1, Patm::ARRAY_REST, 2] do
179
+ it { should match_to([0,1,2]) }
180
+ it { should match_to([0,1,10,20,30,2]) }
181
+ it { should_not match_to([0,1]) }
182
+ end
183
+
184
+ pattern [0, [1, 2]] do
185
+ it { should match_to [0, [1, 2]] }
186
+ it { should_not match_to [0, [1, 3]] }
187
+ end
188
+
189
+ context 'regression' do
190
+ pattern [:assign, [:var_field, [:@ident, Patm._1, [Patm._2, Patm._3]]], Patm._4] do
191
+ it { should match_to([:assign, [:var_field, [:@ident, 10, [20, 30]]], false]).and_capture(10, 20, 30, false) }
192
+ end
193
+ pattern [Patm.or(1, 2)] do
194
+ it { should match_to [1] }
195
+ it { should match_to [2] }
196
+ end
197
+ end
143
198
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - todesking
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-24 00:00:00.000000000 Z
11
+ date: 2014-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -48,6 +48,7 @@ files:
48
48
  - Gemfile
49
49
  - LICENSE
50
50
  - README.md
51
+ - benchmark/benchmark.rb
51
52
  - lib/patm.rb
52
53
  - patm.gemspec
53
54
  - spec/patm_spec.rb
@@ -71,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
72
  version: '0'
72
73
  requirements: []
73
74
  rubyforge_project:
74
- rubygems_version: 2.2.2
75
+ rubygems_version: 2.0.14
75
76
  signing_key:
76
77
  specification_version: 4
77
78
  summary: PATtern Matching library