File Coverage

blib/lib/Data/ZPath/_Parser.pm
Criterion Covered Total %
statement 202 207 97.5
branch 65 74 87.8
condition 48 102 47.0
subroutine 24 24 100.0
pod n/a
total 339 407 83.2


line stmt bran cond sub pod time code
1 8     8   62 use strict;
  8         21  
  8         358  
2 8     8   42 use warnings;
  8         44  
  8         632  
3              
4             package Data::ZPath::_Parser;
5              
6 8     8   49 use Carp qw(croak);
  8         14  
  8         556  
7 8     8   46 use Data::ZPath::_Lexer;
  8         15  
  8         23116  
8              
9             our $VERSION = '0.001000';
10              
11             sub _parse_top_level_terms {
12 376     376   780 my ( $src ) = @_;
13              
14 376         867 my @parts = _split_top_level_commas($src);
15 376         553 my @terms;
16              
17 376         828 for my $p (@parts) {
18 431         1937 my $lexer = Data::ZPath::_Lexer->new($p);
19 426         1016 my $expr = _parse_expression($lexer);
20 426         1294 $lexer->expect('EOF');
21 422         3100 push @terms, $expr;
22             }
23              
24 367         2389 return \@terms;
25             }
26              
27             sub _split_top_level_commas {
28 376     376   754 my ( $src ) = @_;
29 376         607 my @out;
30              
31 376         642 my $depth_paren = 0;
32 376         574 my $depth_brack = 0;
33 376         633 my $in_string = 0;
34 376         562 my $escape = 0;
35              
36 376         644 my $buf = '';
37 376         2080 my @chars = split //, $src;
38              
39 376         1150 for ( my $i = 0; $i < @chars; $i++ ) {
40 6199         8333 my $c = $chars[$i];
41              
42 6199 100       10047 if ( $in_string ) {
43 857         916 $buf .= $c;
44 857 50       1296 if ( $escape ) {
45 0         0 $escape = 0;
46 0         0 next;
47             }
48 857 50       1217 if ( $c eq '\\' ) { $escape = 1; next; }
  0         0  
  0         0  
49 857 100       1251 if ( $c eq '"' ) { $in_string = 0; next; }
  104         173  
  104         236  
50 753         1235 next;
51             }
52              
53 5342 100       8860 if ( $c eq '"' ) { $in_string = 1; $buf .= $c; next; }
  104         162  
  104         146  
  104         230  
54 5238 100       8661 if ( $c eq '(') { $depth_paren++; $buf .= $c; next; }
  121         222  
  121         312  
  121         333  
55 5117 100       8332 if ( $c eq ')') { $depth_paren--; $buf .= $c; next; }
  121         192  
  121         203  
  121         277  
56 4996 100       8336 if ( $c eq '[' ) { $depth_brack++; $buf .= $c; next; }
  96         175  
  96         178  
  96         303  
57 4900 100       8020 if ( $c eq ']' ) { $depth_brack--; $buf .= $c; next; }
  96         145  
  96         173  
  96         224  
58              
59 4804 100 100     8855 if ( $c eq ',' && $depth_paren == 0 && $depth_brack == 0 ) {
      66        
60 55         117 push @out, _trim($buf);
61 55         100 $buf = '';
62 55         125 next;
63             }
64              
65 4749         9629 $buf .= $c;
66             }
67              
68 376 50       942 push @out, _trim($buf) if length _trim($buf);
69 376         1804 return @out;
70             }
71              
72             sub _trim {
73 807     807   1530 my ( $s ) = @_;
74 807         5553 $s =~ s/^\s+//;
75 807         2017 $s =~ s/\s+$//;
76 807         2534 return $s;
77             }
78              
79             # Expression grammar (recursive descent):
80             # expr := ternary
81             # ternary := or ( WS? '?' WS? expr WS? ':' WS? expr )?
82             # or := and ( '||' and )*
83             # and := bit_or ( '&&' bit_or )*
84             # bit_or := bit_xor ( '|' bit_xor )*
85             # bit_xor := bit_and ( '^' bit_and )*
86             # bit_and := equality ( '&' equality )*
87             # equality := rel ( ('=='|'!=' ) rel )*
88             # rel := add ( ('>='|'<='|'>'|'<') add )*
89             # add := mul (('+'|'-') mul)*
90             # mul := unary (('*'|'/'|'%') unary)*
91             # unary := ('!'|'~') unary | primary
92             # primary := number | string | function | path | '(' expr ')'
93             #
94             # NOTE: per zpath.me, binary ops require whitespace around them.
95             # We enforce this at lexer-time: a binary op token is only produced if
96             # it has whitespace before and after in source.
97              
98 672     672   1501 sub _parse_expression { _parse_ternary(@_) }
99              
100             sub _parse_ternary {
101 672     672   1175 my ( $lx ) = @_;
102 672         1395 my $cond = _parse_or($lx);
103              
104 672 100       1609 if ( $lx->peek_kind eq 'QMARK' ) {
105 7         13 $lx->next_tok; # ?
106 7         24 my $then = _parse_expression($lx);
107 7         37 $lx->expect('COLON');
108 7         10 my $els = _parse_expression($lx);
109 7         24 return { t => 'ternary', c => $cond, a => $then, b => $els };
110             }
111              
112 665         1514 return $cond;
113             }
114              
115             sub _bin_left_assoc {
116 6405     6405   10413 my ( $lx, $next_parser, $ops ) = @_;
117 6405         12686 my $left = $next_parser->($lx);
118              
119 6405         11187 while ( 1 ) {
120 6530         11209 my $k = $lx->peek_kind;
121 6530 100       12958 last unless $ops->{$k};
122 125         269 my $op = $lx->next_tok->{v};
123 125         276 my $right = $next_parser->($lx);
124 125         621 $left = { t => 'bin', op => $op, l => $left, r => $right };
125             }
126              
127 6405         18091 return $left;
128             }
129              
130             sub _parse_or {
131 672     672   1216 my ( $lx ) = @_;
132 672         2330 return _bin_left_assoc($lx, \&_parse_and, { OROR => 1 });
133             }
134              
135             sub _parse_and {
136 676     676   1097 my ( $lx ) = @_;
137 676         1900 return _bin_left_assoc($lx, \&_parse_bitor, { ANDAND => 1 });
138             }
139              
140             sub _parse_bitor {
141 684     684   1104 my ( $lx ) = @_;
142 684         1906 return _bin_left_assoc($lx, \&_parse_bitxor, { BOR => 1 });
143             }
144              
145             sub _parse_bitxor {
146 685     685   1154 my ( $lx ) = @_;
147 685         1900 return _bin_left_assoc($lx, \&_parse_bitand, { BXOR => 1 });
148             }
149              
150             sub _parse_bitand {
151 686     686   1106 my ( $lx ) = @_;
152 686         1872 return _bin_left_assoc($lx, \&_parse_equality, { BAND => 1 });
153             }
154              
155             sub _parse_equality {
156 688     688   1280 my ( $lx ) = @_;
157 688         4570 return _bin_left_assoc($lx, \&_parse_rel, { EQEQ => 1, NEQ => 1 });
158             }
159              
160             sub _parse_rel {
161 766     766   1287 my ( $lx ) = @_;
162 766         3952 return _bin_left_assoc($lx, \&_parse_add, { GE => 1, LE => 1, GT => 1, LT => 1 });
163             }
164              
165             sub _parse_add {
166 769     769   1337 my ( $lx ) = @_;
167 769         2703 return _bin_left_assoc($lx, \&_parse_mul, { PLUS => 1, MINUS => 1 });
168             }
169              
170             sub _parse_mul {
171 779     779   1309 my ( $lx ) = @_;
172 779         2736 return _bin_left_assoc($lx, \&_parse_unary, { STAR => 1, SLASH => 1, PCT => 1 });
173             }
174              
175             sub _parse_unary {
176 805     805   1950 my ( $lx ) = @_;
177 805         2191 my $k = $lx->peek_kind;
178 805 100 100     3490 if ( $k eq 'NOT' || $k eq 'BNOT' ) {
179 8         51 my $op = $lx->next_tok->{v};
180 8         29 my $e = _parse_unary($lx);
181 8         50 return { t => 'un', op => $op, e => $e };
182             }
183 797         1728 return _parse_primary($lx);
184             }
185              
186             sub _parse_primary {
187 797     797   1259 my ( $lx ) = @_;
188 797         1719 my $k = $lx->peek_kind;
189              
190 797 100       1801 if ( $k eq 'NUMBER' ) {
191 143         390 return { t => 'num', v => $lx->next_tok->{v} };
192             }
193 654 100       1289 if ( $k eq 'STRING' ) {
194 137         370 return { t => 'str', v => $lx->next_tok->{v} };
195             }
196 517 100       1059 if ( $k eq 'LPAREN' ) {
197 5         19 $lx->next_tok;
198 5         16 my $e = _parse_expression($lx);
199 5         20 $lx->expect('RPAREN');
200 5         11 return $e;
201             }
202              
203             # Function: NAME '(' ...
204 512 100 100     1498 if ( $k eq 'NAME' && $lx->peek_kind_n(1) eq 'LPAREN' ) {
205 109         252 my $name = $lx->next_tok->{v};
206 109         363 $lx->expect('LPAREN');
207 109         158 my @args;
208 109 100       265 if ( $lx->peek_kind ne 'RPAREN' ) {
209 78         250 push @args, _parse_expression($lx);
210 78         238 while ( $lx->peek_kind eq 'COMMA' ) {
211 45         184 $lx->next_tok;
212 45         109 push @args, _parse_expression($lx);
213             }
214             }
215 109         323 $lx->expect('RPAREN');
216 109         568 return { t => 'fn', n => $name, a => \@args };
217             }
218              
219             # Otherwise treat it as a path-expression
220 403         1063 return _parse_path_expr($lx);
221             }
222              
223             sub _parse_path_expr {
224 403     403   801 my ( $lx ) = @_;
225              
226 403         621 my @segs;
227              
228             # path can start with "/" or with a segment.
229 403 100       2048 if ( $lx->peek_kind eq 'SLASH_PATH' ) {
    100          
230 132         414 $lx->next_tok; # consume '/'
231 132         591 push @segs, { k => 'root', q => [] };
232              
233 132 100       387 if ( $lx->peek_kind eq 'LBRACK' ) {
234 1         5 $segs[-1]->{q} = _parse_qualifiers($lx);
235             }
236              
237 132 50 66     306 if (
      66        
      33        
      33        
      33        
      66        
      66        
      66        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
      33        
238             $lx->peek_kind eq 'EOF'
239             or $lx->peek_kind eq 'COMMA'
240             or $lx->peek_kind eq 'RPAREN'
241             or $lx->peek_kind eq 'RBRACK'
242             or $lx->peek_kind eq 'QMARK'
243             or $lx->peek_kind eq 'COLON'
244             or $lx->peek_kind eq 'EQEQ'
245             or $lx->peek_kind eq 'NEQ'
246             or $lx->peek_kind eq 'GE'
247             or $lx->peek_kind eq 'LE'
248             or $lx->peek_kind eq 'GT'
249             or $lx->peek_kind eq 'LT'
250             or $lx->peek_kind eq 'ANDAND'
251             or $lx->peek_kind eq 'OROR'
252             or $lx->peek_kind eq 'PLUS'
253             or $lx->peek_kind eq 'MINUS'
254             or $lx->peek_kind eq 'STAR'
255             or $lx->peek_kind eq 'SLASH'
256             or $lx->peek_kind eq 'PCT'
257             or $lx->peek_kind eq 'BAND'
258             or $lx->peek_kind eq 'BOR'
259             or $lx->peek_kind eq 'BXOR'
260             ) {
261 10         56 return { t => 'path', s => \@segs };
262             }
263             }
264             elsif ( $lx->peek_kind eq 'LBRACK' ) {
265 2         9 my $seg = { k => 'dot', q => _parse_qualifiers($lx) };
266 2         5 push @segs, $seg;
267 2 0 33     7 return { t => 'path', s => \@segs }
      33        
      0        
268             if $lx->peek_kind eq 'EOF' || $lx->peek_kind eq 'COMMA' || $lx->peek_kind eq 'RPAREN' || $lx->peek_kind eq 'RBRACK';
269             }
270              
271 391 50 33     2227 if ( $lx->peek_kind ne 'SLASH_PATH'
      33        
      33        
      33        
272             && $lx->peek_kind ne 'EOF'
273             && $lx->peek_kind ne 'COMMA'
274             && $lx->peek_kind ne 'RPAREN'
275             && $lx->peek_kind ne 'RBRACK' ) {
276 391         1118 push @segs, _parse_path_segment($lx);
277             }
278              
279 391         1006 while ( $lx->peek_kind eq 'SLASH_PATH' ) {
280 271         699 $lx->next_tok;
281 271 100       551 if ( $lx->peek_kind eq 'LBRACK' ) {
282             # "list/[expr]" implies "*"
283 1         6 my $seg = { k => 'star', q => [] };
284 1         3 $seg->{q} = _parse_qualifiers($lx);
285 1         3 push @segs, $seg;
286 1         4 next;
287             }
288 270         514 push @segs, _parse_path_segment($lx);
289             }
290              
291 391         1853 return { t => 'path', s => \@segs };
292             }
293              
294             sub _parse_path_segment {
295 661     661   1139 my ( $lx ) = @_;
296              
297 661         1274 my $k = $lx->peek_kind;
298              
299 661         5115 my $seg;
300 661 100 66     3830 if ( $k eq 'DOT' ) { $lx->next_tok; $seg = { k => 'dot' }; }
  18 100       66  
  18 100       58  
    100          
    100          
    100          
    100          
    100          
    50          
301 11         36 elsif ( $k eq 'DOTDOT' ) { $lx->next_tok; $seg = { k => 'parent' }; }
  11         38  
302 2         9 elsif ( $k eq 'DOTDOTSTAR' ) { $lx->next_tok; $seg = { k => 'ancestors' }; }
  2         8  
303 44         122 elsif ( $k eq 'STAR_PATH' ) { $lx->next_tok; $seg = { k => 'star' }; }
  44         121  
304 85         290 elsif ( $k eq 'STARSTAR' ) { $lx->next_tok; $seg = { k => 'desc' }; }
  85         281  
305 6         19 elsif ( $k eq 'INDEX' ) { my $i = $lx->next_tok->{v}; $seg = { k => 'index', i => $i }; }
  6         24  
306 22         78 elsif ( $k eq 'NUMBER' ) { my $i = $lx->next_tok->{v}; $seg = { k => 'index', i => $i }; }
  22         65  
307             elsif ( $k eq 'NAME' && $lx->peek_kind_n(1) eq 'LPAREN' ) {
308 7         21 my $name = $lx->next_tok->{v};
309 7         41 $lx->expect('LPAREN');
310 7         11 my @args;
311 7 50       18 if ( $lx->peek_kind ne 'RPAREN' ) {
312 7         25 push @args, _parse_expression($lx);
313 7         22 while ( $lx->peek_kind eq 'COMMA' ) {
314 1         3 $lx->next_tok;
315 1         3 push @args, _parse_expression($lx);
316             }
317             }
318 7         28 $lx->expect('RPAREN');
319 7         34 $seg = { k => 'fnseg', n => $name, a => \@args };
320             }
321 466         929 elsif ( $k eq 'NAME' ) { my $n = $lx->next_tok->{v}; $seg = { k => 'name', n => $n }; }
  466         1679  
322             else {
323 0         0 croak "Unexpected token in path segment: $k";
324             }
325              
326             # optional name#index
327 661 100 100     2213 if ( $seg->{k} eq 'name' && $lx->peek_kind eq 'INDEX' ) {
328 1         4 $seg->{i} = $lx->next_tok->{v};
329             }
330              
331             # qualifiers
332 661         1387 $seg->{q} = _parse_qualifiers($lx);
333              
334 661         1729 return $seg;
335             }
336              
337             sub _parse_qualifiers {
338 665     665   2324 my ( $lx ) = @_;
339 665         955 my @q;
340              
341 665         1427 while ( $lx->peek_kind eq 'LBRACK' ) {
342 96         276 $lx->next_tok;
343 96         216 my $e = _parse_expression($lx);
344 96         345 $lx->expect('RBRACK');
345 96         289 push @q, $e;
346             }
347              
348 665         1677 return \@q;
349             }
350              
351             1;