File Coverage

blib/lib/Positron/Expression.pm
Criterion Covered Total %
statement 179 204 87.7
branch 110 130 84.6
condition 25 32 78.1
subroutine 24 26 92.3
pod 4 14 28.5
total 342 406 84.2


line stmt bran cond sub pod time code
1             package Positron::Expression;
2             our $VERSION = 'v0.1.3'; # VERSION
3              
4             =head1 NAME
5              
6             Positron::Expression - a simple language for template parameters
7              
8             =head1 VERSION
9              
10             version v0.1.3
11              
12             =head1 SYNOPSIS
13              
14             use Positron::Expression;
15              
16             my $env = Positron::Environment->new({ key => 'value' });
17             my $value = Positron::Expression::evaluate($string, $env);
18              
19             =head1 DESCRIPTION
20              
21             A simple expression language for templating constructs.
22             The main function, C, takes an expression as a string and a
23             L object, and evaluates the two.
24             The result is a scalar value.
25              
26             =head1 GRAMMAR
27              
28             The grammar is basically built up of the following rules.
29             The exact grammar is available as a package variable
30             C<$Positron::Expression::grammar>; this is a string which could be fed to
31             L starting at the token C.
32              
33             However, the L path has been replaced with a version
34             using plain regular expressions, so the string is no longer the direct
35             definition of the grammar.
36              
37             =head2 Whitespace
38              
39             Whitespace is generally allowed between individual parts of the grammar.
40              
41             =head2 Literals
42              
43             4 , -3.8 , "A string" , 'another string' , `a third string`
44              
45             The grammar allows for literal strings and numbers. Numbers are integers or
46             floating point numbers. Notations with exponents or with different bases are
47             not supported. Negative numbers are possible.
48              
49             Strings are delimited by double quotes, single quotes, or backticks.
50             Strings cannot contain their own delimiters; with three delimiters to choose
51             from, though, this should cover most use cases.
52              
53             =head2 Variable lookups
54              
55             a , key0 , ListValues , flag_not_possible
56              
57             A single, non-deliminated word is looked up in the environment; that value
58             is returned.
59             This may be C if the environment does not contain such a key.
60              
61             Words follow the rules for identifiers in most C-like languages (and Perl),
62             in that they may start with a letter or an underscore, and contain only
63             letters or underscores.
64             Currently, only ASCII letters are supported; this will hopefully change in
65             the future.
66              
67             =head2 Function calls
68              
69             a() , b(0) , find_file("./root", filename)
70              
71             Functions are looked up in the environment, like variables.
72             They obey the same rules for identifiers, and are expected to return an
73             anonymous function (a sub reference).
74              
75             This function is then called with the evaluated arguments.
76             In the last example above, C is looked up in the environment, and
77             the resulting value passed as the second argument to the function.
78              
79             All function calls are made in scalar context.
80              
81             =head2 Subselects
82              
83             Subselects allow you to to select a part of something else, like getting the
84             value for a given key in a hash, or an indexed entry in a list, or call a
85             method on an object etc.
86             In C, these are denoted with a dot, C<.>, hence the
87             alternative name "dotted expression".
88             Subselects can be chained.
89              
90             The following subselects are possible:
91              
92             =head3 Array index
93              
94             a.0 , b.4.-1 , c.$i
95              
96             Arrays are indexed by appending an integer to the variable or expression holding
97             the array.
98             Like Perl, indices start with 0, and negative indices count from the back.
99             The form C<< $ >> can be used to take any expression that evaluates
100             to an integer as an index.
101              
102             =head3 Hash index
103              
104             pos.x , server.link.url , authors."Ben Deutsch" , names.$current
105              
106             Hashes are indexed by appending a key, an identifier or string,
107             to the variable or expression holding the hash.
108             Most keys in practice will fit the form of an identifier as above
109             (letters, digits, underscores).
110             If not, a quoted string can be used.
111             The form C<< $ >> can again be used to take any expression that
112             evaluates to a string as the key.
113              
114             =head3 Object attributes
115              
116             obj.length , task.parent.priority , obj.$attr
117              
118             Object attributes work just like hash indices above, except they are called
119             on an object and look up that attribute.
120              
121             (In Perl, this is the same as a method call without parameters.)
122              
123             =head3 Object method calls
124              
125             img.make_src(320, 240) , abs(int(-4.2))
126              
127             Method calls work like a mixture between attributes and function calls.
128             The method name is restricted to an actual key, however, and not a free-form
129             string or a C<$>-expression.
130              
131             Like functions, methods are called in scalar context.
132              
133             =head2 Nested expressions
134              
135             hash.(var).length , ports.(resource.server)
136              
137             Expressions can be nested with parentheses.
138             The C expression above is equivalent to C, since
139             C as an expression is a variable lookup in the environment.
140              
141             =head2 Boolean combinations
142              
143             a ? !b , if ? then : else , !!empty
144              
145             The C, C<:> and C operands stand for "and", "or" and "not", respectively.
146             This terminology, while a bit obscure, is the mirror of Python's
147             C ternary operator replacement.
148             In practice, this allows for some common use cases:
149              
150             =head3 Not
151              
152             The C operator has a higher precedence than C or C<:>, binding closer.
153             It reverses the "truth" of the expression it precedes.
154              
155             B: unlike pure Perl, a reference to an empty array or an empty hash counts as false!
156             In Perl, it would be true because all references are true, barring overloading; only non-reference
157             empty arrays and hashes are false.
158             Positron's use is closer related to the Perl usages of C than C,
159             and is typically what you mean.
160              
161             =head3 Conditional values: And
162              
163             only_if ? value , first_cond ? second_cond ? result
164              
165             The C operator is a short-circuiting C<&&> or C equivalent.
166             If the left hand side is false, it is returned, otherwise the right hand side is returned.
167             It is chainable, and left associative.
168              
169             The most common use case is text insertion with a condition which is C<''> when false;
170             the right hand text is only inserted if the condition is true.
171              
172             =head3 Defaults: Or
173              
174             first_try : second_try : third_try
175              
176             The C<:> operator is a short-circuiting C<||> or C equivalent.
177             If the left hand side is true, it is returned, otherwise the right hand side is returned.
178             It is chainable, left associative, and has the same precedence as C.
179              
180             The most common use case is to provide a chain of fallback values, selecting the first
181             fitting (i.e. true) one.
182              
183             =head3 Ternary Operator
184              
185             if ? then : else
186              
187             Taken together, the C and C<:> operators form the well-known ternary operator: if the
188             left-most term is true, the middle term is chosen; else the right-most term is.
189              
190             =cut
191              
192 33     33   123993 use v5.10;
  33         118  
  33         1854  
193 33     33   178 use strict;
  33         55  
  33         1084  
194 33     33   175 use warnings;
  33         168  
  33         1108  
195              
196 33     33   177 use Carp qw(croak);
  33         70  
  33         1866  
197 33     33   17568 use Data::Dump qw(pp);
  33         181991  
  33         2164  
198 33     33   38066 use IO::String qw();
  33         192968  
  33         928  
199 33     33   2754 use Positron::Environment;
  33         73  
  33         1366  
200             #use Parse::RecDescent; # obsolete, see below
201 33     33   198 use Scalar::Util qw(blessed);
  33         67  
  33         303730  
202              
203             # The following grammar is used by Parse::RecDescent to create a "parse tree".
204             # Note that the Parse::RecDescent path is currently obsolete.
205              
206             our $grammar = <<'EOT';
207             # We start with our "boolean / ternary" expressions
208             expression: { @{$item[1]} == 1 ? $item[1]->[0] : ['expression', @{$item[1]}]; }
209             alternative: '!' alternative { ['not', $item[2]]; } | operand
210              
211             # strings and numbers cannot start a dotted expression
212             # in fact, numbers can have decimal points.
213             operand: string | number | lterm ('.' rterm)(s) { ['dot', $item[1], @{$item[2]}] } | lterm
214              
215             # The first part of a dotted expression is looked up in the environment.
216             # The following parts are parts of whatever came before, and consequently looked
217             # up there.
218             lterm: '(' expression ')' { $item[2] } | funccall | identifier | '$' lterm { ['env', $item[2]] }
219             rterm: '(' expression ')' { $item[2] } | methcall | key | string | integer | '$' lterm { $item[2] }
220              
221             # Strings currently cannot contain their delimiters, sorry.
222             string: '"' /[^"]*/ '"' { $item[2] } | /\'/ /[^\']*/ /\'/ { $item[2] } | '`' /[^`]*/ '`' { $item[2] }
223              
224             identifier: /[a-zA-Z_]\w*/ {['env', $item[1]]}
225             key: /[a-zA-Z_]\w*/ { $item[1] }
226             number: /[+-]?\d+(?:\.\d+)?/ { $item[1] }
227             integer: /[+-]?\d+/ { $item[1] }
228              
229             # We need "function calls" and "method calls", since with the latter, the function
230             # is *not* looked up in the environment.
231             funccall: identifier '(' expression(s? /\s*,\s*/) ')' { ['funccall', $item[1], @{$item[3]}] }
232             methcall: key '(' expression(s? /\s*,\s*/) ')' { ['methcall', $item[1], @{$item[3]}] }
233             EOT
234              
235             # A Parse::RecDescent object; currently obsolete
236             our $parser = undef;
237              
238             =head1 FUNCTIONS
239              
240             =head2 evaluate
241              
242             my $value = Positron::Expression::evaluate($string, $environment);
243              
244             Evaluates the expression in C<$string> with the L C<$env>.
245             The result is always a scalar value, which may be a plain scalar or a reference.
246             For example, the expression C with the environment C<< { x => [1] } >>
247             will evaluate to a reference to an array with one element.
248              
249             =cut
250              
251             sub evaluate {
252 530     530 1 1632 my ($string, $environment) = @_;
253 530 100 100     2626 return undef unless defined $string and $string ne '';
254 518         7829 my $tree = parse($string);
255             # Force scalar context, always
256 518         1069 return scalar(_evaluate($tree, $environment));
257             }
258              
259             =head2 parse
260              
261             my $tree = Positron::Expression::parse($string);
262              
263             Parses the string in the first argument, and returns an abstract parse tree.
264             The exact form of the tree is not important, it is usually a structure made
265             of nested array references. The important part is that it contains no
266             blessed references, only strings, numbers, arrays and hashes (that is, references
267             to those).
268              
269             This makes it easy to serialize the tree, for distributed caching or
270             persistant storage, if parsing time is critical.
271              
272             See also C to continue the evaluation.
273              
274             =cut
275              
276             # Obsolete interface based on Parse::RecDescent
277             sub parse_recd {
278 0     0 0 0 my ($string) = @_;
279              
280             # lazy build, why not
281 0 0       0 if (not $parser) {
282 0         0 require Parse::RecDescent;
283 0         0 $parser = Parse::RecDescent->new($grammar);
284             }
285             # We lazy-build the parser in any case, only then do we "fast abort"
286 0 0 0     0 return undef unless defined $string and $string ne '';
287 0         0 my $try_string = $string;
288 0         0 my $error_string = '';
289             #local *STDERR = IO::String->new($error_string);
290 0         0 local *STDERR;
291 0         0 open(STDERR, '>', \$error_string);
292 0         0 my $tree = $parser->expression(\$try_string);
293             #croak "Error string: $error_string";
294 0 0       0 if ($error_string) {
295 0         0 croak "Oh no: $error_string";
296             }
297 0 0       0 if ($try_string =~ m{ \S }xms) {
298 0         0 $try_string =~ s{\A \s+ | \s+ \z}{}xmsg;
299 0         0 croak "Expression error: superfluous text $try_string in expression $string";
300             }
301 0         0 return $parser->expression($string);
302             }
303              
304             # current home-grown version
305             sub parse {
306 539     539 1 26331 my ($string) = @_;
307 539 50       1221 return undef unless defined $string;
308 539         1366 $string =~ s{\A\s+}{}xms; $string =~ s{\s+\z}{}xms;
  539         1090  
309 539 50       1097 return undef if $string eq '';
310 539         1014 my $expression = expression($string);
311 525 100       1212 if ($string =~ m{ \G \s* \S}xms) {
312 4         9 croak "Syntax error: Superfluous text " . _critisize($string);
313             }
314 521         1083 return $expression;
315             }
316              
317             # Starting characters:
318             # identifier: ID_Start
319             # key: ID_Start
320             # number: + - \d
321             # integer: + - \d
322             # funccall: ID_Start
323             # key: ID_Start
324             # string: " ' `
325             # lterm: ( ID_Start $
326             # rterm: ( ID_Start $ " ' ` \d
327             # operand: " ' ` \d ( ID_Start $
328             # alternative: ! " ' ` \d ( ID_Start $
329             # expression: ! " ' ` \d ( ID_Start $
330              
331             # Helper for 'parse'
332             sub expression {
333 568     568 0 1253 my $alternative = alternative($_[0]);
334 555         845 my @others = ();
335             #$_[0] =~ m{\G\s*}gc; # fast forward
336 555         1376 while ($_[0] =~ m{\G \s* ([?:]) \s* }xmsgc) {
337             # another alternative
338 33         50 my $operator = $1;
339 33         69 push @others, ($operator, alternative($_[0]));
340             }
341 554 100       1543 return (@others) ? ['expression', $alternative, @others] : $alternative;
342             }
343              
344             # Helper for 'parse'
345             sub alternative {
346 615 100   615 0 1487 if ($_[0] =~ m{\G \s* (!) \s*}xmsgc) {
347 14         35 return ['not', alternative($_[0])];
348             } else {
349 601         1219 return operand($_[0]);
350             }
351             }
352              
353             # Helper for 'parse'
354             sub operand {
355 601 100   601 0 3276 if ($_[0] =~ m{\G \s* (["'`])}xms) {
    100          
    100          
356 86         219 return string($_[0], $1);
357             } elsif ($_[0] =~ m{\G \s* [\d+-]}xms) {
358 49         97 return number($_[0]);
359             } elsif ($_[0] =~ m{\G \s* [([:alpha:]_\$]}xms) {
360 463         994 my $lterm = lterm($_[0]);
361 460         715 my @rterms = ();
362 460         1234 while ($_[0] =~ m{\G \s* \. \s*}xmsgc) {
363 57         179 push @rterms, rterm($_[0]);
364             }
365 455 100       1504 return @rterms ? ['dot', $lterm, @rterms] : $lterm;
366             } else {
367 3         10 croak q{Syntax error: Operand expected } . _critisize($_[0]);
368             }
369             }
370              
371             # Helper for 'parse'
372             sub lterm {
373 474 100   474 0 2236 if ($_[0] =~ m{ \G \s* \( \s* }xmsgc) {
    100          
    100          
374 10         37 my $expression = expression($_[0]);
375 10 100       41 if ($_[0] =~ m{ \G \s* \) \s* }xmsgc) {
376 9         17 return $expression;
377             } else {
378 1         3 croak q{Syntax error: Unbalanced parentheses: missing a ')' } . _critisize($_[0]);
379             }
380             } elsif ($_[0] =~ m{ \G \s* \$ }xmsgc) {
381 7         17 my $lterm = lterm($_[0]);
382 7         19 return ['env', $lterm];
383             } elsif ($_[0] =~ m{\G \s* [[:alpha:]_] }xms) {
384             # funccall or plain identifier
385 456         1053 my $identifier = identifier($_[0]);
386 456 100       1056 if ($_[0] =~ m{ \G \s* \( \s* }xmsgc) {
387             # argument list, go for funccall
388             #$identifier = $identifier->[1]; # just the name
389 10         20 my @arguments = ();
390 10         40 while ($_[0] =~ m{ \G (?= [^)] ) }xmsgc) {
391 10 100       27 if (@arguments) {
392             # need a ',' before the next argument if we have some already.
393             # trailing ',' are a-ok.
394 4 100       18 $_[0] =~ m{ \s* , [[:space:],]* }xmsgc
395             or croak q{Syntax error: Need commas in argument list } . _critisize($_[0]);
396             }
397 9         26 push @arguments, expression($_[0]);
398             }
399             # trailing ',' are a-ok.
400 9 100       42 if ($_[0] =~ m{ \G [[:space:],]* \) \s* }xmsgc) {
401 8         27 return ['funccall', $identifier, @arguments];
402             } else {
403 1         3 croak q{Syntax error: Unbalanced parentheses: missing a ')' } . _critisize($_[0]);
404             }
405             } else {
406 446         3749 return $identifier;
407             }
408             } else {
409 1         4 croak q{Syntax error: Term expected } . _critisize($_[0]);
410             }
411             }
412              
413             # Helper for 'parse'
414             sub rterm {
415             # second verse: same as the first!
416 57 100   57 0 479 if ($_[0] =~ m{ \G \s* \( \s* }xmsgc) {
    100          
    50          
    100          
    50          
417 4         9 my $expression = expression($_[0]);
418 4 100       15 if ($_[0] =~ m{ \G \s* \) \s* }xmsgc) {
419 3         8 return $expression;
420             } else {
421 1         4 croak q{Syntax error: Unbalanced parentheses: missing a ')' } . _critisize($_[0]);
422             }
423             } elsif ($_[0] =~ m{ \G \s* \$ }xmsgc) {
424             # yes, inside an rterm, it's an lterm, but as a key
425 4         11 my $lterm = lterm($_[0]);
426 3         11 return $lterm;
427             } elsif ($_[0] =~ m{\G \s* (?=["'`])}xmsgc) {
428 0         0 return string($_[0], $1);
429             } elsif ($_[0] =~ m{\G \s* (?=[\d+-])}xmsgc) {
430 6         17 return integer($_[0]);
431             } elsif ($_[0] =~ m{\G \s* [[:alpha:]_] }xms) {
432             # methcall or plain key
433 43         90 my $identifier = identifier($_[0]);
434 43         89 $identifier = $identifier->[1]; # just the name, in any case
435 43 100       164 if ($_[0] =~ m{ \G \s* \( \s* }xmsgc) {
436             # argument list, go for methcall
437 10         19 my @arguments = ();
438 10         35 while ($_[0] =~ m{ \G (?= [^)] ) }xmsgc) {
439 7 100       18 if (@arguments) {
440             # need a ',' before the next argument if we have some already.
441             # trailing ',' are a-ok.
442 2 100       15 $_[0] =~ m{ \s* , [[:space:],]* }xmsgc
443             or croak q{Syntax error: Need commas in argument list } . _critisize($_[0]);
444             }
445 6         16 push @arguments, expression($_[0]);
446             }
447             # trailing ',' are a-ok.
448 9 50       34 if ($_[0] =~ m{ \G [[:space:],]* \) \s* }xmsgc) {
449 9         45 return ['methcall', $identifier, @arguments];
450             } else {
451 0         0 croak q{Syntax error: Unbalanced parentheses: missing a ')' near } . _critisize($_[0]);
452             }
453             } else {
454 33         126 return $identifier;
455             }
456             } else {
457             # this can probably not be reached
458 0         0 croak q{Syntax error: Term expected } . _critisize($_[0]);
459             }
460             }
461              
462             # Helper for 'parse'
463             sub string {
464 86     86 0 100 my ($contents, $delim);
465 86         167 $delim = $_[1];
466 86         134 given ($delim) {
467 86 100       273 when (q{"}) { $_[0] =~ m{ \G \s* " ([^"]*) " \s* }xmsgc and $contents = $1; }
  64         548  
468 22 50       33 when (q{'}) { $_[0] =~ m{ \G \s* ' ([^']*) ' \s* }xmsgc and $contents = $1; }
  1         14  
469 21 50       60 when (q{`}) { $_[0] =~ m{ \G \s* ` ([^`]*) ` \s* }xmsgc and $contents = $1; }
  21         188  
470 0         0 default { die "Internal error: string called with invalid delimiter $delim"; }
  0         0  
471             }
472 86 100       250 if (defined $contents) {
473 85         246 return $contents;
474             } else {
475 1         4 croak qq{Syntax error: Missing string delimiter '$delim' } . _critisize($_[0]);
476             }
477             }
478              
479             # Helper for 'parse'
480             sub identifier {
481 499 50   499 0 1796 if ($_[0] =~ m{ \G ( [[:alpha:]_] [[:alnum:]_]*) \s* }xmsgc) {
482 499         1821 return [ 'env', $1 ];
483             } else {
484             # this can probably never be reached
485 0         0 croak q{Syntax error: Invalid identifier } . _critisize($_[0]);
486             }
487             }
488              
489             # Helper for 'parse'
490             sub number {
491             # TODO: can we get this to commit after the period?
492 49 100   49 0 184 if ($_[0] =~ m{ \G \s* ([+-]? \d+ (?:\.\d+)? ) \s* }xmscg) {
493 47         208 return $1;
494             } else {
495 2         9 croak q{Syntax error: Invalid number } . _critisize($_[0]);
496             }
497             }
498              
499             # Helper for 'parse'
500             sub integer {
501 6 100   6 0 24 if ($_[0] =~ m{ \G \s* ([+-]? \d+ ) \s* }xmscg) {
502 4         19 return $1;
503             } else {
504 2         5 croak q{Syntax error: Invalid integer } . _critisize($_[0]);
505             }
506             }
507              
508             # Helper function: report errors "from the point of parsing"
509             # The entire expression is assumed to be short, and one of many, so the entire
510             # expression is included in the diagnostics to help you find it.
511             sub _critisize {
512 18   100 18   250 return qq{near '} . substr($_[0], pos($_[0]) || 0, 10) . q{' in '} . $_[0] . q{'};
513             }
514              
515              
516             =head2 reduce
517              
518             my $value = Positron::Expression::reduce($tree, $environment);
519              
520             The companion of C, this function takes an abstract parse tree and
521             returns a scalar value. Essentially,
522              
523             my $tree = Positron::Expression::parse($string);
524             my $value = Positron::Expression::reduce($tree, $environment);
525              
526             is equivalent to
527              
528             my $value = Positron::Expression::evaluate($string, $environment);
529              
530             =cut
531              
532             sub reduce {
533 0     0 1 0 my ($tree, $environment) = @_;
534 0         0 return scalar(_evaluate($tree, $environment));
535             }
536              
537             =head2 true
538              
539             In Perl, empty lists and hashes count as false. The only way for C
540             to contain lists and hashes is as array or hash references. However, these count as C
541             in Perl, even if they reference an empty array or hash.
542              
543             To aid decisions in templates, the function C returns a false value for references to
544             empty arrays or hashes, and a true value for non-empty ones.
545             Other values, such as plain scalars, blessed references, subroutine references or C,
546             are returned verbatim.
547             Their truth values are therefore up to Perl (a reference blessed into a package with an
548             overloaded C method may still return false, for example).
549              
550             =cut
551              
552             sub true {
553 82     82 1 125 my ($it) = @_;
554 82 100       152 if (ref($it)) {
555 11 100       38 if (ref($it) eq 'ARRAY') {
    50          
556 7         31 return @$it;
557             } elsif (ref($it) eq 'HASH') {
558 4         23 return scalar(keys %$it);
559             } else {
560 0         0 return $it;
561             }
562             } else {
563 71         224 return $it;
564             }
565             }
566              
567             # Recursive helper version of _evaluate
568             sub _evaluate {
569 1100     1100   1864 my ($tree, $env, $obj) = @_;
570 1100 100       2036 if (not ref($tree)) {
571 578         1459 return $tree;
572             } else {
573 522         1115 my ($operand, @args) = @$tree;
574 522 100       1470 if ($operand eq 'env') {
    100          
    100          
    100          
    100          
    50          
575 433         1083 my $key = _evaluate($args[0], $env);
576 433         1518 return $env->get($key);
577             } elsif ($operand eq 'funccall') {
578 8         13 my $func = shift @args; # probably [env]
579 8         18 $func = _evaluate($func, $env);
580 8 100 100     79 return undef unless $func and ref($func) eq 'CODE'; # skip arguments then, too
581 4         13 @args = map _evaluate($_, $env), @args;
582 4         15 return $func->(@args);
583             } elsif ($operand eq 'methcall') {
584             # On error, do not evaluate arguments !?
585             # Needs $obj argument
586 5 100       19 return undef unless $obj;
587 4         8 my $func = shift @args; # probably literal
588 4         10 $func = _evaluate($func, $env);
589 4 50       11 return undef unless $func;
590 4         12 @args = map _evaluate($_, $env), @args;
591 4 100 100     55 if (blessed($obj) and $obj->can($func)) {
    100 100        
592             # actual method call
593 1         7 return ($obj->can($func))->($obj, @args);
594             } elsif (ref($obj) eq 'HASH' and ref($obj->{$func}) eq 'CODE') {
595             # subroutine inside hash, still ok
596 1         8 return ($obj->{$func})->(@args);
597             } else {
598             # neither, abort
599 2         9 return undef;
600             }
601             } elsif ($operand eq 'not') {
602 12         25 my $what = _evaluate($args[0], $env);
603 12         26 return ! true($what);
604             } elsif ($operand eq 'expression') {
605 17         42 my $left = _evaluate(shift @args, $env);
606 17         44 while (@args) {
607 26         33 my $op = shift @args;
608 26         37 my $right = shift @args;
609 26 100       45 if ($op eq '?') {
610             # and
611 11 100       24 if (true($left)) {
612 6         16 $left = _evaluate($right, $env);
613             }
614             } else {
615             # or
616 15 100       31 if (!true($left)) {
617 7         15 $left = _evaluate($right, $env);
618             }
619             }
620             }
621 17         107 return $left;
622             } elsif ($operand eq 'dot') {
623 47         100 my $left = _evaluate(shift @args, $env);
624 47         188 while (@args) {
625 49 100       227 if (blessed($left)) {
    100          
    100          
626 4         9 my $key = shift @args;
627 4 100 66     31 if (ref($key) and ref($key) eq 'ARRAY' and $key->[0] eq 'methcall' ) {
      66        
628             # Method, like 'funccall' but pass the object as extra parameter
629 2         5 $left = _evaluate($key, $env, $left);
630             } else {
631             # Attribute or similar.
632             # In Perl, still a method (without additional arguments)
633 2         6 $key = _evaluate($key, $env);
634 2 100 66     40 return undef unless defined($key) and $left->can($key);
635 1         6 $left = ($left->can($key))->($left);
636             }
637             } elsif (ref($left) eq 'HASH') {
638 27         46 my $key = shift @args;
639 27 100 66     167 if (ref($key) and ref($key) eq 'ARRAY' and $key->[0] eq 'methcall' ) {
      100        
640             # "Method", i.e. function lookup in hash
641 2         6 $left = _evaluate($key, $env, $left);
642             } else {
643             # Regular hash lookup
644 25         53 $key = _evaluate($key, $env);
645 25 50       79 return undef unless defined($key);
646 25         85 $left = $left->{$key};
647             }
648             } elsif (ref($left) eq 'ARRAY') {
649 9         21 my $key = _evaluate(shift @args, $env);
650 9 100       58 return undef unless defined($key);
651 33     33   652 no warnings 'numeric';
  33         77  
  33         7376  
652 8         31 $left = $left->[ int($key) ];
653             } else {
654 9         22 _warn("Asked to subselect a scalar");
655 9         68 return undef;
656             }
657             }
658 36         258 return $left;
659             }
660             }
661             }
662              
663             # Helper function, if diagnostics are requested, outputs the "less than ideal"
664             # condition the user may find interesting.
665             sub _warn {
666 9     9   12 my ($message) = @_;
667             # TODO: warn the $message if debugging is requested
668 9         12 return;
669             }
670              
671             1; # End of Positron::Expression
672              
673             __END__