# Copyright 2011-2012 Yuki Izumi. ( anneli AT cpan DOT org )
# This is free software; you can redistribute it and/or modify it under the
# same terms as Perl itself.

%nonassoc KW_CATCH

# ???
%nonassoc LARROW LDARROW

%right MATCH SEND
%left KW_ORELSE
%left KW_ANDALSO
%nonassoc EQUAL NOT_EQUAL LTE GTE LT GT STRICTLY_EQUAL STRICTLY_NOT_EQUAL
%right LISTADD LISTSUBTRACT
%left ADD SUBTRACT KW_BOR KW_BXOR KW_BSL KW_BSR KW_OR KW_XOR
%left DIVIDE MULTIPLY KW_DIV KW_REM KW_AND KW_BAND
%left NEG POS KW_BNOT KW_NOT
%nonassoc OPENRECORD
%nonassoc COLON

%{
    use strict;
    use warnings;

    use Erlang::Parser::Node::Directive;
    use Erlang::Parser::Node::DefList;
    use Erlang::Parser::Node::Def;
    use Erlang::Parser::Node::WhenList;
    use Erlang::Parser::Node::Atom;
    use Erlang::Parser::Node::Integer;
    use Erlang::Parser::Node::BinOp;
    use Erlang::Parser::Node::List;
    use Erlang::Parser::Node::Variable;
    use Erlang::Parser::Node::Tuple;
    use Erlang::Parser::Node::Macro;
    use Erlang::Parser::Node::String;
    use Erlang::Parser::Node::Call;
    use Erlang::Parser::Node::Alt;
    use Erlang::Parser::Node::Try;
    use Erlang::Parser::Node::Literal;
    use Erlang::Parser::Node::FunRef;
    use Erlang::Parser::Node::FunLocal;
    use Erlang::Parser::Node::FunLocalCase;
    use Erlang::Parser::Node::Case;
    use Erlang::Parser::Node::RecordNew;
    use Erlang::Parser::Node::VariableRecordAccess;
    use Erlang::Parser::Node::VariableRecordUpdate;
    use Erlang::Parser::Node::Float;
    use Erlang::Parser::Node::BaseInteger;
    use Erlang::Parser::Node::BinaryExpr;
    use Erlang::Parser::Node::Binary;
    use Erlang::Parser::Node::UnOp;
    use Erlang::Parser::Node::Begin;
    use Erlang::Parser::Node::Comprehension;
    use Erlang::Parser::Node::If;
    use Erlang::Parser::Node::IfExpr;
    use Erlang::Parser::Node::Receive;
    use Erlang::Parser::Node::ReceiveAfter;

    sub new_node {
        my ($kind, %args) = @_;
        "Erlang::Parser::Node::$kind"->new(%args);
    }
%}

%%

# TODO: A few of these lists are flawed in that their optional type isn't done correctly
# (they allow constructs like [, 1, 2]). Fix this.

root:
                                { [] }
    | root rootstmt             { [@{$_[1]}, $_[2]] }
    ;

rootstmt:
      SUBTRACT ATOM LPAREN exprlist RPAREN PERIOD       { new_node 'Directive', directive => $_[2], args => $_[4] }
    | deflist PERIOD                                    { $_[1] }
    ;

deflist:
      def                       { new_node('DefList')->_append($_[1]) }
    | deflist SEMICOLON def     { $_[1]->_append($_[3]) }
    ;

def:
      ATOM LPAREN exprlist RPAREN whenlist RARROW stmtlist      { new_node 'Def', def => $_[1], args => $_[3], whens => $_[5]->_group, stmts => $_[7] }
    ;

whenlist:
                                { new_node 'WhenList' }
    | KW_WHEN expr              { new_node('WhenList')->_append($_[2]) }
    # TODO differentiate these. (a;b,c (A)||(B&&C))
    | whenlist COMMA expr       { $_[1]->_append($_[3]) }
    | whenlist SEMICOLON expr   { $_[1]->_group->_append($_[3]) }
    ;

# somehow this is an idiom. exprlist = 0 or more. stmtlist = 1 or more.
exprlist:
                                { [] }
    | stmtlist                  { $_[1] }
    ;

stmtlist:
      expr                      { [$_[1]] }
    | stmtlist COMMA expr       { [@{$_[1]}, $_[3]] }
    ;

unparenexpr:
      immexpr
    | case
    | fun
    | binary
    | receive
    | comprehension
    | try
    | if
    | KW_BEGIN exprlist KW_END          { new_node 'Begin', exprs => $_[2] }
    | expr SEND expr                    { new_node 'BinOp', op => '!',       a => $_[1], b => $_[3] }
    | expr LT expr                      { new_node 'BinOp', op => '<',       a => $_[1], b => $_[3] }
    | expr LTE expr                     { new_node 'BinOp', op => '=<',      a => $_[1], b => $_[3] }
    | expr GT expr                      { new_node 'BinOp', op => '>',       a => $_[1], b => $_[3] }
    | expr GTE expr                     { new_node 'BinOp', op => '>=',      a => $_[1], b => $_[3] }
    | expr DIVIDE expr                  { new_node 'BinOp', op => '/',       a => $_[1], b => $_[3] }
    | expr KW_DIV expr                  { new_node 'BinOp', op => 'div',     a => $_[1], b => $_[3] }
    | expr MULTIPLY expr                { new_node 'BinOp', op => '*',       a => $_[1], b => $_[3] }
    | expr ADD expr                     { new_node 'BinOp', op => '+',       a => $_[1], b => $_[3] }
    | expr SUBTRACT expr                { new_node 'BinOp', op => '-',       a => $_[1], b => $_[3] }
    | expr MATCH expr                   { new_node 'BinOp', op => '=',       a => $_[1], b => $_[3] }
    | expr LISTADD expr                 { new_node 'BinOp', op => '++',      a => $_[1], b => $_[3] }
    | expr LISTSUBTRACT expr            { new_node 'BinOp', op => '--',      a => $_[1], b => $_[3] }
    | expr EQUAL expr                   { new_node 'BinOp', op => '==',      a => $_[1], b => $_[3] }
    | expr STRICTLY_EQUAL expr          { new_node 'BinOp', op => '=:=',     a => $_[1], b => $_[3] }
    | expr STRICTLY_NOT_EQUAL expr      { new_node 'BinOp', op => '=/=',     a => $_[1], b => $_[3] }
    | expr NOT_EQUAL expr               { new_node 'BinOp', op => '/=',      a => $_[1], b => $_[3] }
    | expr KW_BSL expr                  { new_node 'BinOp', op => 'bsl',     a => $_[1], b => $_[3] }
    | expr KW_BSR expr                  { new_node 'BinOp', op => 'bsr',     a => $_[1], b => $_[3] }
    | expr KW_BOR expr                  { new_node 'BinOp', op => 'bor',     a => $_[1], b => $_[3] }
    | expr KW_BAND expr                 { new_node 'BinOp', op => 'band',    a => $_[1], b => $_[3] }
    | expr KW_BXOR expr                 { new_node 'BinOp', op => 'bxor',    a => $_[1], b => $_[3] }
    | expr KW_XOR expr                  { new_node 'BinOp', op => 'xor',     a => $_[1], b => $_[3] }
    | expr KW_REM expr                  { new_node 'BinOp', op => 'rem',     a => $_[1], b => $_[3] }
    | expr KW_ANDALSO expr              { new_node 'BinOp', op => 'andalso', a => $_[1], b => $_[3] }
    | expr KW_ORELSE expr               { new_node 'BinOp', op => 'orelse',  a => $_[1], b => $_[3] }
    | expr KW_AND expr                  { new_node 'BinOp', op => 'and',     a => $_[1], b => $_[3] }
    | expr KW_OR expr                   { new_node 'BinOp', op => 'or',      a => $_[1], b => $_[3] }
    | SUBTRACT expr %prec NEG           { new_node 'UnOp',  op => '-',       a => $_[2] }
    | ADD expr %prec POS                { new_node 'UnOp',  op => '+',       a => $_[2] }
    | KW_BNOT expr                      { new_node 'UnOp',  op => 'bnot',    a => $_[2] }
    | KW_NOT expr                       { new_node 'UnOp',  op => 'not',     a => $_[2] }
    | KW_CATCH expr                     { new_node 'UnOp',  op => 'catch',   a => $_[2] }

    # TODO: unhack this.
    | expr LARROW expr          { new_node 'BinOp', op => '<-', a => $_[1], b => $_[3] }
    | expr LDARROW expr         { new_node 'BinOp', op => '<=', a => $_[1], b => $_[3] }

    | call
    ;

parenexpr:
      LPAREN expr RPAREN                { $_[2] }
    ;

expr:
      unparenexpr
    | parenexpr
    ;

parenorimm:
      parenexpr
    | immexpr
    ;

immexpr:
      FLOAT                     { new_node 'Float', float => $_[1] }
    | BASE_INTEGER              { new_node 'BaseInteger', baseinteger => $_[1] }
    | INTEGER                   { new_node 'Integer', int => $_[1] }
    | string
    | variable OPENRECORD atom  { new_node 'VariableRecordAccess', variable => $_[1], record => $_[3] }
    | variable newrecord        { new_node 'VariableRecordUpdate', variable => $_[1], update => $_[2] }
    | LITERAL                   { new_node 'Literal', literal => substr($_[1], 1) }
    | list
    | tuple
    | newrecord
    | macro
    | variable
    | atom
    ;

atom:
      ATOM                      { new_node 'Atom', atom => $_[1] }
    ;

macro:
      MACRO                     { new_node 'Macro', macro => substr($_[1], 1) }
    ;

variable:
      VARIABLE                  { new_node 'Variable', variable => $_[1] }
    ;

string:
      STRING                    { new_node 'String', string => $_[1] }
    | string STRING             { $_[1]->_append($_[2]) }
    ;

call:
      intcall
    | extcall
    ;

intcall:
      parenorimm LPAREN exprlist RPAREN         { new_node 'Call', function => $_[1], args => $_[3] }
    ;

extcall:
      parenorimm COLON intcall                  { $_[3]->module($_[1]); $_[3] }
    ;

list:
      LISTOPEN exprlist listcdr LISTCLOSE       { new_node 'List', elems => $_[2], cdr => $_[3] }
    ;

# This is not a full node.
listcdr:
                                                { undef }
    | PIPE expr                                 { $_[2] }
    ;

comprehension:
      LISTOPEN expr COMPREHENSION exprlist LISTCLOSE            { new_node 'Comprehension', output => $_[2], generators => $_[4] }
    | OPENBINARY binary COMPREHENSION exprlist CLOSEBINARY      { new_node 'Comprehension', output => $_[2], generators => $_[4], binary => 1 }
    ;

tuple:
      TUPLEOPEN exprlist TUPLECLOSE     { new_node 'Tuple', elems => $_[2] }
    ;

case:
      KW_CASE expr KW_OF altlist KW_END { new_node 'Case', of => $_[2], alts => $_[4] }
    ;

altlist:
      alt                       { [$_[1]] }
    | altlist SEMICOLON alt     { [@{$_[1]}, $_[3]] }
    ;

alt:
      expr whenlist RARROW stmtlist     { new_node 'Alt', expr => $_[1], whens => $_[2]->_group, stmts => $_[4] }
    ;

fun:
      funlocal
    | KW_FUN atom COLON ATOM DIVIDE INTEGER     { new_node 'FunRef', module => $_[2], function => $_[4], arity => $_[6] }
    | KW_FUN macro COLON ATOM DIVIDE INTEGER    { new_node 'FunRef', module => $_[2], function => $_[4], arity => $_[6] }
    | KW_FUN variable COLON ATOM DIVIDE INTEGER { new_node 'FunRef', module => $_[2], function => $_[4], arity => $_[6] }
    | KW_FUN ATOM DIVIDE INTEGER                { new_node 'FunRef', function => $_[2], arity => $_[4] }
    ;

funlocal:
      KW_FUN funlocallist KW_END                { new_node 'FunLocal', cases => $_[2] }
    ;
    
# These are not full nodes.
funlocallist:
      funlocalcase                              { [$_[1]] }
    | funlocallist SEMICOLON funlocalcase       { [@{$_[1]}, $_[3]] }
    ;

funlocalcase:
      LPAREN exprlist RPAREN whenlist RARROW stmtlist           { new_node 'FunLocalCase', args => $_[2], whens => $_[4]->_group, stmts => $_[6] }
    ;

newrecord:
      OPENRECORD atom TUPLEOPEN exprlist TUPLECLOSE             { new_node 'RecordNew', record => $_[2], exprs => $_[4] }
    ;

binary:
      OPENBINARY optbinarylist CLOSEBINARY                      { new_node 'Binary', bexprs => $_[2] }
    ;

# These are not full nodes.
optbinarylist:
                                                                { [] }
    | binarylist
    ;

binarylist:
      binaryexpr                                                { [$_[1]] }
    | binarylist COMMA binaryexpr                               { [@{$_[1]}, $_[3]] }
    ;

binaryexpr:
      parenorimm optbinarysize optbinaryqualifier               { new_node 'BinaryExpr', output => $_[1], size => $_[2], qualifier => $_[3] }
    ;

# These are not full nodes.
optbinarysize:
                                                                { undef }
    | COLON immexpr                                             { $_[2] }
    ;

optbinaryqualifier:
                                                                { undef }
    | DIVIDE binaryqualifier                                    { $_[2] }
    ;

binaryqualifier:
      ATOM
    | binaryqualifier SUBTRACT ATOM                             { "$_[1]-$_[3]" }
    ;

receive:
      KW_RECEIVE altlist after KW_END                           { new_node 'Receive', alts => $_[2], aft => $_[3] }
    ;

# This is not a full node.
after:
                                                                { undef }
    | KW_AFTER expr RARROW stmtlist                             { new_node 'ReceiveAfter', time => $_[2], stmts => $_[4] }
    ;

try:
      KW_TRY exprlist opttryof opttrycatch opttryafter KW_END   { new_node 'Try', exprs => $_[2], of => $_[3], catch => $_[4], aft => $_[5] }
    ;

# These are not full nodes.
opttryof:
                                                                { undef }
    | KW_OF altlist                                             { $_[2] }
    ;

opttrycatch:
                                                                { undef }
    | KW_CATCH catchaltlist                                     { $_[2] }
    ;

opttryafter:
                                                                { undef }
    | KW_AFTER exprlist                                         { $_[2] }
    ;

catchaltlist:
      catchalt                          { [$_[1]] }
    | catchaltlist SEMICOLON catchalt   { [@{$_[1]}, $_[3]] }
    ;

catchalt:
      ATOM COLON expr whenlist RARROW stmtlist          { new_node 'Alt', catch => 1, class => $_[1], expr => $_[3], whens => $_[4]->_group, stmts => $_[6] }
    | VARIABLE COLON expr whenlist RARROW stmtlist      { new_node 'Alt', catch => 1, class => $_[1], expr => $_[3], whens => $_[4]->_group, stmts => $_[6] }
    | expr whenlist RARROW stmtlist                     { new_node 'Alt', catch => 1, expr => $_[1], whens => $_[2]->_group, stmts => $_[4] }
    ;

if:
      KW_IF iflist KW_END                       { new_node 'If', cases => $_[2] }
    ;

iflist:
      ifexpr                    { [$_[1]] }
    | iflist SEMICOLON ifexpr   { [@{$_[1]}, $_[3]] }
    ;

ifexpr:
      ifseq RARROW stmtlist     { new_node 'IfExpr', seq => $_[1], stmts => $_[3] }
    ;

ifseq:
      expr              { [$_[1]] }
    | ifseq COMMA expr  { [@{$_[1]}, $_[3]] }
    ;
%%

=over 4

=item C<new>

Creates a new parser object. See L<Parse::Yapp> for more information.

=item C<new_node>

Helper function used to create new nodes.

    # These are identical.
    my $n1 = new_node('X', @y);
    my $n2 = Erlang::Parser::Node::X->new(@y);

=cut

1;

# vim: set sw=4 ts=4 et filetype=perl: