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.
- 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:
|