package Type::Parser;
use strict;
use warnings;
sub _croak ($;@) { require Error::TypeTiny; goto \&Error::TypeTiny::croak }
our $AUTHORITY = 'cpan:TOBYINK';
our $VERSION = '1.004004';
# Token types
#
sub TYPE () { "TYPE" };
sub QUOTELIKE () { "QUOTELIKE" };
sub STRING () { "STRING" };
sub CLASS () { "CLASS" };
sub L_BRACKET () { "L_BRACKET" };
sub R_BRACKET () { "R_BRACKET" };
sub COMMA () { "COMMA" };
sub SLURPY () { "SLURPY" };
sub UNION () { "UNION" };
sub INTERSECT () { "INTERSECT" };
sub NOT () { "NOT" };
sub L_PAREN () { "L_PAREN" };
sub R_PAREN () { "R_PAREN" };
sub MYSTERY () { "MYSTERY" };
our @EXPORT_OK = qw( eval_type _std_eval parse extract_type );
require Exporter::Tiny;
our @ISA = 'Exporter::Tiny';
Evaluate: {
sub parse
{
my $str = $_[0];
my $parser = "Type::Parser::AstBuilder"->new(input => $str);
$parser->build;
wantarray ? ($parser->ast, $parser->remainder) : $parser->ast;
}
sub extract_type
{
my ($str, $reg) = @_;
my ($parsed, $tail) = parse($str);
wantarray ? (_eval_type($parsed, $reg), $tail) : _eval_type($parsed, $reg);
}
sub eval_type
{
my ($str, $reg) = @_;
my ($parsed, $tail) = parse($str);
_croak("Unexpected tail on type expression: $tail") if $tail =~ /\S/sm;
return _eval_type($parsed, $reg);
}
my $std;
sub _std_eval
{
require Type::Registry;
unless ($std)
{
$std = "Type::Registry"->new;
$std->add_types(-Standard);
}
eval_type($_[0], $std);
}
sub _eval_type
{
my ($node, $reg) = @_;
$node = _simplify_expression($node);
if ($node->{type} eq "list")
{
return map _eval_type($_, $reg), @{$node->{list}};
}
if ($node->{type} eq "union")
{
return $reg->make_union(
map _eval_type($_, $reg), @{$node->{union}}
);
}
if ($node->{type} eq "intersect")
{
return $reg->make_intersection(
map _eval_type($_, $reg), @{$node->{intersect}}
);
}
if ($node->{type} eq "slurpy")
{
return +{ slurpy => _eval_type($node->{of}, $reg) };
}
if ($node->{type} eq "complement")
{
return _eval_type($node->{of}, $reg)->complementary_type;
}
if ($node->{type} eq "parameterized")
{
my $base = _eval_type($node->{base}, $reg);
return $base unless $base->is_parameterizable || $node->{params};
return $base->parameterize($node->{params} ? _eval_type($node->{params}, $reg) : ());
}
if ($node->{type} eq "primary" and $node->{token}->type eq CLASS)
{
my $class = substr(
$node->{token}->spelling,
0,
length($node->{token}->spelling) - 2
);
return $reg->make_class_type($class);
}
if ($node->{type} eq "primary" and $node->{token}->type eq QUOTELIKE)
{
return eval($node->{token}->spelling); #ARGH
}
if ($node->{type} eq "primary" and $node->{token}->type eq STRING)
{
return $node->{token}->spelling;
}
if ($node->{type} eq "primary" and $node->{token}->type eq TYPE)
{
my $t = $node->{token}->spelling;
my $r = ($t =~ /^(.+)::(\w+)$/)
? $reg->foreign_lookup($t, 1)
: $reg->simple_lookup($t, 1);
$r or _croak("%s is not a known type constraint", $node->{token}->spelling);
return $r;
}
}
sub _simplify_expression
{
my $expr = shift;
if ($expr->{type} eq "expression" and $expr->{op}[0] eq COMMA)
{
return _simplify("list", COMMA, $expr);
}
if ($expr->{type} eq "expression" and $expr->{op}[0] eq UNION)
{
return _simplify("union", UNION, $expr);
}
if ($expr->{type} eq "expression" and $expr->{op}[0] eq INTERSECT)
{
return _simplify("intersect", INTERSECT, $expr);
}
return $expr;
}
sub _simplify
{
my $type = shift;
my $op = shift;
my @list;
for my $expr ($_[0]{lhs}, $_[0]{rhs})
{
if ($expr->{type} eq "expression" and $expr->{op}[0] eq $op)
{
my $simple = _simplify($type, $op, $expr);
push @list, @{ $simple->{$type} };
}
else
{
push @list, $expr;
}
}
return { type => $type, $type => \@list };
}
}
{
package # hide from CPAN
Type::Parser::AstBuilder;
sub new
{
my $class = shift;
bless { @_ }, $class;
}
our %precedence = (
# Type::Parser::COMMA() , 1 ,
Type::Parser::UNION() , 2 ,
Type::Parser::INTERSECT() , 3 ,
Type::Parser::NOT() , 4 ,
);
sub _parse_primary
{
my $self = shift;
my $tokens = $self->{tokens};
$tokens->assert_not_empty;
if ($tokens->peek(0)->type eq Type::Parser::NOT)
{
$tokens->eat(Type::Parser::NOT);
$tokens->assert_not_empty;
return {
type => "complement",
of => $self->_parse_primary,
};
}
if ($tokens->peek(0)->type eq Type::Parser::SLURPY)
{
$tokens->eat(Type::Parser::SLURPY);
$tokens->assert_not_empty;
return {
type => "slurpy",
of => $self->_parse_primary,
};
}
if ($tokens->peek(0)->type eq Type::Parser::L_PAREN)
{
$tokens->eat(Type::Parser::L_PAREN);
my $r = $self->_parse_expression;
$tokens->eat(Type::Parser::R_PAREN);
return $r;
}
if ($tokens->peek(1)
and $tokens->peek(0)->type eq Type::Parser::TYPE
and $tokens->peek(1)->type eq Type::Parser::L_BRACKET)
{
my $base = { type => "primary", token => $tokens->eat(Type::Parser::TYPE) };
$tokens->eat(Type::Parser::L_BRACKET);
$tokens->assert_not_empty;
local $precedence{ Type::Parser::COMMA() } = 1;
my $params = undef;
if ($tokens->peek(0)->type eq Type::Parser::R_BRACKET)
{
$tokens->eat(Type::Parser::R_BRACKET);
}
else
{
$params = $self->_parse_expression;
$params = { type => "list", list => [$params] } unless $params->{type} eq "list";
$tokens->eat(Type::Parser::R_BRACKET);
}
return {
type => "parameterized",
base => $base,
params => $params,
};
}
my $type = $tokens->peek(0)->type;
if ($type eq Type::Parser::TYPE
or $type eq Type::Parser::QUOTELIKE
or $type eq Type::Parser::STRING
or $type eq Type::Parser::CLASS)
{
return { type => "primary", token => $tokens->eat };
}
Type::Parser::_croak("Unexpected token in primary type expression; got '%s'", $tokens->peek(0)->spelling);
}
sub _parse_expression_1
{
my $self = shift;
my $tokens = $self->{tokens};
my ($lhs, $min_p) = @_;
while (!$tokens->empty and defined($precedence{$tokens->peek(0)->type}) and $precedence{$tokens->peek(0)->type} >= $min_p)
{
my $op = $tokens->eat;
my $rhs = $self->_parse_primary;
while (!$tokens->empty and defined($precedence{$tokens->peek(0)->type}) and $precedence{$tokens->peek(0)->type} > $precedence{$op->type})
{
my $lookahead = $tokens->peek(0);
$rhs = $self->_parse_expression_1($rhs, $precedence{$lookahead->type});
}
$lhs = {
type => "expression",
op => $op,
lhs => $lhs,
rhs => $rhs,
};
}
return $lhs;
}
sub _parse_expression
{
my $self = shift;
my $tokens = $self->{tokens};
return $self->_parse_expression_1($self->_parse_primary, 0);
}
sub build
{
my $self = shift;
$self->{tokens} = "Type::Parser::TokenStream"->new(remaining => $self->{input});
$self->{ast} = $self->_parse_expression;
}
sub ast
{
$_[0]{ast};
}
sub remainder
{
$_[0]{tokens}->remainder;
}
}
{
package # hide from CPAN
Type::Parser::Token;
sub type { $_[0][0] }
sub spelling { $_[0][1] }
}
{
package # hide from CPAN
Type::Parser::TokenStream;
use Scalar::Util qw(looks_like_number);
sub new
{
my $class = shift;
bless { stack => [], done => [], @_ }, $class;
}
sub peek
{
my $self = shift;
my $ahead = $_[0];
while ($self->_stack_size <= $ahead and length $self->{remaining})
{
$self->_stack_extend;
}
my @tokens = grep ref, @{ $self->{stack} };
return $tokens[$ahead];
}
sub empty
{
my $self = shift;
not $self->peek(0);
}
sub eat
{
my $self = shift;
$self->_stack_extend unless $self->_stack_size;
my $r;
while (defined(my $item = shift @{$self->{stack}}))
{
push @{ $self->{done} }, $item;
if (ref $item)
{
$r = $item;
last;
}
}
if (@_ and $_[0] ne $r->type)
{
unshift @{$self->{stack}}, pop @{$self->{done}};
Type::Parser::_croak("Expected $_[0]; got ".$r->type);
}
return $r;
}
sub assert_not_empty
{
my $self = shift;
Type::Parser::_croak("Expected token; got empty string") if $self->empty;
}
sub _stack_size
{
my $self = shift;
scalar grep ref, @{ $self->{stack} };
}
sub _stack_extend
{
my $self = shift;
push @{ $self->{stack} }, $self->_read_token;
my ($space) = ($self->{remaining} =~ m/^([\s\n\r]*)/sm);
return unless length $space;
push @{ $self->{stack} }, $space;
substr($self->{remaining}, 0, length $space) = "";
}
sub remainder
{
my $self = shift;
return join "",
map { ref($_) ? $_->spelling : $_ }
(@{$self->{stack}}, $self->{remaining})
}
my %punctuation = (
'[' => bless([ Type::Parser::L_BRACKET, "[" ], "Type::Parser::Token"),
']' => bless([ Type::Parser::R_BRACKET, "]" ], "Type::Parser::Token"),
'(' => bless([ Type::Parser::L_PAREN, "[" ], "Type::Parser::Token"),
')' => bless([ Type::Parser::R_PAREN, "]" ], "Type::Parser::Token"),
',' => bless([ Type::Parser::COMMA, "," ], "Type::Parser::Token"),
'=>' => bless([ Type::Parser::COMMA, "=>" ], "Type::Parser::Token"),
'slurpy' => bless([ Type::Parser::SLURPY, "slurpy" ], "Type::Parser::Token"),
'|' => bless([ Type::Parser::UNION, "|" ], "Type::Parser::Token"),
'&' => bless([ Type::Parser::INTERSECT, "&" ], "Type::Parser::Token"),
'~' => bless([ Type::Parser::NOT, "~" ], "Type::Parser::Token"),
);
sub _read_token
{
my $self = shift;
return if $self->{remaining} eq "";
# Punctuation
#
if ($self->{remaining} =~ /^( => | [()\]\[|&~,] )/xsm)
{
my $spelling = $1;
substr($self->{remaining}, 0, length $spelling) = "";
return $punctuation{$spelling};
}
if ($self->{remaining} =~ /\A\s*[q'"]/sm)
{
require Text::Balanced;
if (my $quotelike = Text::Balanced::extract_quotelike($self->{remaining}))
{
return bless([ Type::Parser::QUOTELIKE, $quotelike ], "Type::Parser::Token"),;
}
}
if ($self->{remaining} =~ /^([+-]?[\w:.+]+)/sm)
{
my $spelling = $1;
substr($self->{remaining}, 0, length $spelling) = "";
if ($spelling =~ /::$/sm)
{
return bless([ Type::Parser::CLASS, $spelling ], "Type::Parser::Token"),;
}
elsif (looks_like_number($spelling))
{
return bless([ Type::Parser::STRING, $spelling ], "Type::Parser::Token"),;
}
elsif ($self->{remaining} =~ /^\s*=>/sm) # peek ahead
{
return bless([ Type::Parser::STRING, $spelling ], "Type::Parser::Token"),;
}
elsif ($spelling eq "slurpy")
{
return $punctuation{$spelling};
}
return bless([ Type::Parser::TYPE, $spelling ], "Type::Parser::Token");
}
my $rest = $self->{remaining};
$self->{remaining} = "";
return bless([ Type::Parser::MYSTERY, $rest ], "Type::Parser::Token");
}
}
1;
__END__
=pod
=encoding utf-8
=for stopwords non-whitespace
=head1 NAME
Type::Parser - parse type constraint strings
=head1 SYNOPSIS
use v5.10;
use strict;
use warnings;
use Type::Parser qw( eval_type );
use Type::Registry;
my $reg = Type::Registry->for_me;
$reg->add_types("Types::Standard");
my $type = eval_type("Int | ArrayRef[Int]", $reg);
$type->check(10); # true
$type->check([1..4]); # true
$type->check({foo=>1}); # false
=head1 STATUS
This module is covered by the
L<Type-Tiny stability policy|Type::Tiny::Manual::Policies/"STABILITY">.
=head1 DESCRIPTION
Generally speaking, you probably don't want to be using this module directly.
Instead use the C<< lookup >> method from L<Type::Registry> which wraps it.
=head2 Functions
=over
=item C<< parse($string) >>
Parse the type constraint string into something like an AST.
If called in list context, also returns any "tail" found on the original string.
=item C<< extract_type($string, $registry) >>
Compile a type constraint string into a L<Type::Tiny> object.
If called in list context, also returns any "tail" found on the original string.
=item C<< eval_type($string, $registry) >>
Compile a type constraint string into a L<Type::Tiny> object.
Throws an error if the "tail" contains any non-whitespace character.
=back
=head2 Constants
The following constants correspond to values returned by C<< $token->type >>.
=over
=item C<< TYPE >>
=item C<< QUOTELIKE >>
=item C<< STRING >>
=item C<< CLASS >>
=item C<< L_BRACKET >>
=item C<< R_BRACKET >>
=item C<< COMMA >>
=item C<< SLURPY >>
=item C<< UNION >>
=item C<< INTERSECT >>
=item C<< NOT >>
=item C<< L_PAREN >>
=item C<< R_PAREN >>
=item C<< MYSTERY >>
=back
=head1 BUGS
Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=Type-Tiny>.
=head1 SEE ALSO
L<Type::Registry>.
=head1 AUTHOR
Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2013-2014, 2017-2019 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=head1 DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.