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.
- checksums.yaml +4 -4
- data/.gemignore +2 -0
- data/.rubocop.yml +3 -2
- data/.rubocop_todo.yml +19 -29
- data/Gemfile +1 -2
- data/README.md +57 -17
- data/delorean.gemspec +7 -1
- data/lib/delorean/base.rb +25 -3
- data/lib/delorean/cache.rb +42 -4
- data/lib/delorean/delorean.rb +357 -36
- data/lib/delorean/delorean.treetop +12 -2
- data/lib/delorean/engine.rb +44 -5
- data/lib/delorean/nodes.rb +32 -2
- data/lib/delorean/version.rb +1 -1
- data/spec/eval_spec.rb +40 -0
- data/spec/node_level_cache_spec.rb +135 -0
- data/spec/parse_spec.rb +20 -1
- data/spec/perf_spec.rb +42 -0
- data/spec/spec_helper.rb +17 -1
- data/spec/support/simplecov_helper.rb +30 -0
- metadata +7 -3
- data/.gitlab-ci.yml +0 -37
@@ -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
|
data/lib/delorean/engine.rb
CHANGED
@@ -258,7 +258,7 @@ module Delorean
|
|
258
258
|
@line_no += 1
|
259
259
|
|
260
260
|
# skip comments
|
261
|
-
next if
|
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
|
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
|
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
|
-
|
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
|
-
|
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)]
|
data/lib/delorean/nodes.rb
CHANGED
@@ -318,7 +318,9 @@ eos
|
|
318
318
|
class IString < Literal
|
319
319
|
def rewrite(_context)
|
320
320
|
# FIXME: hacky to just fail
|
321
|
-
|
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
|
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) : []
|
data/lib/delorean/version.rb
CHANGED
data/spec/eval_spec.rb
CHANGED
@@ -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
|
data/spec/parse_spec.rb
CHANGED
@@ -112,7 +112,7 @@ describe 'Delorean' do
|
|
112
112
|
|
113
113
|
expect do
|
114
114
|
engine.parse defn('A:',
|
115
|
-
'
|
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:',
|
data/spec/perf_spec.rb
CHANGED
@@ -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:
|