delorean_lang 2.1.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: