# SigParse.yp: Parse::Yapp input to parse signatures in Sub::Multi::Tiny
#############################################################################
# Header
%{
# Imports {{{1
use 5.006;
use strict;
use warnings;
use Text::Balanced qw(extract_codeblock);
# Types of constraints we've seen - bit offsets
use enum
# Flags set by the parser
'SEEN_WHERE', # `where` clause
'SEEN_TYPE', # Type constraint
'SEEN_POS', # Positional argument
'SEEN_NAMED', # Named argument
# future: SEEN_LITERAL for signatures holding literal values
# instead of name matches.
# Flags set by later processing
'HAS_MULTIPLE_ARITIES', # Set if there are at least two
# different positional arities in a
# set of impls
;
# Set bits in YYData->{SEEN}
sub _seen {
vec($_[0]->YYData->{SEEN}, $_[$_], 1) = 1 foreach 1..$#_;
}
# }}}1
# Documentation {{{1
=head1 NAME
Sub::Multi::Tiny::SigParse - Parse::Yapp input to parse signatures in Sub::Multi::Tiny
=head1 SYNOPSIS
Generate the .pm file:
yapp -m Sub::Multi::Tiny::SigParse -o lib/Sub/Multi/Tiny/SigParse.pm support/SigParse.yp
And then:
use Sub::Multi::Tiny::SigParse;
my $ast = Sub::Multi::Tiny::SigParse::Parse($signature);
=head1 FUNCTIONS
=cut
# }}}1
%}
#############################################################################
# Token and precedence definitions
# TODO: slurpies (prefix *, +); trailing ?, !
# Separator (usually a comma)
%token SEPAR
# Type before a variable name
%token TYPE
# Parameter, named or positional
%token PARAM
# "where BLOCK"
%token WHERE
%%
#############################################################################
# Rules
signature:
{ [] } # always return arrayref
| parameter { [ $_[1] ] }
| parameter SEPAR signature { [ $_[1], @{$_[3]} ] }
# Permit trailing comma
| parameter SEPAR signature SEPAR { [ $_[1], @{$_[3]} ] }
;
parameter:
PARAM
{
_seen $_[0], $_[1]->{named} ? SEEN_NAMED : SEEN_POS;
return $_[1];
}
| PARAM WHERE
{
_seen $_[0], $_[1]->{named} ? SEEN_NAMED : SEEN_POS;
_seen $_[0], SEEN_WHERE;
return +{%{$_[1]}, where=>$_[2]};
}
| TYPE PARAM
{
_seen $_[0], $_[2]->{named} ? SEEN_NAMED : SEEN_POS;
_seen $_[0], SEEN_TYPE;
return +{%{$_[2]}, type => $_[1]};
}
| TYPE PARAM WHERE
{
_seen $_[0], $_[2]->{named} ? SEEN_NAMED : SEEN_POS;
_seen $_[0], SEEN_TYPE, SEEN_WHERE;
return +{%{$_[2]}, where=>$_[3], type => $_[1]}
}
;
%%
#############################################################################
# Footer
# Tokenizer and error-reporting routine for Parse::Yapp {{{1
# The lexer
sub _next_token {
my $parser = shift;
my $text = $parser->YYData->{TEXT};
$$text =~ m/\G\s+/gc; # Skip H and V whitespace
$parser->YYData->{CURR_TOK_POS} = pos($$text);
return ('', undef) unless (pos($$text)||0) < length($$text); # EOF
$$text =~ m/\G,/gc and return (SEPAR => 0); # 0 is a dummy value
if($$text =~ m/\G([:]?)([\$\@\%\&\*]\w+)\b([?!]?)/gc) {
my $retval = {};
$retval->{name} = $2;
$retval->{named} = !!$1;
$retval->{reqd} = (
($retval->{named} && $3 eq '!') || # Named: optional unless !
(!$retval->{named} && $3 ne '?') # Positional: reqd unless ?
);
return (PARAM => $retval);
}
if($$text =~ m/\Gwhere\s*\{/gci) {
pos($$text) -= 1; # Get the lbrace back
my ($block) = extract_codeblock($$text); # Updates pos()
return (WHERE => $block) if defined $block;
die "Saw a 'where' without a valid block after it";
}
# Permit braced expressions for complex type checks
if($$text =~ m/\G\{/gc) {
pos($$text) -= 1; # Get the lbrace back
my ($block) = extract_codeblock($$text); # Updates pos()
return (TYPE => $block) if defined $block;
die "Saw an opening brace without a valid block after it";
}
# If the next thing is a backslash, die --- prohibit backslash to start
# a typecheck as a guard against '' vs "" confusion. If you want a
# backslash, use the {} form.
if($$text =~ m{\G\\}gc) {
die "Saw a backslash where I don't know what to do with it! ('' vs \"\" confusion?)";
}
# Otherwise, assume a single word is a typecheck
$$text =~ m/\G(\S+)/gc and return (TYPE => $1);
die "This should never happen! Unlexable text was: " .
substr($$text, pos($$text));
} #_next_token()
# Report an error
sub _report_error {
my $parser = shift;
my $startpos = $parser->YYData->{CURR_TOK_POS};
my $endpos = pos(${ $parser->YYData->{TEXT} });
my $got = $parser->YYCurtok || '<end of input>';
my $val='';
$val = ' (' . $parser->YYCurval . ')' if $parser->YYCurval;
my $errmsg = 'Syntax error: could not understand ' . $got . $val .
" at positions $startpos..$endpos";
if(ref($parser->YYExpect) eq 'ARRAY') {
$errmsg .= ".\nExpected one of: " . join(',', @{$parser->YYExpect});
} else {
$errmsg .= ':'
}
# Print the text and flag the error
my $copy = ${ $parser->YYData->{TEXT} };
$copy =~ s/\s/ /g; # Normalize spaces so pos values line up
$errmsg .= "\n$copy";
$errmsg .= "\n" . (' ' x $startpos) . ('^' x ($endpos-$startpos));
$errmsg .= "\n"; # No stack trace
die $errmsg;
} #_report_error()
# }}}1
# Top-level parse function {{{1
=head2 Parse
Parse arguments. Usage:
my $ast = Sub::Multi::Tiny::SigParse::Parse($signature);
=cut
sub Parse {
my $text = shift;
unless(defined $text) {
require Carp;
Carp::croak 'Parse: Need a signature to parse';
}
my $parser = __PACKAGE__->new;
my $hrData = $parser->YYData;
# Data we use while parsing.
# TEXT: The input text. Store it as a reference so pos() will
# be preserved across calls to _next_token.
$hrData->{TEXT} = \"$text";
# CURR_TOK_POS: the pos() value where the current token started.
# Used in reporting errors.
$hrData->{CURR_TOK_POS} = -1;
# SEEN: bit flags for which types of things we've seen
$hrData->{SEEN} = '';
my $lrParms = $parser->YYParse(yylex => \&_next_token,
yyerror => \&_report_error,
(@_ ? (yydebug => $_[0]) : ()),
);
my %retval = (seen => $hrData->{SEEN}, parms => $lrParms);
return \%retval;
} #Parse()
# }}}1
# Rest of the docs {{{1
=head1 AUTHOR
Chris White E<lt>cxw@cpan.orgE<gt>
=head1 LICENSE
Copyright (C) 2019 Chris White E<lt>cxw@cpan.orgE<gt>
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# }}}1
# vi: set fdm=marker: #