delorean_lang 1.1.0 → 2.0.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
  SHA256:
3
- metadata.gz: 9255b38f00277f412eb2a42699d03646470199d261d6cc66298e9e55aa8bd7c0
4
- data.tar.gz: 3ae838764cd7a0f7355d1586b60112b1e073feb367d4b513a239a495b63940ca
3
+ metadata.gz: e0116cc5a84f3c0e7c2efc68c31862529ce3172f7bc7a8f1ee61c8804de3d6da
4
+ data.tar.gz: d29819d73fd26cd4057b6badd1eb831c2c86c095683f14039b61d1165f13fd8b
5
5
  SHA512:
6
- metadata.gz: 3d05eb327cd542977ff5eb795cb57326bd2daed68b6d599e62365adf6a0d828df2bcc7514095605a9318ce281d7dcf6bfb636b1dbdae6055708b2cb4c32835ff
7
- data.tar.gz: b315ab0405e667e6484b2a109b435f0a53a403fec92baa41d483be8b1978477002a82930d8457c1819c076f37f009494c80c313867cabed1af2f3e307c10f65a
6
+ metadata.gz: c448503aec6a6929311129849be0223d5ab0dbed702c22e67ab72fd5ad6c40cb71f60b169299e57795a61731505d458b474a040995790fc948ee50ef183dd318
7
+ data.tar.gz: 56f0224391bc1d1401033c0301297ccb8fb993180196d827c07b5a5f986d19c5f5cf1d051f42af8206e280a6fb0840ee8c434b5fcfe21d3f0fcf26581837f555
@@ -1,6 +1,7 @@
1
- image: thepry/docker-ruby-ci:2.4.2-latest
1
+ image: ruby:2.6.3-buster
2
2
 
3
3
  before_script:
4
+ - gem install bundler:2.0.2
4
5
  - bundle install --jobs $(nproc) --path vendor "${FLAGS[@]}"
5
6
 
6
7
  .base-test:
@@ -28,7 +29,7 @@ cache:
28
29
 
29
30
  variables:
30
31
  GIT_SSL_NO_VERIFY: "true"
31
- BUNDLER_VERSION: "2.0.1"
32
+ BUNDLER_VERSION: "2.0.2"
32
33
  POSTGRES_USER: "runner"
33
34
  POSTGRES_PASSWORD: ""
34
35
  RAILS_ENV: "test"
@@ -42,7 +42,7 @@ Metrics/AbcSize:
42
42
  # Offense count: 2
43
43
  # Configuration parameters: CountComments.
44
44
  Metrics/ClassLength:
45
- Max: 266
45
+ Max: 300
46
46
 
47
47
  # Offense count: 6
48
48
  Metrics/CyclomaticComplexity:
data/Gemfile CHANGED
@@ -4,3 +4,9 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in delorean.gemspec
6
6
  gemspec
7
+
8
+ group :development, :test do
9
+ gem 'benchmark-ips'
10
+ gem 'pry'
11
+ gem 'rspec-instafail', require: false
12
+ end
@@ -378,7 +378,7 @@ module Delorean
378
378
  raise "bad node '#{node}'" unless node =~ /^[A-Z][a-zA-Z0-9_]*$/
379
379
 
380
380
  begin
381
- klass = @m.module_eval(node)
381
+ klass = @m.const_get(node)
382
382
  rescue NameError
383
383
  err(UndefinedNodeError, "node #{node} is undefined")
384
384
  end
@@ -386,15 +386,17 @@ module Delorean
386
386
 
387
387
  params[:_engine] = self
388
388
 
389
- type_arr = attrs.is_a?(Array)
390
- attrs = [attrs] unless type_arr
389
+ if attrs.is_a?(Array)
390
+ attrs.map do |attr|
391
+ raise "bad attribute '#{attr}'" unless attr =~ /^[a-z][A-Za-z0-9_]*$/
391
392
 
392
- res = attrs.map do |attr|
393
- raise "bad attribute '#{attr}'" unless attr =~ /^[a-z][A-Za-z0-9_]*$/
393
+ klass.send("#{attr}#{POST}".to_sym, params)
394
+ end
395
+ else
396
+ raise "bad attribute '#{attrs}'" unless attrs =~ /^[a-z][A-Za-z0-9_]*$/
394
397
 
395
- klass.send("#{attr}#{POST}".to_sym, params)
398
+ klass.send("#{attrs}#{POST}".to_sym, params)
396
399
  end
397
- type_arr ? res : res[0]
398
400
  end
399
401
 
400
402
  def eval_to_hash(node, attrs, params = {})
@@ -748,9 +748,63 @@ eos
748
748
 
749
749
  def rewrite(context)
750
750
  return '{}' unless defined?(args)
751
+ return rewrite_with_literal(context) if can_be_literal?
752
+ return rewrite_with_splat_optimisation(context) if can_optimise_splat?
751
753
 
752
754
  var = "_h#{context.hcount}"
753
- "(#{var}={}; " + args.rewrite(context, var) + "; #{var})"
755
+ "(#{var}={}; #{args.rewrite(context, var)}; #{var})"
756
+ end
757
+
758
+ private
759
+
760
+ def rewrite_with_literal(context)
761
+ "{#{args.rewrite_with_literal(context)}}"
762
+ end
763
+
764
+ # By default { **hash1, **hash2 } is compiled to something like
765
+ # `h1 = {}; h1.merge!(hash1); h1.merge!(hash2)`.
766
+ # In cases when hash starts with two splats, we can skip empty hash
767
+ # assignment and compile it to `h1 = hash1.merge(hash2)`
768
+ # This would work faster in ruby 2.6+
769
+ def rewrite_with_splat_optimisation(context)
770
+ var = "_h#{context.hcount}"
771
+ args2 = args.args_rest.al
772
+
773
+ arg1_str = args.e0.rewrite(context)
774
+ arg2_str = args2.e0.rewrite(context)
775
+
776
+ args_rest = args.args_rest.al.args_rest
777
+
778
+ unless defined?(args_rest.al) && !args_rest.al.text_value.empty?
779
+ return "(#{var} = #{arg1_str}.merge(#{arg2_str}); #{var})"
780
+ end
781
+
782
+ args_rest_str = args_rest.al.rewrite(context, var)
783
+
784
+ "(#{var} = #{arg1_str}.merge(#{arg2_str}); #{args_rest_str} #{var})"
785
+ end
786
+
787
+ def can_be_literal?
788
+ return false if args.conditions?
789
+ return false if args.splats?
790
+
791
+ true
792
+ end
793
+
794
+ def can_optimise_splat?
795
+ # First argument must be splat without condition
796
+ return false unless args.splat?
797
+ return false if args.condition?
798
+
799
+ # Second argument must be splat without condition
800
+ return false unless defined?(args.args_rest.al)
801
+
802
+ args_rest = args.args_rest.al
803
+ return false unless args_rest.text_value.present?
804
+ return false unless args_rest.splat?
805
+ return false if args_rest.condition?
806
+
807
+ true
754
808
  end
755
809
  end
756
810
 
@@ -795,16 +849,48 @@ eos
795
849
  end
796
850
 
797
851
  def rewrite(context, var)
798
- res = if defined?(splat)
852
+ res = if splat?
799
853
  "#{var}.merge!(#{e0.rewrite(context)})"
800
854
  else
801
855
  "#{var}[#{e0.rewrite(context)}]=(#{e1.rewrite(context)})"
802
856
  end
803
- res += " if (#{ifexp.e3.rewrite(context)})" if defined?(ifexp.e3)
857
+ res += " if (#{ifexp.e3.rewrite(context)})" if condition?
804
858
  res += ';'
805
859
  res += args_rest.al.rewrite(context, var) if
806
860
  defined?(args_rest.al) && !args_rest.al.text_value.empty?
807
861
  res
808
862
  end
863
+
864
+ def rewrite_with_literal(context)
865
+ res = "#{e0.rewrite(context)} => #{e1.rewrite(context)},"
866
+ res += args_rest.al.rewrite_with_literal(context) if
867
+ defined?(args_rest.al) && !args_rest.al.text_value.empty?
868
+
869
+ res
870
+ end
871
+
872
+ def splat?
873
+ defined?(splat)
874
+ end
875
+
876
+ def condition?
877
+ defined?(ifexp.e3)
878
+ end
879
+
880
+ def conditions?
881
+ return true if condition?
882
+ return false unless defined?(args_rest.al)
883
+ return false if args_rest.al.text_value.empty?
884
+
885
+ args_rest.al.conditions?
886
+ end
887
+
888
+ def splats?
889
+ return true if splat?
890
+ return false unless defined?(args_rest.al)
891
+ return false if args_rest.al.text_value.empty?
892
+
893
+ args_rest.al.splats?
894
+ end
809
895
  end
810
896
  end
@@ -296,6 +296,14 @@ module Delorean
296
296
  method.called_on Array
297
297
  end
298
298
 
299
+ add_method :to_h do |method|
300
+ method.called_on Object
301
+ end
302
+
303
+ add_method :abs do |method|
304
+ method.called_on Numeric
305
+ end
306
+
299
307
  add_method :to_time do |method|
300
308
  (DT_TYPES + [String]).each do |type|
301
309
  method.called_on type
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delorean
4
- VERSION = '1.1.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require 'benchmark/ips'
5
+ require 'pry'
6
+
4
7
  describe 'Delorean' do
5
8
  let(:sset) do
6
9
  TestContainer.new(
@@ -780,6 +780,7 @@ describe 'Delorean' do
780
780
  engine.parse defn('A:',
781
781
  ' a =?',
782
782
  " d = {'a':1, 2:2, **a, **(a+a)}",
783
+ ' c = {**a, **(a+a)}',
783
784
  )
784
785
  end
785
786
 
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require 'benchmark/ips'
5
+ require 'pry'
6
+
7
+ describe 'Delorean' do
8
+ let(:sset) do
9
+ TestContainer.new({})
10
+ end
11
+
12
+ let(:engine) do
13
+ Delorean::Engine.new 'XXX', sset
14
+ end
15
+
16
+ # FIXME: perhpas add more optimization to hash compilation
17
+ xit 'hash splat performance as expected' do
18
+ perf_test = <<-DELOREAN
19
+ A:
20
+ x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
21
+ h = [[k, k.to_s] for k in x.product(x)].to_h
22
+ hh = {**h, "a":1, "b":2, **h, **h, **h, "c":3}
23
+ DELOREAN
24
+
25
+ engine.parse perf_test.gsub(/^ /, '')
26
+
27
+ bm = Benchmark.ips do |x|
28
+ x.report('delorean') { engine.evaluate('A', 'hh') }
29
+
30
+ x.report('ruby') do
31
+ il = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
32
+ h = il.product(il).map { |i| [i, i.to_s] }.to_h
33
+ h.merge('a' => 1, 'b' => 2).merge(h).merge(h).merge(h).merge('c' => 3)
34
+ end
35
+
36
+ x.compare!
37
+ end
38
+
39
+ # get iterations/sec for each report
40
+ h = bm.entries.each_with_object({}) do |e, hh|
41
+ hh[e.label] = e.stats.central_tendency
42
+ end
43
+
44
+ factor = h['ruby'] / h['delorean']
45
+
46
+ # p factor
47
+
48
+ expect(factor).to be < 1.10
49
+ end
50
+
51
+ it 'hash splat performance (2) as expected' do
52
+ perf_test = <<-DELOREAN
53
+ A:
54
+ h =?
55
+ hh = {**h, **h, **h, **h}
56
+ DELOREAN
57
+
58
+ engine.parse perf_test.gsub(/^ /, '')
59
+
60
+ il = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
61
+ h = il.product(il).map { |i| [i, i.to_s] }.to_h
62
+
63
+ bm = Benchmark.ips do |x|
64
+ x.report('delorean') { engine.evaluate('A', 'hh', 'h' => h) }
65
+
66
+ x.report('ruby') do
67
+ h.merge(h).merge(h).merge(h)
68
+ end
69
+
70
+ x.report('ruby!') do
71
+ hh = {}
72
+ 4.times { hh.merge!(h) }
73
+ end
74
+
75
+ x.compare!
76
+ end
77
+
78
+ # get iterations/sec for each report
79
+ h = bm.entries.each_with_object({}) do |e, hh|
80
+ hh[e.label] = e.stats.central_tendency
81
+ end
82
+
83
+ factor = h['ruby!'] / h['delorean']
84
+ # p factor
85
+ expect(factor).to be_within(0.25).of(0.9)
86
+
87
+ # perf of mutable vs immutable hash ops are as expected
88
+ factor = h['ruby!'] / h['ruby']
89
+ # p factor
90
+ expect(factor).to be_within(0.3).of(1.0)
91
+
92
+ factor = h['ruby'] / h['delorean']
93
+ # p factor
94
+ expect(factor).to be_within(0.15).of(1.0)
95
+ end
96
+
97
+ it 'hash literal performance as expected' do
98
+ il = (1..10).to_a
99
+
100
+ hdef1 = il.map { |i| "'#{'xo' * i}' : #{i}" }.join(',')
101
+ hdef2 = il.map { |i| "'#{'yo' * i}' : #{i}" }.join(',')
102
+
103
+ perf_test = <<-DELOREAN
104
+ A:
105
+ v =?
106
+ h = { #{hdef1}, #{hdef2} }
107
+ DELOREAN
108
+
109
+ engine.parse perf_test.gsub(/^ /, '')
110
+
111
+ bm = Benchmark.ips do |x|
112
+ x.report('delorean') { engine.evaluate('A', 'h', {}) }
113
+
114
+ x.report('ruby') do
115
+ {
116
+ 'xo' => 1,
117
+ 'xoxo' => 2,
118
+ 'xoxoxo' => 3,
119
+ 'xoxoxoxo' => 4,
120
+ 'xoxoxoxoxo' => 5,
121
+ 'xoxoxoxoxoxo' => 6,
122
+ 'xoxoxoxoxoxoxo' => 7,
123
+ 'xoxoxoxoxoxoxoxo' => 8,
124
+ 'xoxoxoxoxoxoxoxoxo' => 9,
125
+ 'xoxoxoxoxoxoxoxoxoxo' => 10,
126
+ 'yo' => 1,
127
+ 'yoyo' => 2,
128
+ 'yoyoyo' => 3,
129
+ 'yoyoyoyo' => 4,
130
+ 'yoyoyoyoyo' => 5,
131
+ 'yoyoyoyoyoyo' => 6,
132
+ 'yoyoyoyoyoyoyo' => 7,
133
+ 'yoyoyoyoyoyoyoyo' => 8,
134
+ 'yoyoyoyoyoyoyoyoyo' => 9,
135
+ 'yoyoyoyoyoyoyoyoyoyo' => 10,
136
+ }
137
+ end
138
+
139
+ x.compare!
140
+ end
141
+
142
+ # get iterations/sec for each report
143
+ h = bm.entries.each_with_object({}) do |e, hh|
144
+ hh[e.label] = e.stats.central_tendency
145
+ end
146
+
147
+ factor = h['ruby'] / h['delorean']
148
+ # p factor
149
+
150
+ # FIXME: locally the factor is around 4, but in Gitlab CI it's around 7
151
+ # expect(factor).to be_within(2.5).of(4)
152
+ expect(factor).to be < 8
153
+ end
154
+
155
+ it 'array and node call performance as expected' do
156
+ perf_test = <<-DELOREAN
157
+ A:
158
+ i =? 0
159
+ max =?
160
+ range = if i>max then [] else A(i=i+1, max=max).range + [i]
161
+
162
+ res = [x*2 for x in range]
163
+ result = res.sum
164
+ DELOREAN
165
+
166
+ engine.parse perf_test.gsub(/^ /, '')
167
+
168
+ bm = Benchmark.ips do |x|
169
+ lim = 100
170
+
171
+ x.report('delorean') { engine.evaluate('A', 'result', 'max' => lim) }
172
+
173
+ x.report('ruby') do
174
+ def range(max, counter = 0)
175
+ counter > max ? [] : range(max, counter + 1)
176
+ end
177
+
178
+ r = range(lim)
179
+ r.map { |i| i * 2 }.sum
180
+ end
181
+
182
+ x.compare!
183
+ end
184
+
185
+ # get iterations/sec for each report
186
+ h = bm.entries.each_with_object({}) do |e, hh|
187
+ hh[e.label] = e.stats.central_tendency
188
+ end
189
+
190
+ factor = h['ruby'] / h['delorean']
191
+
192
+ # p factor
193
+
194
+ expect(factor).to be < 135
195
+ end
196
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: delorean_lang
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arman Bostani
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-08 00:00:00.000000000 Z
11
+ date: 2019-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -171,6 +171,7 @@ files:
171
171
  - spec/eval_spec.rb
172
172
  - spec/func_spec.rb
173
173
  - spec/parse_spec.rb
174
+ - spec/perf_spec.rb
174
175
  - spec/ruby/whitelist_spec.rb
175
176
  - spec/spec_helper.rb
176
177
  homepage: https://github.com/arman000/delorean_lang
@@ -192,8 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
193
  - !ruby/object:Gem::Version
193
194
  version: '0'
194
195
  requirements: []
195
- rubyforge_project:
196
- rubygems_version: 2.7.8
196
+ rubygems_version: 3.0.6
197
197
  signing_key:
198
198
  specification_version: 4
199
199
  summary: Delorean compiler
@@ -203,5 +203,6 @@ test_files:
203
203
  - spec/eval_spec.rb
204
204
  - spec/func_spec.rb
205
205
  - spec/parse_spec.rb
206
+ - spec/perf_spec.rb
206
207
  - spec/ruby/whitelist_spec.rb
207
208
  - spec/spec_helper.rb