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