File Coverage

lib/Type/Parser.pm
Criterion Covered Total %
statement 214 218 99.0
branch 91 102 89.2
condition 59 69 85.5
subroutine 31 31 100.0
pod 3 3 100.0
total 398 423 94.5


line stmt bran cond sub pod time code
1             package Type::Parser;
2              
3 64     64   26444 use 5.008001;
  64         247  
4 64     64   349 use strict;
  64         147  
  64         1452  
5 64     64   365 use warnings;
  64         185  
  64         96248  
6              
7 9     9   1031 sub _croak ($;@) { require Error::TypeTiny; goto \&Error::TypeTiny::croak }
  9         48  
8              
9             our $AUTHORITY = 'cpan:TOBYINK';
10             our $VERSION = '2.002001';
11              
12             $VERSION =~ tr/_//d;
13              
14             # Token types
15             #
16             sub TYPE () { "TYPE" }
17             sub QUOTELIKE () { "QUOTELIKE" }
18             sub STRING () { "STRING" }
19             sub HEXNUM () { "HEXNUM" }
20             sub CLASS () { "CLASS" }
21             sub L_BRACKET () { "L_BRACKET" }
22             sub R_BRACKET () { "R_BRACKET" }
23             sub COMMA () { "COMMA" }
24             sub SLURPY () { "SLURPY" }
25             sub UNION () { "UNION" }
26             sub INTERSECT () { "INTERSECT" }
27             sub SLASH () { "SLASH" }
28             sub NOT () { "NOT" }
29             sub L_PAREN () { "L_PAREN" }
30             sub R_PAREN () { "R_PAREN" }
31             sub MYSTERY () { "MYSTERY" }
32              
33             our @EXPORT_OK = qw( eval_type _std_eval parse extract_type );
34              
35             require Exporter::Tiny;
36             our @ISA = 'Exporter::Tiny';
37              
38             Evaluate: {
39              
40             sub parse {
41 184     184 1 4581 my $str = $_[0];
42 184         750 my $parser = "Type::Parser::AstBuilder"->new( input => $str );
43 184         567 $parser->build;
44 181 100       818 wantarray ? ( $parser->ast, $parser->remainder ) : $parser->ast;
45             }
46            
47             sub extract_type {
48 1     1 1 7 my ( $str, $reg ) = @_;
49 1         4 my ( $parsed, $tail ) = parse( $str );
50             wantarray
51 1 50       11 ? ( _eval_type( $parsed, $reg ), $tail )
52             : _eval_type( $parsed, $reg );
53             }
54            
55             sub eval_type {
56 69     69 1 174 my ( $str, $reg ) = @_;
57 69         132 my ( $parsed, $tail ) = parse( $str );
58 66 100       204 _croak( "Unexpected tail on type expression: $tail" ) if $tail =~ /\S/sm;
59 64         132 return _eval_type( $parsed, $reg );
60             }
61            
62             my $std;
63            
64             sub _std_eval {
65 52     52   3660 require Type::Registry;
66 52 100       141 unless ( $std ) {
67 1         4 $std = "Type::Registry"->new;
68 1         5 $std->add_types( -Standard );
69             }
70 52         106 eval_type( $_[0], $std );
71             }
72            
73             sub _eval_type {
74 312     312   471 my ( $node, $reg ) = @_;
75            
76 312         467 $node = _simplify_expression( $node );
77            
78 312 100       533 if ( $node->{type} eq "list" ) {
79 59         74 return map _eval_type( $_, $reg ), @{ $node->{list} };
  59         235  
80             }
81            
82 253 100       412 if ( $node->{type} eq "union" ) {
83 18         33 return $reg->_make_union_by_overload( map _eval_type( $_, $reg ), @{ $node->{union} } );
  18         54  
84             }
85            
86 235 100       389 if ( $node->{type} eq "intersect" ) {
87             return $reg->_make_intersection_by_overload(
88             map _eval_type( $_, $reg ),
89 8         11 @{ $node->{intersect} }
  8         23  
90             );
91             }
92            
93 227 100       391 if ( $node->{type} eq "slash" ) {
94 2         5 my @types = map _eval_type( $_, $reg ), @{ $node->{slash} };
  2         19  
95 2 50       31 _croak( "Expected exactly two types joined with slash operator" )
96             unless @types == 2;
97 2         14 return $types[0] / $types[1];
98             }
99            
100 225 100       369 if ( $node->{type} eq "slurpy" ) {
101 2         11 require Types::Standard;
102 2         11 return Types::Standard::Slurpy()->of( _eval_type( $node->{of}, $reg ) );
103             }
104            
105 223 100       360 if ( $node->{type} eq "complement" ) {
106 4         9 return _eval_type( $node->{of}, $reg )->complementary_type;
107             }
108            
109 219 100       341 if ( $node->{type} eq "parameterized" ) {
110 53         159 my $base = _eval_type( $node->{base}, $reg );
111            
112 53 50 66     144 return $base unless $base->is_parameterizable || $node->{params};
113             return $base->parameterize(
114 51 100       170 $node->{params} ? _eval_type( $node->{params}, $reg ) : () );
115             }
116            
117 166 100 66     463 if ( $node->{type} eq "primary" and $node->{token}->type eq CLASS ) {
118             my $class = substr(
119             $node->{token}->spelling,
120             0,
121 8         25 length( $node->{token}->spelling ) - 2
122             );
123 8         31 return $reg->make_class_type( $class );
124             }
125            
126 158 100 66     422 if ( $node->{type} eq "primary" and $node->{token}->type eq QUOTELIKE ) {
127 12         26 return eval( $node->{token}->spelling ); #ARGH
128             }
129            
130 146 100 66     418 if ( $node->{type} eq "primary" and $node->{token}->type eq STRING ) {
131 6         13 return $node->{token}->spelling;
132             }
133            
134 140 100 66     341 if ( $node->{type} eq "primary" and $node->{token}->type eq HEXNUM ) {
135 3         4 my $sign = '+';
136 3         7 my $spelling = $node->{token}->spelling;
137 3 100       9 if ( $spelling =~ /^[+-]/ ) {
138 1         5 $sign = substr( $spelling, 0, 1);
139 1         2 $spelling = substr( $spelling, 1 );
140             }
141             return (
142 3 100       21 ( $sign eq '-' ) ? ( 0 - hex($spelling) ) : hex($spelling)
143             );
144             }
145            
146 137 50 33     354 if ( $node->{type} eq "primary" and $node->{token}->type eq TYPE ) {
147 137         224 my $t = $node->{token}->spelling;
148 137 50       471 my $r =
149             ( $t =~ /^(.+)::(\w+)$/ )
150             ? $reg->foreign_lookup( $t, 1 )
151             : $reg->simple_lookup( $t, 1 );
152 137 100       436 $r or _croak( "%s is not a known type constraint", $node->{token}->spelling );
153 133         433 return $r;
154             }
155             } #/ sub _eval_type
156            
157             sub _simplify_expression {
158 312     312   352 my $expr = shift;
159            
160 312 100 100     711 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq COMMA ) {
161 10         22 return _simplify( "list", COMMA, $expr );
162             }
163            
164 302 100 100     601 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq UNION ) {
165 18         39 return _simplify( "union", UNION, $expr );
166             }
167            
168 284 100 100     520 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq INTERSECT ) {
169 8         17 return _simplify( "intersect", INTERSECT, $expr );
170             }
171            
172 276 100 66     478 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq SLASH ) {
173 2         23 return _simplify( "slash", SLASH, $expr );
174             }
175            
176 274         342 return $expr;
177             } #/ sub _simplify_expression
178            
179             sub _simplify {
180 64     64   623 no warnings 'recursion';
  64         482  
  64         78075  
181 52     52   81 my $type = shift;
182 52         62 my $op = shift;
183            
184 52         61 my @list;
185 52         100 for my $expr ( $_[0]{lhs}, $_[0]{rhs} ) {
186 104 100 100     245 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq $op ) {
187 14         28 my $simple = _simplify( $type, $op, $expr );
188 14         17 push @list, @{ $simple->{$type} };
  14         40  
189             }
190             else {
191 90         144 push @list, $expr;
192             }
193             }
194            
195 52         172 return { type => $type, $type => \@list };
196             } #/ sub _simplify
197             } #/ Evaluate:
198              
199             {
200             package Type::Parser::AstBuilder;
201            
202             our $AUTHORITY = 'cpan:TOBYINK';
203             our $VERSION = '2.002001';
204            
205             $VERSION =~ tr/_//d;
206            
207             sub new {
208 184     184   316 my $class = shift;
209 184         598 bless {@_}, $class;
210             }
211            
212             our %precedence = (
213            
214             # Type::Parser::COMMA() , 1 ,
215             Type::Parser::SLASH(), 1,
216             Type::Parser::UNION(), 2,
217             Type::Parser::INTERSECT(), 3,
218             Type::Parser::NOT(), 4,
219             );
220            
221             sub _parse_primary {
222 614     614   860 my $self = shift;
223 614         2082 my $tokens = $self->{tokens};
224            
225 614         1250 $tokens->assert_not_empty;
226            
227 614 100       1354 if ( $tokens->peek( 0 )->type eq Type::Parser::NOT ) {
228 4         12 $tokens->eat( Type::Parser::NOT );
229 4         10 $tokens->assert_not_empty;
230             return {
231 4         11 type => "complement",
232             of => $self->_parse_primary,
233             };
234             }
235            
236 610 100       1089 if ( $tokens->peek( 0 )->type eq Type::Parser::SLURPY ) {
237 2         5 $tokens->eat( Type::Parser::SLURPY );
238 2         6 $tokens->assert_not_empty;
239             return {
240 2         10 type => "slurpy",
241             of => $self->_parse_primary,
242             };
243             }
244            
245 608 100       1154 if ( $tokens->peek( 0 )->type eq Type::Parser::L_PAREN ) {
246 9         19 $tokens->eat( Type::Parser::L_PAREN );
247 9         20 my $r = $self->_parse_expression;
248 9         21 $tokens->eat( Type::Parser::R_PAREN );
249 9         26 return $r;
250             }
251            
252 599 100 100     1121 if ( $tokens->peek( 1 )
      100        
253             and $tokens->peek( 0 )->type eq Type::Parser::TYPE
254             and $tokens->peek( 1 )->type eq Type::Parser::L_BRACKET )
255             {
256 217         559 my $base = { type => "primary", token => $tokens->eat( Type::Parser::TYPE ) };
257 217         560 $tokens->eat( Type::Parser::L_BRACKET );
258 217         505 $tokens->assert_not_empty;
259            
260 217         638 local $precedence{ Type::Parser::COMMA() } = 1;
261            
262 217         383 my $params = undef;
263 217 100       1836 if ( $tokens->peek( 0 )->type eq Type::Parser::R_BRACKET ) {
264 4         16 $tokens->eat( Type::Parser::R_BRACKET );
265             }
266             else {
267 213         1723 $params = $self->_parse_expression;
268             $params = { type => "list", list => [$params] }
269 213 50       1536 unless $params->{type} eq "list";
270 213         541 $tokens->eat( Type::Parser::R_BRACKET );
271             }
272             return {
273 217         1243 type => "parameterized",
274             base => $base,
275             params => $params,
276             };
277             } #/ if ( $tokens->peek( 1 ...))
278            
279 382         913 my $type = $tokens->peek( 0 )->type;
280 382 100 100     1445 if ( $type eq Type::Parser::TYPE
      100        
      100        
      100        
281             or $type eq Type::Parser::QUOTELIKE
282             or $type eq Type::Parser::STRING
283             or $type eq Type::Parser::HEXNUM
284             or $type eq Type::Parser::CLASS )
285             {
286 379         741 return { type => "primary", token => $tokens->eat };
287             }
288            
289             Type::Parser::_croak(
290 3         12 "Unexpected token in primary type expression; got '%s'",
291             $tokens->peek( 0 )->spelling
292             );
293             } #/ sub _parse_primary
294            
295             sub _parse_expression_1 {
296 406     406   611 my $self = shift;
297 406         608 my $tokens = $self->{tokens};
298            
299 406         727 my ( $lhs, $min_p ) = @_;
300 406   100     738 while ( !$tokens->empty
      66        
301             and defined( $precedence{ $tokens->peek( 0 )->type } )
302             and $precedence{ $tokens->peek( 0 )->type } >= $min_p )
303             {
304 202         467 my $op = $tokens->eat;
305 202         446 my $rhs = $self->_parse_primary;
306            
307 202   100     529 while ( !$tokens->empty
      100        
308             and defined( $precedence{ $tokens->peek( 0 )->type } )
309             and $precedence{ $tokens->peek( 0 )->type } > $precedence{ $op->type } )
310             {
311 3         10 my $lookahead = $tokens->peek( 0 );
312 3         14 $rhs = $self->_parse_expression_1( $rhs, $precedence{ $lookahead->type } );
313             }
314            
315             $lhs = {
316 202         951 type => "expression",
317             op => $op,
318             lhs => $lhs,
319             rhs => $rhs,
320             };
321             } #/ while ( !$tokens->empty and...)
322 406         1041 return $lhs;
323             } #/ sub _parse_expression_1
324            
325             sub _parse_expression {
326 406     406   555 my $self = shift;
327 406         601 my $tokens = $self->{tokens};
328            
329 406         2119 return $self->_parse_expression_1( $self->_parse_primary, 0 );
330             }
331            
332             sub build {
333 184     184   282 my $self = shift;
334             $self->{tokens} =
335 184         875 "Type::Parser::TokenStream"->new( remaining => $self->{input} );
336 184         463 $self->{ast} = $self->_parse_expression;
337             }
338            
339             sub ast {
340 181     181   1243 $_[0]{ast};
341             }
342            
343             sub remainder {
344 69     69   145 $_[0]{tokens}->remainder;
345             }
346             }
347              
348             {
349             package Type::Parser::Token;
350            
351             our $AUTHORITY = 'cpan:TOBYINK';
352             our $VERSION = '2.002001';
353            
354             $VERSION =~ tr/_//d;
355            
356 6080     6080   20104 sub type { $_[0][0] }
357 490     490   9637 sub spelling { $_[0][1] }
358             }
359              
360             {
361             package Type::Parser::TokenStream;
362            
363             our $AUTHORITY = 'cpan:TOBYINK';
364             our $VERSION = '2.002001';
365            
366             $VERSION =~ tr/_//d;
367            
368 64     64   579 use Scalar::Util qw(looks_like_number);
  64         174  
  64         89642  
369            
370             sub new {
371 184     184   295 my $class = shift;
372 184         849 bless { stack => [], done => [], @_ }, $class;
373             }
374            
375             sub peek {
376 6585     6585   7619 my $self = shift;
377 6585         7169 my $ahead = $_[0];
378            
379 6585   100     8907 while ( $self->_stack_size <= $ahead and length $self->{remaining} ) {
380 1264         2208 $self->_stack_extend;
381             }
382            
383 6585         7799 my @tokens = grep ref, @{ $self->{stack} };
  6585         11630  
384 6585         17748 return $tokens[$ahead];
385             } #/ sub peek
386            
387             sub empty {
388 1650     1650   2315 my $self = shift;
389 1650         2505 not $self->peek( 0 );
390             }
391            
392             sub eat {
393 1256     1256   1531 my $self = shift;
394 1256 50       1791 $self->_stack_extend unless $self->_stack_size;
395 1256         1609 my $r;
396 1256         1603 while ( defined( my $item = shift @{ $self->{stack} } ) ) {
  1282         2667  
397 1282         1414 push @{ $self->{done} }, $item;
  1282         2114  
398 1282 100       2266 if ( ref $item ) {
399 1256         1420 $r = $item;
400 1256         1781 last;
401             }
402             }
403            
404 1256 50 66     2984 if ( @_ and $_[0] ne $r->type ) {
405 0         0 unshift @{ $self->{stack} }, pop @{ $self->{done} }; # uncoverable statement
  0         0  
  0         0  
406 0         0 Type::Parser::_croak( "Expected $_[0]; got " . $r->type ); # uncoverable statement
407             }
408            
409 1256         3068 return $r;
410             } #/ sub eat
411            
412             sub assert_not_empty {
413 837     837   989 my $self = shift;
414 837 50       1317 Type::Parser::_croak( "Expected token; got empty string" ) if $self->empty;
415             }
416            
417             sub _stack_size {
418 9105     9105   9921 my $self = shift;
419 9105         9335 scalar grep ref, @{ $self->{stack} };
  9105         26459  
420             }
421            
422             sub _stack_extend {
423 1264     1264   1572 my $self = shift;
424 1264         1417 push @{ $self->{stack} }, $self->_read_token;
  1264         2247  
425 1264         4408 my ( $space ) = ( $self->{remaining} =~ m/^([\s\n\r]*)/sm );
426 1264 100       4033 return unless length $space;
427 32         44 push @{ $self->{stack} }, $space;
  32         62  
428 32         84 substr( $self->{remaining}, 0, length $space ) = "";
429             }
430            
431             sub remainder {
432 69     69   95 my $self = shift;
433             return join "",
434 80 100       541 map { ref( $_ ) ? $_->spelling : $_ }
435 69         90 ( @{ $self->{stack} }, $self->{remaining} );
  69         121  
436             }
437            
438             my %punctuation = (
439             '[' => bless( [ Type::Parser::L_BRACKET, "[" ], "Type::Parser::Token" ),
440             ']' => bless( [ Type::Parser::R_BRACKET, "]" ], "Type::Parser::Token" ),
441             '(' => bless( [ Type::Parser::L_PAREN, "[" ], "Type::Parser::Token" ),
442             ')' => bless( [ Type::Parser::R_PAREN, "]" ], "Type::Parser::Token" ),
443             ',' => bless( [ Type::Parser::COMMA, "," ], "Type::Parser::Token" ),
444             '=>' => bless( [ Type::Parser::COMMA, "=>" ], "Type::Parser::Token" ),
445             'slurpy' => bless( [ Type::Parser::SLURPY, "slurpy" ], "Type::Parser::Token" ),
446             '|' => bless( [ Type::Parser::UNION, "|" ], "Type::Parser::Token" ),
447             '&' => bless( [ Type::Parser::INTERSECT, "&" ], "Type::Parser::Token" ),
448             '/' => bless( [ Type::Parser::SLASH, "/" ], "Type::Parser::Token" ),
449             '~' => bless( [ Type::Parser::NOT, "~" ], "Type::Parser::Token" ),
450             );
451            
452             sub _read_token {
453 1264     1264   1501 my $self = shift;
454            
455 1264 50       2298 return if $self->{remaining} eq "";
456            
457             # Punctuation
458             #
459            
460 1264 100       3929 if ( $self->{remaining} =~ /^( => | [()\]\[|&~,\/] )/xsm ) {
461 660         1401 my $spelling = $1;
462 660         1283 substr( $self->{remaining}, 0, length $spelling ) = "";
463 660         1472 return $punctuation{$spelling};
464             }
465            
466 604 100       1695 if ( $self->{remaining} =~ /\A\s*[q'"]/sm ) {
467 130         17780 require Text::Balanced;
468 130 50       381400 if ( my $quotelike = Text::Balanced::extract_quotelike( $self->{remaining} ) ) {
469 130         10893 return bless( [ Type::Parser::QUOTELIKE, $quotelike ], "Type::Parser::Token" );
470             }
471             }
472            
473 474 100       1727 if ( $self->{remaining} =~ /^([+-]?[\w:.+]+)/sm ) {
474 471         1116 my $spelling = $1;
475 471         1058 substr( $self->{remaining}, 0, length $spelling ) = "";
476            
477 471 100       3297 if ( $spelling =~ /::$/sm ) {
    100          
    100          
    100          
    100          
478 10         60 return bless( [ Type::Parser::CLASS, $spelling ], "Type::Parser::Token" );
479             }
480             elsif ( $spelling =~ /^[+-]?0x[0-9A-Fa-f]+$/sm ) {
481 3         10 return bless( [ Type::Parser::HEXNUM, $spelling ], "Type::Parser::Token" );
482             }
483             elsif ( looks_like_number( $spelling ) ) {
484 5         20 return bless( [ Type::Parser::STRING, $spelling ], "Type::Parser::Token" );
485             }
486             elsif ( $self->{remaining} =~ /^\s*=>/sm ) # peek ahead
487             {
488 4         13 return bless( [ Type::Parser::STRING, $spelling ], "Type::Parser::Token" );
489             }
490             elsif ( $spelling eq "slurpy" ) {
491 2         7 return $punctuation{$spelling};
492             }
493            
494 447         1882 return bless( [ Type::Parser::TYPE, $spelling ], "Type::Parser::Token" );
495             } #/ if ( $self->{remaining...})
496            
497 3         11 my $rest = $self->{remaining};
498 3         8 $self->{remaining} = "";
499 3         13 return bless( [ Type::Parser::MYSTERY, $rest ], "Type::Parser::Token" );
500             } #/ sub _read_token
501             }
502              
503             1;
504              
505             __END__