File Coverage

lib/Type/Parser.pm
Criterion Covered Total %
statement 214 218 99.0
branch 92 102 90.2
condition 59 69 85.5
subroutine 31 31 100.0
pod 3 3 100.0
total 399 423 94.8


line stmt bran cond sub pod time code
1             package Type::Parser;
2              
3 79     79   29559 use 5.008001;
  79         323  
4 79     79   504 use strict;
  79         185  
  79         1835  
5 79     79   490 use warnings;
  79         204  
  79         117375  
6              
7 11     11   65 sub _croak ($;@) { require Error::TypeTiny; goto \&Error::TypeTiny::croak }
  11         70  
8              
9             our $AUTHORITY = 'cpan:TOBYINK';
10             our $VERSION = '2.003_000';
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 261     261 1 6494 my $str = $_[0];
42 261         1233 my $parser = "Type::Parser::AstBuilder"->new( input => $str );
43 261         924 $parser->build;
44 258 100       1237 wantarray ? ( $parser->ast, $parser->remainder ) : $parser->ast;
45             }
46            
47             sub extract_type {
48 1     1 1 10 my ( $str, $reg ) = @_;
49 1         3 my ( $parsed, $tail ) = parse( $str );
50             wantarray
51 1 50       8 ? ( _eval_type( $parsed, $reg ), $tail )
52             : _eval_type( $parsed, $reg );
53             }
54            
55             sub eval_type {
56 113     113 1 313 my ( $str, $reg ) = @_;
57 113         313 my ( $parsed, $tail ) = parse( $str );
58 110 100       388 _croak( "Unexpected tail on type expression: $tail" ) if $tail =~ /\S/sm;
59 108         319 return _eval_type( $parsed, $reg );
60             }
61            
62             my $std;
63            
64             sub _std_eval {
65 53     53   5078 require Type::Registry;
66 53 100       146 unless ( $std ) {
67 2         9 $std = "Type::Registry"->new;
68 2         9 $std->add_types( -Standard );
69             }
70 53         129 eval_type( $_[0], $std );
71             }
72            
73             sub _eval_type {
74 437     437   741 my ( $node, $reg ) = @_;
75            
76 437         729 $node = _simplify_expression( $node );
77            
78 437 100       910 if ( $node->{type} eq "list" ) {
79 72         103 return map _eval_type( $_, $reg ), @{ $node->{list} };
  72         284  
80             }
81            
82 365 100       733 if ( $node->{type} eq "union" ) {
83 39         68 return $reg->_make_union_by_overload( map _eval_type( $_, $reg ), @{ $node->{union} } );
  39         129  
84             }
85            
86 326 100       631 if ( $node->{type} eq "intersect" ) {
87             return $reg->_make_intersection_by_overload(
88             map _eval_type( $_, $reg ),
89 8         11 @{ $node->{intersect} }
  8         22  
90             );
91             }
92            
93 318 100       593 if ( $node->{type} eq "slash" ) {
94 2         4 my @types = map _eval_type( $_, $reg ), @{ $node->{slash} };
  2         19  
95 2 50       26 _croak( "Expected exactly two types joined with slash operator" )
96             unless @types == 2;
97 2         32 return $types[0] / $types[1];
98             }
99            
100 316 100       644 if ( $node->{type} eq "slurpy" ) {
101 2         21 require Types::Standard;
102 2         9 return Types::Standard::Slurpy()->of( _eval_type( $node->{of}, $reg ) );
103             }
104            
105 314 100       574 if ( $node->{type} eq "complement" ) {
106 4         25 return _eval_type( $node->{of}, $reg )->complementary_type;
107             }
108            
109 310 100       593 if ( $node->{type} eq "parameterized" ) {
110 66         331 my $base = _eval_type( $node->{base}, $reg );
111            
112 66 50 66     203 return $base unless $base->is_parameterizable || $node->{params};
113             return $base->parameterize(
114 64 100       262 $node->{params} ? _eval_type( $node->{params}, $reg ) : () );
115             }
116            
117 244 100 66     782 if ( $node->{type} eq "primary" and $node->{token}->type eq CLASS ) {
118             my $class = substr(
119             $node->{token}->spelling,
120             0,
121 12         34 length( $node->{token}->spelling ) - 2
122             );
123 12         57 return $reg->make_class_type( $class );
124             }
125            
126 232 100 66     690 if ( $node->{type} eq "primary" and $node->{token}->type eq QUOTELIKE ) {
127 12         20 return eval( $node->{token}->spelling ); #ARGH
128             }
129            
130 220 100 66     632 if ( $node->{type} eq "primary" and $node->{token}->type eq STRING ) {
131 6         13 return $node->{token}->spelling;
132             }
133            
134 214 100 66     721 if ( $node->{type} eq "primary" and $node->{token}->type eq HEXNUM ) {
135 3         5 my $sign = '+';
136 3         6 my $spelling = $node->{token}->spelling;
137 3 100       12 if ( $spelling =~ /^[+-]/ ) {
138 1         4 $sign = substr( $spelling, 0, 1);
139 1         3 $spelling = substr( $spelling, 1 );
140             }
141             return (
142 3 100       19 ( $sign eq '-' ) ? ( 0 - hex($spelling) ) : hex($spelling)
143             );
144             }
145            
146 211 50 33     588 if ( $node->{type} eq "primary" and $node->{token}->type eq TYPE ) {
147 211         448 my $t = $node->{token}->spelling;
148 211 100       977 my $r =
149             ( $t =~ /^(.+)::(\w+)$/ )
150             ? $reg->foreign_lookup( $t, 1 )
151             : $reg->simple_lookup( $t, 1 );
152 211 100       853 $r or _croak( "%s is not a known type constraint", $node->{token}->spelling );
153 205         880 return $r;
154             }
155             } #/ sub _eval_type
156            
157             sub _simplify_expression {
158 437     437   634 my $expr = shift;
159            
160 437 100 100     1189 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq COMMA ) {
161 10         32 return _simplify( "list", COMMA, $expr );
162             }
163            
164 427 100 100     992 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq UNION ) {
165 39         130 return _simplify( "union", UNION, $expr );
166             }
167            
168 388 100 100     897 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq INTERSECT ) {
169 8         17 return _simplify( "intersect", INTERSECT, $expr );
170             }
171            
172 380 100 66     843 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq SLASH ) {
173 2         25 return _simplify( "slash", SLASH, $expr );
174             }
175            
176 378         531 return $expr;
177             } #/ sub _simplify_expression
178            
179             sub _simplify {
180 79     79   852 no warnings 'recursion';
  79         271  
  79         95376  
181 73     73   119 my $type = shift;
182 73         98 my $op = shift;
183            
184 73         100 my @list;
185 73         185 for my $expr ( $_[0]{lhs}, $_[0]{rhs} ) {
186 146 100 100     382 if ( $expr->{type} eq "expression" and $expr->{op}[0] eq $op ) {
187 14         30 my $simple = _simplify( $type, $op, $expr );
188 14         23 push @list, @{ $simple->{$type} };
  14         41  
189             }
190             else {
191 132         242 push @list, $expr;
192             }
193             }
194            
195 73         310 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.003_000';
204            
205             $VERSION =~ tr/_//d;
206            
207             sub new {
208 261     261   505 my $class = shift;
209 261         947 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 807     807   1089 my $self = shift;
223 807         1238 my $tokens = $self->{tokens};
224            
225 807         1766 $tokens->assert_not_empty;
226            
227 807 100       1789 if ( $tokens->peek( 0 )->type eq Type::Parser::NOT ) {
228 4         22 $tokens->eat( Type::Parser::NOT );
229 4         9 $tokens->assert_not_empty;
230             return {
231 4         14 type => "complement",
232             of => $self->_parse_primary,
233             };
234             }
235            
236 803 100       1578 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         11 type => "slurpy",
241             of => $self->_parse_primary,
242             };
243             }
244            
245 801 100       1510 if ( $tokens->peek( 0 )->type eq Type::Parser::L_PAREN ) {
246 9         20 $tokens->eat( Type::Parser::L_PAREN );
247 9         23 my $r = $self->_parse_expression;
248 9         22 $tokens->eat( Type::Parser::R_PAREN );
249 9         22 return $r;
250             }
251            
252 792 100 100     1520 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 265         1144 my $base = { type => "primary", token => $tokens->eat( Type::Parser::TYPE ) };
257 265         739 $tokens->eat( Type::Parser::L_BRACKET );
258 265         681 $tokens->assert_not_empty;
259            
260 265         827 local $precedence{ Type::Parser::COMMA() } = 1;
261            
262 265         494 my $params = undef;
263 265 100       607 if ( $tokens->peek( 0 )->type eq Type::Parser::R_BRACKET ) {
264 4         9 $tokens->eat( Type::Parser::R_BRACKET );
265             }
266             else {
267 261         882 $params = $self->_parse_expression;
268             $params = { type => "list", list => [$params] }
269 261 50       1330 unless $params->{type} eq "list";
270 261         715 $tokens->eat( Type::Parser::R_BRACKET );
271             }
272             return {
273 265         1514 type => "parameterized",
274             base => $base,
275             params => $params,
276             };
277             } #/ if ( $tokens->peek( 1 ...))
278            
279 527         1185 my $type = $tokens->peek( 0 )->type;
280 527 100 100     1991 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 524         1119 return { type => "primary", token => $tokens->eat };
287             }
288            
289             Type::Parser::_croak(
290 3         10 "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 531     531   861 my $self = shift;
297 531         914 my $tokens = $self->{tokens};
298            
299 531         1047 my ( $lhs, $min_p ) = @_;
300 531   100     965 while ( !$tokens->empty
      66        
301             and defined( $precedence{ $tokens->peek( 0 )->type } )
302             and $precedence{ $tokens->peek( 0 )->type } >= $min_p )
303             {
304 270         692 my $op = $tokens->eat;
305 270         644 my $rhs = $self->_parse_primary;
306            
307 270   100     742 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         12 my $lookahead = $tokens->peek( 0 );
312 3         6 $rhs = $self->_parse_expression_1( $rhs, $precedence{ $lookahead->type } );
313             }
314            
315             $lhs = {
316 270         1321 type => "expression",
317             op => $op,
318             lhs => $lhs,
319             rhs => $rhs,
320             };
321             } #/ while ( !$tokens->empty and...)
322 531         1555 return $lhs;
323             } #/ sub _parse_expression_1
324            
325             sub _parse_expression {
326 531     531   829 my $self = shift;
327 531         867 my $tokens = $self->{tokens};
328            
329 531         1480 return $self->_parse_expression_1( $self->_parse_primary, 0 );
330             }
331            
332             sub build {
333 261     261   475 my $self = shift;
334             $self->{tokens} =
335 261         1414 "Type::Parser::TokenStream"->new( remaining => $self->{input} );
336 261         806 $self->{ast} = $self->_parse_expression;
337             }
338            
339             sub ast {
340 258     258   1608 $_[0]{ast};
341             }
342            
343             sub remainder {
344 113     113   301 $_[0]{tokens}->remainder;
345             }
346             }
347              
348             {
349             package Type::Parser::Token;
350            
351             our $AUTHORITY = 'cpan:TOBYINK';
352             our $VERSION = '2.003_000';
353            
354             $VERSION =~ tr/_//d;
355            
356 7993     7993   28575 sub type { $_[0][0] }
357 656     656   12012 sub spelling { $_[0][1] }
358             }
359              
360             {
361             package Type::Parser::TokenStream;
362            
363             our $AUTHORITY = 'cpan:TOBYINK';
364             our $VERSION = '2.003_000';
365            
366             $VERSION =~ tr/_//d;
367            
368 79     79   887 use Scalar::Util qw(looks_like_number);
  79         244  
  79         111521  
369            
370             sub new {
371 261     261   491 my $class = shift;
372 261         1683 bless { stack => [], done => [], @_ }, $class;
373             }
374            
375             sub peek {
376 8595     8595   10284 my $self = shift;
377 8595         9721 my $ahead = $_[0];
378            
379 8595   100     12018 while ( $self->_stack_size <= $ahead and length $self->{remaining} ) {
380 1621         2981 $self->_stack_extend;
381             }
382            
383 8595         10633 my @tokens = grep ref, @{ $self->{stack} };
  8595         15982  
384 8595         23103 return $tokens[$ahead];
385             } #/ sub peek
386            
387             sub empty {
388 2152     2152   2776 my $self = shift;
389 2152         3301 not $self->peek( 0 );
390             }
391            
392             sub eat {
393 1613     1613   2169 my $self = shift;
394 1613 50       2327 $self->_stack_extend unless $self->_stack_size;
395 1613         2125 my $r;
396 1613         1959 while ( defined( my $item = shift @{ $self->{stack} } ) ) {
  1639         3543  
397 1639         2188 push @{ $self->{done} }, $item;
  1639         2856  
398 1639 100       2991 if ( ref $item ) {
399 1613         1998 $r = $item;
400 1613         2304 last;
401             }
402             }
403            
404 1613 50 66     3909 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 1613         4391 return $r;
410             } #/ sub eat
411            
412             sub assert_not_empty {
413 1078     1078   1399 my $self = shift;
414 1078 50       1811 Type::Parser::_croak( "Expected token; got empty string" ) if $self->empty;
415             }
416            
417             sub _stack_size {
418 11829     11829   13251 my $self = shift;
419 11829         13090 scalar grep ref, @{ $self->{stack} };
  11829         35737  
420             }
421            
422             sub _stack_extend {
423 1621     1621   2058 my $self = shift;
424 1621         1909 push @{ $self->{stack} }, $self->_read_token;
  1621         3037  
425 1621         5697 my ( $space ) = ( $self->{remaining} =~ m/^([\s\n\r]*)/sm );
426 1621 100       4923 return unless length $space;
427 32         44 push @{ $self->{stack} }, $space;
  32         71  
428 32         88 substr( $self->{remaining}, 0, length $space ) = "";
429             }
430            
431             sub remainder {
432 113     113   194 my $self = shift;
433             return join "",
434 124 100       1096 map { ref( $_ ) ? $_->spelling : $_ }
435 113         184 ( @{ $self->{stack} }, $self->{remaining} );
  113         253  
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 1621     1621   2006 my $self = shift;
454            
455 1621 50       3258 return if $self->{remaining} eq "";
456            
457             # Punctuation
458             #
459            
460 1621 100       5380 if ( $self->{remaining} =~ /^( => | [()\]\[|&~,\/] )/xsm ) {
461 824         1744 my $spelling = $1;
462 824         1726 substr( $self->{remaining}, 0, length $spelling ) = "";
463 824         2028 return $punctuation{$spelling};
464             }
465            
466 797 100       2411 if ( $self->{remaining} =~ /\A\s*[q'"]/sm ) {
467 153         22921 require Text::Balanced;
468 153 50       421692 if ( my $quotelike = Text::Balanced::extract_quotelike( $self->{remaining} ) ) {
469 153         12513 return bless( [ Type::Parser::QUOTELIKE, $quotelike ], "Type::Parser::Token" );
470             }
471             }
472            
473 644 100       2402 if ( $self->{remaining} =~ /^([+-]?[\w:.+]+)/sm ) {
474 641         1611 my $spelling = $1;
475 641         1621 substr( $self->{remaining}, 0, length $spelling ) = "";
476            
477 641 100       4639 if ( $spelling =~ /::$/sm ) {
    100          
    100          
    100          
    100          
478 14         57 return bless( [ Type::Parser::CLASS, $spelling ], "Type::Parser::Token" );
479             }
480             elsif ( $spelling =~ /^[+-]?0x[0-9A-Fa-f]+$/sm ) {
481 3         11 return bless( [ Type::Parser::HEXNUM, $spelling ], "Type::Parser::Token" );
482             }
483             elsif ( looks_like_number( $spelling ) ) {
484 5         17 return bless( [ Type::Parser::STRING, $spelling ], "Type::Parser::Token" );
485             }
486             elsif ( $self->{remaining} =~ /^\s*=>/sm ) # peek ahead
487             {
488 4         15 return bless( [ Type::Parser::STRING, $spelling ], "Type::Parser::Token" );
489             }
490             elsif ( $spelling eq "slurpy" ) {
491 2         6 return $punctuation{$spelling};
492             }
493            
494 613         2561 return bless( [ Type::Parser::TYPE, $spelling ], "Type::Parser::Token" );
495             } #/ if ( $self->{remaining...})
496            
497 3         19 my $rest = $self->{remaining};
498 3         12 $self->{remaining} = "";
499 3         14 return bless( [ Type::Parser::MYSTERY, $rest ], "Type::Parser::Token" );
500             } #/ sub _read_token
501             }
502              
503             1;
504              
505             __END__
506              
507             =pod
508              
509             =encoding utf-8
510              
511             =for stopwords non-whitespace
512              
513             =head1 NAME
514              
515             Type::Parser - parse type constraint strings
516              
517             =head1 SYNOPSIS
518              
519             use v5.10;
520             use strict;
521             use warnings;
522            
523             use Type::Parser qw( eval_type );
524             use Type::Registry;
525            
526             my $reg = Type::Registry->for_me;
527             $reg->add_types("Types::Standard");
528            
529             my $type = eval_type("Int | ArrayRef[Int]", $reg);
530            
531             $type->check(10); # true
532             $type->check([1..4]); # true
533             $type->check({foo=>1}); # false
534              
535             =head1 STATUS
536              
537             This module is covered by the
538             L<Type-Tiny stability policy|Type::Tiny::Manual::Policies/"STABILITY">.
539              
540             =head1 DESCRIPTION
541              
542             Generally speaking, you probably don't want to be using this module directly.
543             Instead use the C<< lookup >> method from L<Type::Registry> which wraps it.
544              
545             =head2 Functions
546              
547             =over
548              
549             =item C<< parse($string) >>
550              
551             Parse the type constraint string into something like an AST.
552              
553             If called in list context, also returns any "tail" found on the original string.
554              
555             =item C<< extract_type($string, $registry) >>
556              
557             Compile a type constraint string into a L<Type::Tiny> object.
558              
559             If called in list context, also returns any "tail" found on the original string.
560              
561             =item C<< eval_type($string, $registry) >>
562              
563             Compile a type constraint string into a L<Type::Tiny> object.
564              
565             Throws an error if the "tail" contains any non-whitespace character.
566              
567             =back
568              
569             =head2 Constants
570              
571             The following constants correspond to values returned by C<< $token->type >>.
572              
573             =over
574              
575             =item C<< TYPE >>
576              
577             =item C<< QUOTELIKE >>
578              
579             =item C<< STRING >>
580              
581             =item C<< HEXNUM >>
582              
583             =item C<< CLASS >>
584              
585             =item C<< L_BRACKET >>
586              
587             =item C<< R_BRACKET >>
588              
589             =item C<< COMMA >>
590              
591             =item C<< SLURPY >>
592              
593             =item C<< UNION >>
594              
595             =item C<< INTERSECT >>
596              
597             =item C<< SLASH >>
598              
599             =item C<< NOT >>
600              
601             =item C<< L_PAREN >>
602              
603             =item C<< R_PAREN >>
604              
605             =item C<< MYSTERY >>
606              
607             =back
608              
609             =head1 BUGS
610              
611             Please report any bugs to
612             L<https://github.com/tobyink/p5-type-tiny/issues>.
613              
614             =head1 SEE ALSO
615              
616             L<Type::Registry>.
617              
618             =head1 AUTHOR
619              
620             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
621              
622             =head1 COPYRIGHT AND LICENCE
623              
624             This software is copyright (c) 2013-2014, 2017-2023 by Toby Inkster.
625              
626             This is free software; you can redistribute it and/or modify it under
627             the same terms as the Perl 5 programming language system itself.
628              
629             =head1 DISCLAIMER OF WARRANTIES
630              
631             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
632             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
633             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.