eggshell 0.8.1
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 +7 -0
- data/bin/eggshell +32 -0
- data/lib/eggshell/block-handler.rb +54 -0
- data/lib/eggshell/block.rb +51 -0
- data/lib/eggshell/bundles/basics.rb +854 -0
- data/lib/eggshell/bundles/loader.rb +57 -0
- data/lib/eggshell/bundles.rb +62 -0
- data/lib/eggshell/expression-evaluator.rb +862 -0
- data/lib/eggshell/macro-handler.rb +41 -0
- data/lib/eggshell/processor-context.rb +28 -0
- data/lib/eggshell.rb +758 -0
- metadata +55 -0
@@ -0,0 +1,862 @@
|
|
1
|
+
# Parses and evaluates statements (expressions).
|
2
|
+
#
|
3
|
+
# pre.
|
4
|
+
# # simple expression
|
5
|
+
# 1 + 5
|
6
|
+
# 1 < (5 + 8) || 3 * 4 > 2
|
7
|
+
#
|
8
|
+
# # arrays and maps
|
9
|
+
# [1, 2, 3]
|
10
|
+
# [1, [2, 3], 4]
|
11
|
+
# {'key': 'val', 'another': 'val2', 'num': 8.2}
|
12
|
+
# [1, {'key': 'val'}]
|
13
|
+
# {'arr': [1, 2, 3]}
|
14
|
+
#
|
15
|
+
# # variables set via @var macro
|
16
|
+
# var.name + 5
|
17
|
+
#
|
18
|
+
# # function calls
|
19
|
+
# fn1(arg1, "arg2", 3) + fn2({}, [])
|
20
|
+
# /pre
|
21
|
+
module Eggshell;end
|
22
|
+
class Eggshell::ExpressionEvaluator
|
23
|
+
REGEX_EXPR_PLACEHOLDERS = /(\\|\$\[|\$\{|\]|\}|\+|\-|>|<|=|\s+|\(|\)|\*|\/`)/
|
24
|
+
REGEX_EXPR_STATEMENT = /(\(|\)|,|\[|\]|\+|-|\*|\/|%|<=|>=|==|<|>|"|'|\s+|\\|\{|\}|:|\?)/
|
25
|
+
REGEX_OPERATORS = /\+|-|\*|\/|%|<=|>=|<|>|==|!=|&&|\|\||&|\|/
|
26
|
+
|
27
|
+
LOG_OP = 2
|
28
|
+
LOG_LEVEL = 0
|
29
|
+
|
30
|
+
OP_PRECEDENCE = {
|
31
|
+
'++' => 150, '--' => 150,
|
32
|
+
'*' => 60, '/' => 60, '%' => 60,
|
33
|
+
'<<' => 55, '>>' => 55,
|
34
|
+
'+' => 51, '-' => 50,
|
35
|
+
'<' => 49, '>' => 49, '<=' => 49, '=>' => 49,
|
36
|
+
'==' => 48, '!=' => 48,
|
37
|
+
'&' => 39,
|
38
|
+
'^' => 38,
|
39
|
+
'|' => 37,
|
40
|
+
'&&' => 36,
|
41
|
+
'||' => 35
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
OP_RTL = {
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
class ExprArray < Array
|
48
|
+
attr_accessor :dynamic
|
49
|
+
attr_reader :dyn_keys
|
50
|
+
|
51
|
+
# Adds a term to the array. If it's a dynamic statement, its position is
|
52
|
+
# marked and this array is marked as dynamic for evaluation later.
|
53
|
+
def <<(term)
|
54
|
+
@dyn_keys = [] if !@dyn_keys
|
55
|
+
|
56
|
+
# cascade dynamic status
|
57
|
+
if term.is_a?(ExprArray) || term.is_a?(ExprHash)
|
58
|
+
if term.dynamic
|
59
|
+
@dynamic = true
|
60
|
+
@dyn_keys << self.length
|
61
|
+
end
|
62
|
+
elsif term.is_a?(Array)
|
63
|
+
if term[0] == :op
|
64
|
+
|
65
|
+
end
|
66
|
+
term, dyn = Eggshell::ExpressionEvaluator::struct_compact(term)
|
67
|
+
if dyn
|
68
|
+
@dynamic = true
|
69
|
+
@dyn_keys << self.length
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
self.push(term)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class ExprHash < Hash
|
78
|
+
attr_accessor :dynamic
|
79
|
+
attr_reader :dyn_keys
|
80
|
+
|
81
|
+
def add_term(key, term)
|
82
|
+
@dyn_keys = [] if !@dyn_keys
|
83
|
+
|
84
|
+
if term.is_a?(ExprArray) || term.is_a?(ExprHash)
|
85
|
+
if term.dynamic
|
86
|
+
@dynamic = true
|
87
|
+
@dyn_keys << key
|
88
|
+
end
|
89
|
+
elsif term.is_a?(Array)
|
90
|
+
term, dyn = Eggshell::ExpressionEvaluator::struct_compact(term)
|
91
|
+
if dyn
|
92
|
+
@dynamic = true
|
93
|
+
@dyn_keys << key
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
self[key] = term
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Normalizes a term.
|
102
|
+
# @param Object term If `String`, attempts to infer type (either `Fixnum`, `Float`, or `[:var, varname]`)
|
103
|
+
# @param Boolean preserve_str If true and input is string but not number, return string literal.
|
104
|
+
def self.term_val(term, preserve_str = false)
|
105
|
+
if term.is_a?(String)
|
106
|
+
if preserve_str
|
107
|
+
return term
|
108
|
+
elsif term.match(/^-?\d+$/)
|
109
|
+
return term.to_i
|
110
|
+
elsif term.match(/^-?\d*\.\d+$/)
|
111
|
+
return term.to_f
|
112
|
+
elsif term == 'null' || term == 'nil'
|
113
|
+
return nil
|
114
|
+
elsif term == 'true'
|
115
|
+
return true
|
116
|
+
elsif term == 'false'
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
|
120
|
+
if term[0] == '-'
|
121
|
+
return [:op, '-', 0, [:var, term[1..-1]], :group]
|
122
|
+
else
|
123
|
+
return [:var, term]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
return term
|
127
|
+
end
|
128
|
+
|
129
|
+
# Inserts a term into the right-most operand.
|
130
|
+
#
|
131
|
+
# Operator structure: `[:op, 'operator', 'left-term', 'right-term']
|
132
|
+
def self.op_insert(frag, term)
|
133
|
+
while frag[3].is_a?(Array)
|
134
|
+
frag = frag[3]
|
135
|
+
end
|
136
|
+
frag[3] = term
|
137
|
+
end
|
138
|
+
|
139
|
+
# Restructures operands if new operator has higher predence than previous operator.
|
140
|
+
# @param String nop New operator.
|
141
|
+
# @param Array frag Operator fragment.
|
142
|
+
# @param Array stack Fragment stack.
|
143
|
+
def self.op_precedence(nop, frag, stack)
|
144
|
+
topfrag = frag
|
145
|
+
lptr = topfrag
|
146
|
+
errs "op_precedence: frag=#{frag.inspect}", 1
|
147
|
+
|
148
|
+
# retrieve the right-most operand
|
149
|
+
# lptr = [opB, [opA, left, right], [opC, left, right]]
|
150
|
+
# frag = [opC, left, right]
|
151
|
+
# @todo look out for :nested in [4]?
|
152
|
+
while frag[3].is_a?(Array) && frag[3][0] == :op && frag[3][-1] != :group
|
153
|
+
lptr = frag
|
154
|
+
frag = frag[3]
|
155
|
+
end
|
156
|
+
|
157
|
+
if frag[0] == :op
|
158
|
+
oop = frag[1]
|
159
|
+
p0 = OP_PRECEDENCE[oop]
|
160
|
+
p1 = OP_PRECEDENCE[nop]
|
161
|
+
errs "op_precedence: #{oop} (#{p0}) vs #{nop} (#{p1})", 0
|
162
|
+
|
163
|
+
if p0 > p1
|
164
|
+
# preserve previous fragment and make as left term of new op
|
165
|
+
# [opA, left, right] ==> [opB, [opA, left, right], nil]
|
166
|
+
nfrag = [:op, nop, topfrag.clone, nil]
|
167
|
+
lptr[1] = nop
|
168
|
+
lptr[2] = nfrag[2]
|
169
|
+
lptr[3] = nil
|
170
|
+
else
|
171
|
+
frag[3] = [:op, nop, frag[3], nil]
|
172
|
+
end
|
173
|
+
stack << topfrag
|
174
|
+
else
|
175
|
+
stack << [:op, nop, frag, nil]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.struct(str)
|
180
|
+
errs "struct parse: #{str}", 9
|
181
|
+
toks = str.split(REGEX_EXPR_STATEMENT)
|
182
|
+
char_pos = 0
|
183
|
+
state = [:nil]
|
184
|
+
last_state = nil
|
185
|
+
d = 0
|
186
|
+
|
187
|
+
# each element on the stack points to a nested level (so root is 0, first parenthesis is 1, second is 2, etc.)
|
188
|
+
# ptr points to the deepest level
|
189
|
+
stack = [ [] ]
|
190
|
+
ptr = stack[0]
|
191
|
+
term = nil
|
192
|
+
term_state = nil
|
193
|
+
|
194
|
+
quote_delim = nil
|
195
|
+
|
196
|
+
# last_key tracks the last key being built up in a map expression. this allows maps to contain maps
|
197
|
+
map_key = nil
|
198
|
+
last_key = []
|
199
|
+
|
200
|
+
i = 0
|
201
|
+
toks.delete('')
|
202
|
+
|
203
|
+
# push a new operator or a term onto the stack
|
204
|
+
_op_push = lambda {|operator|
|
205
|
+
errs "_op_push: #{operator} (term=#{term}) (ptr[-1]=#{ptr[-1].inspect})", 1
|
206
|
+
if term
|
207
|
+
# check what the last item is.
|
208
|
+
frag = ptr.pop
|
209
|
+
term = term_val(term, term_state == :quote)
|
210
|
+
if frag.is_a?(Array) && frag[0] == :op
|
211
|
+
op_insert(frag, term)
|
212
|
+
if operator
|
213
|
+
op_precedence(operator, frag, ptr)
|
214
|
+
else
|
215
|
+
ptr << frag
|
216
|
+
end
|
217
|
+
else
|
218
|
+
ptr << frag if frag
|
219
|
+
ptr << [:op, operator, term, nil]
|
220
|
+
end
|
221
|
+
|
222
|
+
errs ">> state=#{state.inspect}, stack=#{stack.inspect}", 0
|
223
|
+
errs ">> frag=#{frag.inspect}", 0
|
224
|
+
term = nil
|
225
|
+
term_state = nil
|
226
|
+
elsif operator
|
227
|
+
# no term, so pushing operator. either generate new operator fragment or put in precedence order
|
228
|
+
frag = ptr.pop
|
229
|
+
if frag.is_a?(Array)
|
230
|
+
if frag[0] == :op
|
231
|
+
# preserve parenthesized group
|
232
|
+
if frag[4] == :group
|
233
|
+
ptr << [:op, operator, frag, nil]
|
234
|
+
else
|
235
|
+
op_precedence(operator, frag, ptr)
|
236
|
+
end
|
237
|
+
else
|
238
|
+
ptr << [:op, operator, frag, nil]
|
239
|
+
end
|
240
|
+
elsif frag
|
241
|
+
ptr << [:op, operator, frag, nil]
|
242
|
+
else
|
243
|
+
# @todo throw exception
|
244
|
+
end
|
245
|
+
end
|
246
|
+
}
|
247
|
+
|
248
|
+
# closes out a state
|
249
|
+
_transition = lambda {|st|
|
250
|
+
errs "_transition: :#{st} (term=#{term.class}, ts=#{term_state})", 1
|
251
|
+
errs "state=#{state.inspect}"
|
252
|
+
ret = nil
|
253
|
+
if st == :op
|
254
|
+
ret = st
|
255
|
+
if term
|
256
|
+
_op_push.call(nil)
|
257
|
+
state.pop
|
258
|
+
term = nil
|
259
|
+
term_state = nil
|
260
|
+
errs "_transition: ptr=#{ptr.inspect}", 1
|
261
|
+
else
|
262
|
+
# @todo throw exception
|
263
|
+
end
|
264
|
+
# @todo throw exception -- no closing ')'
|
265
|
+
elsif st == :tern
|
266
|
+
if term
|
267
|
+
ptr << term_val(term, term_state == :quote)
|
268
|
+
term = nil
|
269
|
+
term_state = nil
|
270
|
+
end
|
271
|
+
|
272
|
+
if ptr[-4] == :tern
|
273
|
+
right = ptr.pop
|
274
|
+
left = ptr.pop
|
275
|
+
cond = ptr.pop
|
276
|
+
ptr.pop
|
277
|
+
ptr << [:op_tern, cond, left, right]
|
278
|
+
else
|
279
|
+
# @todo throw exception
|
280
|
+
end
|
281
|
+
elsif term
|
282
|
+
ret = :term
|
283
|
+
ptr << term_val(term, term_state == :quote)
|
284
|
+
errs "_transition: term <-- #{ptr.inspect}", 0
|
285
|
+
term = nil
|
286
|
+
term_state = nil
|
287
|
+
elsif st == :fnop || st == :nest
|
288
|
+
else
|
289
|
+
# @todo throw exception?
|
290
|
+
end
|
291
|
+
|
292
|
+
return ret
|
293
|
+
}
|
294
|
+
|
295
|
+
errs "toks = #{toks.inspect}", 5
|
296
|
+
while (i < toks.length)
|
297
|
+
tok = toks[i]
|
298
|
+
i += 1
|
299
|
+
char_pos += tok.length
|
300
|
+
errs "tok: #{tok} (state=#{state[-1]}, term=#{term})", 6
|
301
|
+
if tok == '\\'
|
302
|
+
i += 1 if toks[i] == ''
|
303
|
+
char_pos += toks[i].length
|
304
|
+
term += toks[i]
|
305
|
+
i += 1
|
306
|
+
elsif term_state == :quote
|
307
|
+
if tok != quote_delim
|
308
|
+
term += tok
|
309
|
+
else
|
310
|
+
quote_delim = nil
|
311
|
+
_transition.call(state[-1])
|
312
|
+
term = nil
|
313
|
+
term_state = nil
|
314
|
+
end
|
315
|
+
elsif (tok == "'" || tok == '"')
|
316
|
+
quote_delim = tok
|
317
|
+
term = ''
|
318
|
+
term_state = :quote
|
319
|
+
elsif tok == ','
|
320
|
+
errs "comma: ptr=#{ptr.inspect}", 5
|
321
|
+
_transition.call(state[-1])
|
322
|
+
term = nil
|
323
|
+
term_state = nil
|
324
|
+
elsif tok == '{'
|
325
|
+
if state[-1] == :nil && stack[0][-1][0] == :fn
|
326
|
+
delim = '{'
|
327
|
+
while toks[i]
|
328
|
+
delim += toks[i]
|
329
|
+
i += 1
|
330
|
+
end
|
331
|
+
stack[0] << [:brace_op, delim]
|
332
|
+
break
|
333
|
+
end
|
334
|
+
|
335
|
+
stack << []
|
336
|
+
state << :hash
|
337
|
+
ptr = stack[-1]
|
338
|
+
elsif tok == '}'
|
339
|
+
errs "}", 5
|
340
|
+
_transition.call(state[-1])
|
341
|
+
|
342
|
+
rawvals = stack.pop
|
343
|
+
errs "rawvals=#{rawvals.inspect}", 4
|
344
|
+
map = ExprHash.new
|
345
|
+
imap = 0
|
346
|
+
while (imap < rawvals.length)
|
347
|
+
# @todo make sure key is scalar
|
348
|
+
# @todo make sure length is even (indicates unbalanced map def otherwise)
|
349
|
+
mkey = rawvals[imap]
|
350
|
+
mkey = mkey[1] if mkey.is_a?(Array)
|
351
|
+
map[mkey] = rawvals[imap+1]
|
352
|
+
imap += 2
|
353
|
+
end
|
354
|
+
|
355
|
+
state.pop
|
356
|
+
ptr = stack[-1]
|
357
|
+
ptr << map
|
358
|
+
elsif tok == '?'
|
359
|
+
# mark the position
|
360
|
+
if term
|
361
|
+
ptr << :tern
|
362
|
+
ptr << term_val(term, term_state == :quote)
|
363
|
+
term = nil
|
364
|
+
term_state = nil
|
365
|
+
else
|
366
|
+
last = ptr.pop
|
367
|
+
ptr << :tern
|
368
|
+
ptr << last
|
369
|
+
end
|
370
|
+
|
371
|
+
state << :tern
|
372
|
+
elsif tok == ':'
|
373
|
+
errs ": #{state[-1]} | ptr=#{ptr.inspect}", 5
|
374
|
+
if state[-1] == :hash || state[-1] == :tern
|
375
|
+
# case for this: when a key is quoted, the term is committed to the pointer already, so at ':' term is nil
|
376
|
+
if term != nil
|
377
|
+
ptr << term_val(term, term_state == :quote)
|
378
|
+
term = nil
|
379
|
+
term_state = nil
|
380
|
+
end
|
381
|
+
# @todo validate ternary stack length?
|
382
|
+
else
|
383
|
+
# @todo throw exception
|
384
|
+
end
|
385
|
+
errs "ptr=#{ptr.inspect}", 4
|
386
|
+
elsif tok == '['
|
387
|
+
stack << ExprArray.new
|
388
|
+
state << :arr
|
389
|
+
ptr = stack[-1]
|
390
|
+
elsif tok == ']'
|
391
|
+
last_state = _transition.call(state[-1])
|
392
|
+
state.pop
|
393
|
+
vals = stack.pop
|
394
|
+
ptr = stack[-1]
|
395
|
+
ptr << vals
|
396
|
+
elsif tok == '('
|
397
|
+
errs "(", 4
|
398
|
+
if term
|
399
|
+
# @todo throw exception if term is a quote?
|
400
|
+
# @todo throw exception if term is not a valid word?
|
401
|
+
frag = [:fn, term, nil]
|
402
|
+
stack << frag
|
403
|
+
stack << []
|
404
|
+
state << :fnop
|
405
|
+
ptr = stack[-1]
|
406
|
+
term = nil
|
407
|
+
term_state = nil
|
408
|
+
else
|
409
|
+
stack << []
|
410
|
+
state << :nest
|
411
|
+
ptr = stack[-1]
|
412
|
+
end
|
413
|
+
elsif tok == ')'
|
414
|
+
last_state = _transition.call(state[-1])
|
415
|
+
#while state[-1] != :fnop && state[-1] != :nest
|
416
|
+
# last_state = _transition.call(state[-1])
|
417
|
+
# break if state[-1] == :fnop || state[-1] == :nest
|
418
|
+
#end
|
419
|
+
nest_statement = stack.pop
|
420
|
+
ptr = stack[-1]
|
421
|
+
_state = state.pop
|
422
|
+
errs ") nest_statement=#{nest_statement[0].inspect}", 5
|
423
|
+
if _state == :fnop
|
424
|
+
frag = stack.pop
|
425
|
+
frag[2] = nest_statement
|
426
|
+
ptr = stack[-1]
|
427
|
+
ptr << frag
|
428
|
+
elsif _state == :nest
|
429
|
+
if nest_statement.length > 1
|
430
|
+
# @todo throw exception, parenthetical expression should be reduced to single term
|
431
|
+
end
|
432
|
+
nest_statement = nest_statement.pop
|
433
|
+
if !nest_statement.is_a?(Array)
|
434
|
+
errs "#{str}", 0
|
435
|
+
errs "!! nest: #{nest_statement}\n#{stack.inspect}", 0
|
436
|
+
end
|
437
|
+
nest_statement << :group
|
438
|
+
inject_into(ptr, nest_statement)
|
439
|
+
elsif _state == :op
|
440
|
+
ptr << nest_statement
|
441
|
+
else
|
442
|
+
# @throw exception?
|
443
|
+
end
|
444
|
+
elsif tok.match(REGEX_OPERATORS)
|
445
|
+
# assumes negative
|
446
|
+
if tok == '-' && !term
|
447
|
+
t = ptr.pop
|
448
|
+
if t == nil
|
449
|
+
term = '-'
|
450
|
+
next
|
451
|
+
else
|
452
|
+
ptr << t
|
453
|
+
end
|
454
|
+
end
|
455
|
+
# @todo more appropriate way to push stack? otherwise fn(1 + 2 + 3) gets 1 too many :op
|
456
|
+
state << :op if state[-1] != :op
|
457
|
+
_op_push.call(tok)
|
458
|
+
elsif tok != ''
|
459
|
+
# white space; close out state
|
460
|
+
if tok.strip == ''
|
461
|
+
_transition.call(state[-1]) if term
|
462
|
+
next
|
463
|
+
end
|
464
|
+
|
465
|
+
if !term
|
466
|
+
term = tok
|
467
|
+
else
|
468
|
+
term += tok
|
469
|
+
end
|
470
|
+
|
471
|
+
# look-ahead to build variable reference
|
472
|
+
ntok = toks[i]
|
473
|
+
while ntok
|
474
|
+
if ntok != ',' && ntok != ':' && ntok != '(' && ntok != ')' && ntok != '}' && !ntok.match(REGEX_OPERATORS)
|
475
|
+
if ntok != ' ' && ntok != "\t"
|
476
|
+
term += ntok
|
477
|
+
end
|
478
|
+
i += 1
|
479
|
+
ntok = toks[i]
|
480
|
+
else
|
481
|
+
break
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# @todo validate completeness of state (e.g. no omitted parenthesis)
|
488
|
+
if state[1] || term
|
489
|
+
_transition.call(state[-1])
|
490
|
+
end
|
491
|
+
|
492
|
+
errs "*** end state: #{state.inspect} ***", 2
|
493
|
+
errs "*** end stack: #{stack.inspect} ***", 2
|
494
|
+
return stack[0]
|
495
|
+
end
|
496
|
+
|
497
|
+
def self.inject_into(ptr, frag)
|
498
|
+
if !ptr[-1]
|
499
|
+
ptr << frag
|
500
|
+
else
|
501
|
+
if ptr[-1].is_a?(Array)
|
502
|
+
if ptr[-1][0] == :op
|
503
|
+
op_insert(ptr[-1], frag)
|
504
|
+
end
|
505
|
+
# @todo is there an else?
|
506
|
+
end
|
507
|
+
# @todo is there an else?
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
# Takes the output of {@see struct} and evaluates static expressions to speed up
|
512
|
+
# {@see expr_eval}.
|
513
|
+
# @param Object struct The structure to compact.
|
514
|
+
# @return Array `[struct, dynamic]`
|
515
|
+
def self.struct_compact(struct)
|
516
|
+
dyn = false
|
517
|
+
|
518
|
+
if struct.is_a?(ExprArray) || struct.is_a?(ExprHash)
|
519
|
+
# don't need to do anything -- add_term already compacts terms
|
520
|
+
dyn = struct.dynamic
|
521
|
+
elsif struct.is_a?(Array)
|
522
|
+
# the only term that potentially is static is a logical or mathematical operation.
|
523
|
+
# the operation will compact if all terms are static (e.g. can be evaluated now)
|
524
|
+
if struct[0].is_a?(Symbol)
|
525
|
+
if struct[0] == :op
|
526
|
+
lterm = struct[2]
|
527
|
+
if lterm.is_a?(Array)
|
528
|
+
lstruct, ldyn = struct_compact(lterm)
|
529
|
+
if !ldyn
|
530
|
+
lterm = lstruct
|
531
|
+
else
|
532
|
+
lterm = nil
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
rterm = struct[3]
|
537
|
+
if rterm.is_a?(Array)
|
538
|
+
rstruct, rdyn = struct_compact(rterm)
|
539
|
+
if !rdyn
|
540
|
+
rterm = rstruct
|
541
|
+
else
|
542
|
+
rterm = nil
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
if lterm != nil && rterm != nil
|
547
|
+
return [expr_eval_op(struct[1], lterm, rterm), false]
|
548
|
+
else
|
549
|
+
dyn = true
|
550
|
+
end
|
551
|
+
elsif struct[0] == :fn
|
552
|
+
dyn = true
|
553
|
+
struct[2] = struct_compact(struct[2])[0]
|
554
|
+
else
|
555
|
+
dyn = true
|
556
|
+
end
|
557
|
+
else
|
558
|
+
i = 0
|
559
|
+
while i < struct.length
|
560
|
+
substruct = struct[i]
|
561
|
+
substruct, sdyn = struct_compact(substruct)
|
562
|
+
dyn = true if sdyn
|
563
|
+
struct[i] = substruct
|
564
|
+
i += 1
|
565
|
+
end
|
566
|
+
end
|
567
|
+
else
|
568
|
+
return [struct, false]
|
569
|
+
end
|
570
|
+
|
571
|
+
return [struct, dyn]
|
572
|
+
end
|
573
|
+
|
574
|
+
# @param Array expr An expression structure from @{see struct()}
|
575
|
+
# @param Map vtable Map for variable references.
|
576
|
+
# @param Map ftable Map for function calls.
|
577
|
+
def self.expr_eval(expr, vtable, ftable)
|
578
|
+
errs "expr_eval: #{expr.inspect}", 3
|
579
|
+
expr = expr[0] if expr.is_a?(Array) && expr.length == 1
|
580
|
+
|
581
|
+
ret = nil
|
582
|
+
if expr.is_a?(ExprArray) || expr.is_a?(ExprHash)
|
583
|
+
ret = expr.clone
|
584
|
+
(expr.dyn_keys || []).each do |key|
|
585
|
+
ret[key] = expr_eval(expr[key], vtable, ftable)
|
586
|
+
end
|
587
|
+
elsif expr.is_a?(Array)
|
588
|
+
if expr[0] && !expr[0].is_a?(Symbol)
|
589
|
+
ret = []
|
590
|
+
i = 0
|
591
|
+
expr.each do |subexpr|
|
592
|
+
ret[i] = expr_eval(subexpr, vtable, ftable)
|
593
|
+
i += 1
|
594
|
+
end
|
595
|
+
elsif expr[0]
|
596
|
+
frag = expr
|
597
|
+
if frag[0] == :op_tern
|
598
|
+
cond = expr_eval(frag[1], vtable, ftable)
|
599
|
+
if cond
|
600
|
+
ret = expr_eval(frag[2], vtable, ftable)
|
601
|
+
else
|
602
|
+
ret = expr_eval(frag[3], vtable, ftable)
|
603
|
+
end
|
604
|
+
elsif frag[0] == :op
|
605
|
+
op = frag[1]
|
606
|
+
lterm = frag[2]
|
607
|
+
rterm = frag[3]
|
608
|
+
|
609
|
+
errs "lterm >> #{lterm.inspect}", 1
|
610
|
+
errs "rterm >> #{rterm.inspect}", 1
|
611
|
+
if lterm.is_a?(Array)
|
612
|
+
if lterm[0] == :var
|
613
|
+
lterm = retrieve_var(lterm[1], vtable, ftable)
|
614
|
+
else
|
615
|
+
lterm = expr_eval(lterm, vtable, ftable)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
if rterm.is_a?(Array)
|
619
|
+
if rterm[0] == :var
|
620
|
+
rterm = retrieve_var(rterm[1], vtable, ftable)
|
621
|
+
else
|
622
|
+
rterm = expr_eval(rterm, vtable, ftable)
|
623
|
+
end
|
624
|
+
end
|
625
|
+
errs "lterm << #{lterm.inspect}", 0
|
626
|
+
errs "rterm << #{rterm.inspect}", 0
|
627
|
+
ret = expr_eval_op(op, lterm, rterm)
|
628
|
+
elsif frag[0] == :fn
|
629
|
+
# @todo see if fname itself has an entry, and call if it has method `func_call` (?)
|
630
|
+
fkey = frag[1]
|
631
|
+
args = frag[2]
|
632
|
+
ns, fname = fkey.split(':')
|
633
|
+
if !fname
|
634
|
+
fname = ns
|
635
|
+
ns = ''
|
636
|
+
end
|
637
|
+
fname = fname.to_sym
|
638
|
+
|
639
|
+
handler = ftable[fkey]
|
640
|
+
handler = ftable[ns] if !handler
|
641
|
+
|
642
|
+
if handler && handler.respond_to?(fname)
|
643
|
+
cp = []
|
644
|
+
args.each do |arg|
|
645
|
+
el = expr_eval(arg, vtable, ftable)
|
646
|
+
cp << el
|
647
|
+
end
|
648
|
+
|
649
|
+
ret = ftable[ns].send(fname, *cp)
|
650
|
+
else
|
651
|
+
ret = nil
|
652
|
+
# @todo log error or throw exception? maybe this should be a param option
|
653
|
+
end
|
654
|
+
elsif frag[0] == :var
|
655
|
+
ret = retrieve_var(frag[1], vtable, ftable)
|
656
|
+
if ret.is_a?(Array) && ret[0].is_a?(Symbol)
|
657
|
+
ret = expr_eval(ret, vtable, ftable)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
else
|
662
|
+
return expr
|
663
|
+
end
|
664
|
+
return ret
|
665
|
+
end
|
666
|
+
|
667
|
+
def self.expr_eval_op(op, lterm, rterm)
|
668
|
+
ret = nil
|
669
|
+
#$stderr.write "** #{lterm.inspect} ( #{op} ) #{rterm.inspect}\n"
|
670
|
+
case op
|
671
|
+
when '=='
|
672
|
+
ret = lterm == rterm
|
673
|
+
when '!='
|
674
|
+
ret = lterm != rterm
|
675
|
+
when '<'
|
676
|
+
ret = lterm < rterm
|
677
|
+
when '>'
|
678
|
+
ret = lterm > rterm
|
679
|
+
when '<='
|
680
|
+
ret = lterm <= rterm
|
681
|
+
when '>='
|
682
|
+
ret = lterm >= rterm
|
683
|
+
when '+'
|
684
|
+
ret = lterm + rterm
|
685
|
+
when '-'
|
686
|
+
ret = lterm - rterm
|
687
|
+
when '*'
|
688
|
+
ret = lterm * rterm
|
689
|
+
when '/'
|
690
|
+
ret = lterm / rterm
|
691
|
+
when '%'
|
692
|
+
ret = lterm % rterm
|
693
|
+
when '&'
|
694
|
+
ret = lterm & rterm
|
695
|
+
when '|'
|
696
|
+
ret = lterm | rterm
|
697
|
+
when '&&'
|
698
|
+
ret = lterm && rterm
|
699
|
+
when '||'
|
700
|
+
ret = lterm || rterm
|
701
|
+
end
|
702
|
+
# @todo support other bitwise
|
703
|
+
return ret
|
704
|
+
end
|
705
|
+
|
706
|
+
# Attempts to a resolve a variable in the form `a.b.c.d` from an initial
|
707
|
+
# variable map. Using `.` as a delimiter, the longest match name is attempted
|
708
|
+
# to be matched (e.g. `a.b.c.d`, then `a.b.c`, then `a.b`, then `a`), and any
|
709
|
+
# remaining parts are considered getters than are chained together.
|
710
|
+
#
|
711
|
+
# For instance, if `a.b.c.d` partially resolves to `a.b`, then:
|
712
|
+
# # check if `(a.b).c` is a valid key/index/method
|
713
|
+
# # check if `(a.b.c).d` is a valid key/index/method
|
714
|
+
#
|
715
|
+
# If at any point a `nil` is encountered in the above scenario, a `nil` will
|
716
|
+
# be returned.
|
717
|
+
# @param Boolean return_ptr If true, returns the object holding the value, not
|
718
|
+
# the value itself.
|
719
|
+
# @return Either the resolved value (or nil), or if `return_ptr` is true, a structure
|
720
|
+
# in the form {{[n-1, [ley, type]]}} where `n-1` is the parent of the last key/get,
|
721
|
+
# {{key}} is the key/getter, and {{type}} is either {{:arr}} or {{:get}}/
|
722
|
+
def self.retrieve_var(var, vtable, ftable, return_ptr = false)
|
723
|
+
# @todo only do this when there's no [] and return_ptr is false
|
724
|
+
errs "retrieve_var: #{var}", 5
|
725
|
+
retval = vtable[var]
|
726
|
+
|
727
|
+
if !retval
|
728
|
+
# @todo more validation
|
729
|
+
# @todo support key expressions?
|
730
|
+
val = nil
|
731
|
+
vparts = var.split(/(\.|\[|\])/)
|
732
|
+
vparts.delete("")
|
733
|
+
|
734
|
+
ptr = vtable
|
735
|
+
last_ptr = nil
|
736
|
+
last_key = nil
|
737
|
+
|
738
|
+
i = 0
|
739
|
+
key = ''
|
740
|
+
type = :arr
|
741
|
+
# need to quote first element so that loop doesn't assume this to be a var reference
|
742
|
+
vparts[0] = "'#{vparts[0]}'"
|
743
|
+
|
744
|
+
while i < vparts.length
|
745
|
+
tok = vparts[i]
|
746
|
+
errs "var:tok=#{tok}", 4
|
747
|
+
i += 1
|
748
|
+
check_key = false
|
749
|
+
if tok == '.'
|
750
|
+
type = :get
|
751
|
+
check_key = true
|
752
|
+
elsif tok == '['
|
753
|
+
type = :arr
|
754
|
+
check_key = true
|
755
|
+
elsif tok == ']'
|
756
|
+
check_key = true
|
757
|
+
else
|
758
|
+
key += tok
|
759
|
+
end
|
760
|
+
|
761
|
+
if check_key || !vparts[i]
|
762
|
+
next if key == ''
|
763
|
+
errs "var:check=#{key} (#{type})", 3
|
764
|
+
# @todo if :arr but item doesn't respond to [], return nil?
|
765
|
+
last_ptr = ptr
|
766
|
+
if type == :arr
|
767
|
+
if !ptr.respond_to?(:[]) || ptr.is_a?(Numeric)
|
768
|
+
ptr = nil
|
769
|
+
break
|
770
|
+
end
|
771
|
+
|
772
|
+
# lookup var reference if not quoted and not integer
|
773
|
+
if key[0] == '"' || key[0] == "'"
|
774
|
+
key = key[1...-1]
|
775
|
+
elsif key.match(/[0-9]+/)
|
776
|
+
key = key.to_i
|
777
|
+
else
|
778
|
+
key = retrieve_var(key, vtable, {}, false)
|
779
|
+
end
|
780
|
+
|
781
|
+
# make sure key is numeric for array
|
782
|
+
if ptr.is_a?(Array) && !key.is_a?(Numeric)
|
783
|
+
ptr = nil
|
784
|
+
break
|
785
|
+
end
|
786
|
+
|
787
|
+
ptr = ptr[key]
|
788
|
+
if ptr
|
789
|
+
last_key = [key, :arr]
|
790
|
+
else
|
791
|
+
break
|
792
|
+
end
|
793
|
+
else
|
794
|
+
# @todo sanity check? (no quotes, not numeric or starting with num)
|
795
|
+
# @todo restrict to 'get_*'?
|
796
|
+
meth1 = key.to_sym
|
797
|
+
meth2 = ('get_' + key).to_sym
|
798
|
+
if ptr.respond_to?(meth1)
|
799
|
+
last_ptr = ptr
|
800
|
+
ptr = ptr.send(meth1)
|
801
|
+
elsif ptr.respond_to?(meth2)
|
802
|
+
last_ptr = ptr
|
803
|
+
ptr = ptr.send(meth2)
|
804
|
+
else
|
805
|
+
ptr = nil
|
806
|
+
break
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
key = ''
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
retval = ptr
|
815
|
+
if return_ptr
|
816
|
+
if last_ptr != nil
|
817
|
+
retval = [last_ptr, last_key]
|
818
|
+
else
|
819
|
+
retval = nil
|
820
|
+
end
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
return retval
|
825
|
+
end
|
826
|
+
|
827
|
+
#
|
828
|
+
def set_var(var, key, type, value)
|
829
|
+
if type == :arr
|
830
|
+
# @todo check key if Array
|
831
|
+
if var.is_a?(Hash) || var.is_a?(Array)
|
832
|
+
var[key] = value
|
833
|
+
end
|
834
|
+
else
|
835
|
+
sym = "set_#{key}".to_sym
|
836
|
+
if var.respond_to?(sym)
|
837
|
+
var.send(sym, value)
|
838
|
+
end
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
def self.restructure(struct)
|
843
|
+
if !struct.is_a?(Array)
|
844
|
+
return struct
|
845
|
+
elsif struct[0] == :op
|
846
|
+
lterm = restructure(struct[2])
|
847
|
+
rterm = restructure(struct[3])
|
848
|
+
return "#{lterm} #{struct[1]} #{rterm}"
|
849
|
+
elsif struct[0] == :var
|
850
|
+
return struct[1]
|
851
|
+
else
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
def self.errs(str, lvl = 5)
|
856
|
+
return if lvl < $errs_write
|
857
|
+
$stderr.write "[#{lvl}]#{' ' * ($errs_write_indent-lvl)}#{str}\n"
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
$errs_write = 10
|
862
|
+
$errs_write_indent = 10
|