delorean_lang 2.1.0 → 2.3.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.
@@ -32,6 +32,10 @@ grammar Delorean
32
32
  [A-Z] [a-zA-Z0-9_]* (('::' [A-Z] [a-zA-Z0-9_]*) 2..)
33
33
  end
34
34
 
35
+ rule elsif
36
+ 'elsif' sp? v:expression sp? 'then' sp? e1:expression sp?
37
+ end
38
+
35
39
  rule expression
36
40
  'ERR(' sp? args:fn_args? sp? ')' <ErrorOp>
37
41
  /
@@ -41,6 +45,11 @@ grammar Delorean
41
45
  'then' sp? e1:expression sp?
42
46
  'else' sp? e2:expression <IfElse>
43
47
  /
48
+ 'if' sp? v:expression sp?
49
+ 'then' sp? e1:expression sp?
50
+ sp? elsifs:elsif+ sp?
51
+ 'else' sp? e2:expression <IfElsifElse>
52
+ /
44
53
  v:getattr_exp sp? op:binary_op sp? e:expression <BinOp>
45
54
  /
46
55
  getattr_exp
@@ -138,8 +147,8 @@ grammar Delorean
138
147
  string /
139
148
  boolean /
140
149
  nil_val /
141
- identifier /
142
150
  sup /
151
+ identifier /
143
152
  self /
144
153
  list_expr /
145
154
  set_expr /
@@ -186,7 +195,8 @@ grammar Delorean
186
195
  end
187
196
 
188
197
  rule identifier
189
- [a-z] [a-zA-Z0-9_]* '?'? <Identifier>
198
+ [a-z] [a-zA-Z0-9_]* '?'? <Identifier> /
199
+ [_] [a-zA-Z0-9_]+ '?'? <Identifier>
190
200
  end
191
201
 
192
202
  rule boolean
@@ -258,7 +258,7 @@ module Delorean
258
258
  @line_no += 1
259
259
 
260
260
  # skip comments
261
- next if line =~ /^\s*\#/
261
+ next if /^\s*\#/.match?(line)
262
262
 
263
263
  # remove trailing blanks
264
264
  line.rstrip!
@@ -302,7 +302,7 @@ module Delorean
302
302
  t = parser.parse(line)
303
303
 
304
304
  if !t
305
- err(ParseError, 'syntax error') unless line =~ /^\s+/
305
+ err(ParseError, 'syntax error') unless /^\s+/.match?(line)
306
306
 
307
307
  multi_line = line
308
308
  @multi_no = @line_no
@@ -375,7 +375,7 @@ module Delorean
375
375
  if node.is_a?(Class)
376
376
  klass = node
377
377
  else
378
- raise "bad node '#{node}'" unless node =~ /^[A-Z][a-zA-Z0-9_]*$/
378
+ raise "bad node '#{node}'" unless /^[A-Z][a-zA-Z0-9_]*$/.match?(node)
379
379
 
380
380
  begin
381
381
  klass = @m.const_get(node)
@@ -386,19 +386,58 @@ module Delorean
386
386
 
387
387
  params[:_engine] = self
388
388
 
389
+ if klass.respond_to?(NODE_CACHE_ARG) && klass.send(NODE_CACHE_ARG, params)
390
+ return _evaluate_with_cache(klass, attrs, params)
391
+ end
392
+
389
393
  if attrs.is_a?(Array)
390
394
  attrs.map do |attr|
391
- raise "bad attribute '#{attr}'" unless attr =~ /^[a-z][A-Za-z0-9_]*$/
395
+ unless /^[_a-z][A-Za-z0-9_]*$/.match?(attr)
396
+ raise "bad attribute '#{attr}'"
397
+ end
392
398
 
393
399
  klass.send("#{attr}#{POST}".to_sym, params)
394
400
  end
395
401
  else
396
- raise "bad attribute '#{attrs}'" unless attrs =~ /^[a-z][A-Za-z0-9_]*$/
402
+ unless /^[_a-z][A-Za-z0-9_]*$/.match?(attrs)
403
+ raise "bad attribute '#{attrs}'"
404
+ end
397
405
 
398
406
  klass.send("#{attrs}#{POST}".to_sym, params)
399
407
  end
400
408
  end
401
409
 
410
+ def _evaluate_with_cache(klass, attrs, params)
411
+ if attrs.is_a?(Array)
412
+ attrs.map do |attr|
413
+ unless /^[_a-z][A-Za-z0-9_]*$/.match?(attr)
414
+ raise "bad attribute '#{attr}'"
415
+ end
416
+
417
+ _evaluate_attr_with_cache(klass, attr, params)
418
+ end
419
+ else
420
+ unless /^[_a-z][A-Za-z0-9_]*$/.match?(attrs)
421
+ raise "bad attribute '#{attrs}'"
422
+ end
423
+
424
+ _evaluate_attr_with_cache(klass, attrs, params)
425
+ end
426
+ end
427
+
428
+ def _evaluate_attr_with_cache(klass, attr, params)
429
+ params_without_engine = params.reject { |k, _| k == :_engine }
430
+
431
+ ::Delorean::Cache.with_cache(
432
+ klass: klass,
433
+ method: attr,
434
+ mutable_params: params,
435
+ params: params_without_engine
436
+ ) do
437
+ klass.send("#{attr}#{POST}".to_sym, params)
438
+ end
439
+ end
440
+
402
441
  def eval_to_hash(node, attrs, params = {})
403
442
  res = evaluate(node, attrs, params)
404
443
  Hash[* attrs.zip(res).flatten(1)]
@@ -318,7 +318,9 @@ eos
318
318
  class IString < Literal
319
319
  def rewrite(_context)
320
320
  # FIXME: hacky to just fail
321
- raise 'String interpolation not supported' if text_value =~ /\#\{.*\}/
321
+ if /\#\{.*\}/.match?(text_value)
322
+ raise 'String interpolation not supported'
323
+ end
322
324
 
323
325
  # FIXME: syntax check?
324
326
  text_value
@@ -388,7 +390,7 @@ eos
388
390
 
389
391
  def rewrite(_context, vcode)
390
392
  attr = i.text_value
391
- attr = "'#{attr}'" unless attr =~ /\A[0-9]+\z/
393
+ attr = "'#{attr}'" unless /\A[0-9]+\z/.match?(attr)
392
394
  "_get_attr(#{vcode}, #{attr}, _e)"
393
395
  end
394
396
  end
@@ -626,6 +628,34 @@ eos
626
628
  end
627
629
  end
628
630
 
631
+ class IfElsifElse < SNode
632
+ def check(context, *)
633
+ vc = v.check(context)
634
+ e1c = e1.check(context)
635
+ e2c = e2.check(context)
636
+
637
+ elsifs_check = elsifs.elements.map do |node|
638
+ [node.v.check(context), node.e1.check(context)]
639
+ end.flatten
640
+
641
+ vc + e1c + e2c + elsifs_check
642
+ end
643
+
644
+ def rewrite(context)
645
+ elsifs_string = elsifs.elements.map do |node|
646
+ "elsif (#{node.v.rewrite(context)})
647
+ (#{node.e1.rewrite(context)})"
648
+ end.join("\n")
649
+
650
+ "if (#{v.rewrite(context)})
651
+ (#{e1.rewrite(context)})
652
+ #{elsifs_string}
653
+ else
654
+ (#{e2.rewrite(context)})
655
+ end"
656
+ end
657
+ end
658
+
629
659
  class ListExpr < SNode
630
660
  def check(context, *)
631
661
  defined?(args) ? args.check(context) : []
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delorean
4
- VERSION = '2.1.0'
4
+ VERSION = '2.3.0'
5
5
  end
@@ -211,6 +211,46 @@ describe 'Delorean' do
211
211
  expect(engine.evaluate('A', 'c')).to eq([[1, 2, 3], ['a', 'b']])
212
212
  end
213
213
 
214
+ it 'should handle if else' do
215
+ engine.parse defn('A:',
216
+ ' n =?',
217
+ ' fact = if n <= 1 then 1',
218
+ ' else n'
219
+ )
220
+
221
+ expect(engine.evaluate('A', 'fact', 'n' => 0)).to eq(1)
222
+ expect(engine.evaluate('A', 'fact', 'n' => 10)).to eq(10)
223
+ end
224
+
225
+ it 'should handle elsif 1' do
226
+ engine.parse defn('A:',
227
+ ' n =?',
228
+ ' fact = if n <= 1 then 1',
229
+ ' elsif n < 7 then 7',
230
+ ' else n'
231
+ )
232
+
233
+ expect(engine.evaluate('A', 'fact', 'n' => 0)).to eq(1)
234
+ expect(engine.evaluate('A', 'fact', 'n' => 5)).to eq(7)
235
+ expect(engine.evaluate('A', 'fact', 'n' => 10)).to eq(10)
236
+ end
237
+
238
+ it 'should handle elsif 2' do
239
+ engine.parse defn('A:',
240
+ ' n =?',
241
+ ' m = 2',
242
+ ' fact = if n <= 1 then 1',
243
+ ' elsif n < 3 then 3',
244
+ ' elsif (n < 7 && (m + Dummy.call_me_maybe(n)) > 1) then 7',
245
+ ' else n'
246
+ )
247
+
248
+ expect(engine.evaluate('A', 'fact', 'n' => 0)).to eq(1)
249
+ expect(engine.evaluate('A', 'fact', 'n' => 2)).to eq(3)
250
+ expect(engine.evaluate('A', 'fact', 'n' => 5)).to eq(7)
251
+ expect(engine.evaluate('A', 'fact', 'n' => 10)).to eq(10)
252
+ end
253
+
214
254
  it 'should handle operator precedence properly' do
215
255
  engine.parse defn('A:',
216
256
  ' b = 3+2*4-1',
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe 'Node level caching' do
6
+ let(:sset) do
7
+ TestContainer.new(
8
+ 'AAA' =>
9
+ defn('X:',
10
+ ' a =? 123',
11
+ ' b = a*2',
12
+ )
13
+ )
14
+ end
15
+
16
+ let(:engine) do
17
+ eng = Delorean::Engine.new 'XXX', sset
18
+
19
+ eng.parse defn('A:',
20
+ ' _cache = true',
21
+ ' arg1 =?',
22
+ ' arg2 =?',
23
+ ' arg3 =?',
24
+ ' result = Dummy.all_of_me()',
25
+ 'B:',
26
+ ' arg1 =?',
27
+ ' arg2 =?',
28
+ ' arg3 =?',
29
+ ' result = A(arg1=arg1, arg2=arg2, arg3=arg3).result',
30
+ )
31
+ eng
32
+ end
33
+
34
+ after do
35
+ default_callback = ::Delorean::Cache::NODE_CACHE_DEFAULT_CALLBACK
36
+
37
+ ::Delorean::Cache.node_cache_callback = default_callback
38
+ ::Delorean::Cache.adapter.clear_all!
39
+ end
40
+
41
+ def evaluate_b
42
+ r = engine.evaluate('B', 'result', 'arg1' => 1, 'arg2' => 2, 'arg3' => 3)
43
+ expect(r).to eq([{ 'name' => 'hello', 'foo' => 'bar' }])
44
+ end
45
+
46
+ def evaluate_a
47
+ r = engine.evaluate('A', 'result', 'arg1' => 1, 'arg2' => 2, 'arg3' => 3)
48
+ expect(r).to eq([{ 'name' => 'hello', 'foo' => 'bar' }])
49
+ end
50
+
51
+ it 'uses cache when the same arguments were passed' do
52
+ expect(Dummy).to receive(:all_of_me).once.and_call_original
53
+ 2.times { evaluate_b }
54
+ end
55
+
56
+ it 'uses cache if evaluate is called from ruby' do
57
+ expect(Dummy).to receive(:all_of_me).once.and_call_original
58
+ 2.times { evaluate_a }
59
+ end
60
+
61
+ it "doesn't use cache when the different arguments were passed" do
62
+ expect(Dummy).to receive(:all_of_me).twice.and_call_original
63
+
64
+ evaluate_a
65
+
66
+ r = engine.evaluate('A', 'result', 'arg1' => 10, 'arg2' => 2, 'arg3' => 3)
67
+ expect(r).to eq([{ 'name' => 'hello', 'foo' => 'bar' }])
68
+ end
69
+
70
+ describe 'compex caching' do
71
+ let(:engine) do
72
+ eng = Delorean::Engine.new 'XXX', sset
73
+
74
+ eng.parse defn('A:',
75
+ ' _cache = true',
76
+ ' arg1 =?',
77
+ ' arg2 =?',
78
+ ' arg3 =?',
79
+ ' result = Dummy.all_of_me()',
80
+ 'B:',
81
+ ' _cache = true',
82
+ ' arg1 =?',
83
+ ' arg2 =?',
84
+ ' arg3 =?',
85
+ ' result = [A(arg1=1, arg2=2, arg3=3).result, Dummy.one_or_two(1, 2)]',
86
+ 'C:',
87
+ ' arg1 =?',
88
+ ' result = [ Dummy.call_me_maybe(2), B(arg1=arg1, arg2=2, arg3=3).result]',
89
+ )
90
+ eng
91
+ end
92
+
93
+ it 'Calling node can be cached as well' do
94
+ # A should be called once
95
+ expect(Dummy).to receive(:all_of_me).once.and_call_original
96
+
97
+ # B should be called twice
98
+ expect(Dummy).to receive(:one_or_two).twice.and_call_original
99
+
100
+ # C should be called 3 times
101
+ expect(Dummy).to receive(:call_me_maybe).exactly(3).times
102
+ .and_call_original
103
+
104
+ 2.times do
105
+ r = engine.evaluate('C', 'result', 'arg1' => 1)
106
+ expect(r).to eq([2, [[{ 'name' => 'hello', 'foo' => 'bar' }], [1, 2]]])
107
+ end
108
+
109
+ r = engine.evaluate('C', 'result', 'arg1' => 2)
110
+ expect(r).to eq([2, [[{ 'name' => 'hello', 'foo' => 'bar' }], [1, 2]]])
111
+ end
112
+ end
113
+
114
+ it 'allows to override caching callback 1' do
115
+ ::Delorean::Cache.node_cache_callback = lambda do |**_kwargs|
116
+ {
117
+ cache: false,
118
+ }
119
+ end
120
+
121
+ expect(Dummy).to receive(:all_of_me).twice.and_call_original
122
+ 2.times { evaluate_a }
123
+ end
124
+
125
+ it 'allows to override caching callback 2' do
126
+ ::Delorean::Cache.node_cache_callback = lambda do |**_kwargs|
127
+ {
128
+ cache: true,
129
+ }
130
+ end
131
+
132
+ expect(Dummy).to receive(:all_of_me).once.and_call_original
133
+ 2.times { evaluate_a }
134
+ end
135
+ end
@@ -112,7 +112,7 @@ describe 'Delorean' do
112
112
 
113
113
  expect do
114
114
  engine.parse defn('A:',
115
- ' _b = 1',
115
+ ' %b = 1',
116
116
  )
117
117
  end.to raise_error(Delorean::ParseError)
118
118
  end
@@ -167,6 +167,25 @@ describe 'Delorean' do
167
167
  )
168
168
  end
169
169
 
170
+ it 'should allow elsif 1' do
171
+ engine.parse defn('A:',
172
+ ' n =?',
173
+ ' fact = if n <= 1 then 1',
174
+ ' elsif n < 3 then 3',
175
+ ' else n'
176
+ )
177
+ end
178
+
179
+ it 'should allow elsif 2' do
180
+ engine.parse defn('A:',
181
+ ' n =?',
182
+ ' fact = if n <= 1 then 1',
183
+ ' elsif n < 3 then 3',
184
+ ' elsif n < 7 then Dummy.call_me_maybe(7)',
185
+ ' else n'
186
+ )
187
+ end
188
+
170
189
  it 'should allow non-recursive code 1' do
171
190
  # this is not a recursion error
172
191
  engine.parse defn('A:',
@@ -152,6 +152,48 @@ describe 'Delorean' do
152
152
  expect(factor).to be < 8
153
153
  end
154
154
 
155
+ it 'cache allows to get result faster' do
156
+ perf_test = <<-DELOREAN
157
+ A:
158
+ v =?
159
+ result = Dummy.sleep_1ms
160
+
161
+ AWithCache:
162
+ _cache = true
163
+ v =?
164
+ result = Dummy.sleep_1ms
165
+ DELOREAN
166
+
167
+ engine.parse perf_test.gsub(/^ /, '')
168
+
169
+ bm = Benchmark.ips do |x|
170
+ x.report('delorean') do
171
+ engine.evaluate('A', 'result', {})
172
+ end
173
+
174
+ x.report('delorean_node_cache') do
175
+ engine.evaluate('AWithCache', 'result', {})
176
+ end
177
+
178
+ x.report('ruby') do
179
+ Dummy.sleep_1ms
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
+ cache_factor = h['delorean_node_cache'] / h['delorean']
191
+ # p cache_factor
192
+
193
+ expected_cache_factor = ENV['COVERAGE'] ? 64 : 80
194
+ expect(cache_factor).to be > expected_cache_factor
195
+ end
196
+
155
197
  it 'array and node call performance as expected' do
156
198
  perf_test = <<-DELOREAN
157
199
  A: