# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os, sys
from ply import lex, yacc

from ipdl.ast import *

def _getcallerpath():
    '''Return the absolute path of the file containing the code that
**CALLED** this function.'''
    return os.path.abspath(sys._getframe(1).f_code.co_filename)

##-----------------------------------------------------------------------------

class ParseError(Exception):
    def __init__(self, loc, fmt, *args):
        self.loc = loc
        self.error = ('%s%s: error: %s'% (
            Parser.includeStackString(), loc, fmt)) % args
    def __str__(self):
        return self.error

def _safeLinenoValue(t):
    lineno, value = 0, '???'
    if hasattr(t, 'lineno'): lineno = t.lineno
    if hasattr(t, 'value'):  value = t.value
    return lineno, value

def _error(loc, fmt, *args):
    raise ParseError(loc, fmt, *args)

class Parser:
    # when we reach an |include [protocol] foo;| statement, we need to
    # save the current parser state and create a new one.  this "stack" is
    # where that state is saved
    #
    # there is one Parser per file
    current = None
    parseStack = [ ]
    parsed = { }

    def __init__(self, type, name, debug=0):
        assert type and name
        self.type = type
        self.debug = debug
        self.filename = None
        self.includedirs = None
        self.loc = None         # not always up to date
        self.lexer = None
        self.parser = None
        self.tu = TranslationUnit(type, name)
        self.direction = None
        self.errout = None

    def parse(self, input, filename, includedirs, errout):
        assert os.path.isabs(filename)

        if filename in Parser.parsed:
            return Parser.parsed[filename].tu

        self.lexer = lex.lex(debug=self.debug,
                             optimize=not self.debug,
                             lextab="ipdl_lextab")
        self.parser = yacc.yacc(debug=self.debug,
                                optimize=not self.debug,
                                tabmodule="ipdl_yacctab")
        self.filename = filename
        self.includedirs = includedirs
        self.tu.filename = filename
        self.errout = errout

        Parser.parsed[filename] = self
        Parser.parseStack.append(Parser.current)
        Parser.current = self

        try:
            ast = self.parser.parse(input=input, lexer=self.lexer,
                                    debug=self.debug)
        except ParseError, p:
            print >>errout, p
            return None

        Parser.current = Parser.parseStack.pop()
        return ast

    def resolveIncludePath(self, filepath):
        '''Return the absolute path from which the possibly partial
|filepath| should be read, or |None| if |filepath| cannot be located.'''
        for incdir in self.includedirs +[ '' ]:
            realpath = os.path.join(incdir, filepath)
            if os.path.isfile(realpath):
                return os.path.abspath(realpath)
        return None

    # returns a GCC-style string representation of the include stack.
    # e.g.,
    #   in file included from 'foo.ipdl', line 120:
    #   in file included from 'bar.ipd', line 12:
    # which can be printed above a proper error message or warning
    @staticmethod
    def includeStackString():
        s = ''
        for parse in Parser.parseStack[1:]:
            s += "  in file included from `%s', line %d:\n"% (
                parse.loc.filename, parse.loc.lineno)
        return s

def locFromTok(p, num):
    return Loc(Parser.current.filename, p.lineno(num))


##-----------------------------------------------------------------------------

reserved = set((
        'answer',
        'as',
        'async',
        'both',
        'bridges',
        'call',
        'child',
        'class',
        'compress',
        'compressall',
        '__delete__',
        'delete',                       # reserve 'delete' to prevent its use
        'from',
        'goto',
        'high',
        'include',
        'intr',
        'manager',
        'manages',
        'namespace',
        'normal',
        'nullable',
        'opens',
        'or',
        'parent',
        'prio',
        'protocol',
        'recv',
        'returns',
        'send',
        'spawns',
        'start',
        'state',
        'struct',
        'sync',
        'union',
        'upto',
        'urgent',
        'using'))
tokens = [
    'COLONCOLON', 'ID', 'STRING',
] + [ r.upper() for r in reserved ]

t_COLONCOLON = '::'

literals = '(){}[]<>;:,~'
t_ignore = ' \f\t\v'

def t_linecomment(t):
    r'//[^\n]*'

def t_multilinecomment(t):
    r'/\*(\n|.)*?\*/'
    t.lexer.lineno += t.value.count('\n')

def t_NL(t):
    r'(?:\r\n|\n|\n)+'
    t.lexer.lineno += len(t.value)

def t_ID(t):
    r'[a-zA-Z_][a-zA-Z0-9_]*'
    if t.value in reserved:
        t.type = t.value.upper()
    return t

def t_STRING(t):
    r'"[^"\n]*"'
    t.value = t.value[1:-1]
    return t

def t_error(t):
    _error(Loc(Parser.current.filename, t.lineno),
           'lexically invalid characters `%s', t.value)

##-----------------------------------------------------------------------------

def p_TranslationUnit(p):
    """TranslationUnit : Preamble NamespacedStuff"""
    tu = Parser.current.tu
    tu.loc = Loc(tu.filename)
    for stmt in p[1]:
        if isinstance(stmt, CxxInclude):
            tu.addCxxInclude(stmt)
        elif isinstance(stmt, Include):
            tu.addInclude(stmt)
        elif isinstance(stmt, UsingStmt):
            tu.addUsingStmt(stmt)
        else:
            assert 0

    for thing in p[2]:
        if isinstance(thing, StructDecl):
            tu.addStructDecl(thing)
        elif isinstance(thing, UnionDecl):
            tu.addUnionDecl(thing)
        elif isinstance(thing, Protocol):
            if tu.protocol is not None:
                _error(thing.loc, "only one protocol definition per file")
            tu.protocol = thing
        else:
            assert(0)

    # The "canonical" namespace of the tu, what it's considered to be
    # in for the purposes of C++: |#include "foo/bar/TU.h"|
    if tu.protocol:
        assert tu.filetype == 'protocol'
        tu.namespaces = tu.protocol.namespaces
        tu.name = tu.protocol.name
    else:
        assert tu.filetype == 'header'
        # There's not really a canonical "thing" in headers.  So
        # somewhat arbitrarily use the namespace of the last
        # interesting thing that was declared.
        for thing in reversed(tu.structsAndUnions):
            tu.namespaces = thing.namespaces
            break

    p[0] = tu

##--------------------
## Preamble
def p_Preamble(p):
    """Preamble : Preamble PreambleStmt ';'
                |"""
    if 1 == len(p):
        p[0] = [ ]
    else:
        p[1].append(p[2])
        p[0] = p[1]

def p_PreambleStmt(p):
    """PreambleStmt : CxxIncludeStmt
                    | IncludeStmt
                    | UsingStmt"""
    p[0] = p[1]

def p_CxxIncludeStmt(p):
    """CxxIncludeStmt : INCLUDE STRING"""
    p[0] = CxxInclude(locFromTok(p, 1), p[2])

def p_IncludeStmt(p):
    """IncludeStmt : INCLUDE PROTOCOL ID
                   | INCLUDE ID"""
    loc = locFromTok(p, 1)
 
    Parser.current.loc = loc
    if 4 == len(p):
        id = p[3]
        type = 'protocol'
    else:
        id = p[2]
        type = 'header'
    inc = Include(loc, type, id)

    path = Parser.current.resolveIncludePath(inc.file)
    if path is None:
        raise ParseError(loc, "can't locate include file `%s'"% (
                inc.file))

    inc.tu = Parser(type, id).parse(open(path).read(), path, Parser.current.includedirs, Parser.current.errout)
    p[0] = inc

def p_UsingStmt(p):
    """UsingStmt : USING CxxType FROM STRING
                 | USING CLASS CxxType FROM STRING
                 | USING STRUCT CxxType FROM STRING"""
    if 6 == len(p):
        header = p[5]
    elif 5 == len(p):
        header = p[4]
    else:
        header = None
    if 6 == len(p):
        kind = p[2]
    else:
        kind = None
    if 6 == len(p):
        cxxtype = p[3]
    else:
        cxxtype = p[2]
    p[0] = UsingStmt(locFromTok(p, 1), cxxtype, header, kind)

##--------------------
## Namespaced stuff
def p_NamespacedStuff(p):
    """NamespacedStuff : NamespacedStuff NamespaceThing
                       | NamespaceThing"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[1].extend(p[2])
        p[0] = p[1]

def p_NamespaceThing(p):
    """NamespaceThing : NAMESPACE ID '{' NamespacedStuff '}'
                      | StructDecl
                      | UnionDecl
                      | ProtocolDefn"""
    if 2 == len(p):
        p[0] = [ p[1] ]
    else:
        for thing in p[4]:
            thing.addOuterNamespace(Namespace(locFromTok(p, 1), p[2]))
        p[0] = p[4]

def p_StructDecl(p):
    """StructDecl : STRUCT ID '{' StructFields '}' ';'
                  | STRUCT ID '{' '}' ';'"""
    if 7 == len(p):
        p[0] = StructDecl(locFromTok(p, 1), p[2], p[4])
    else:
        p[0] = StructDecl(locFromTok(p, 1), p[2], [ ])

def p_StructFields(p):
    """StructFields : StructFields StructField ';'
                    | StructField ';'"""
    if 3 == len(p):
        p[0] = [ p[1] ]
    else:
        p[1].append(p[2])
        p[0] = p[1]

def p_StructField(p):
    """StructField : Type ID"""
    p[0] = StructField(locFromTok(p, 1), p[1], p[2])

def p_UnionDecl(p):
    """UnionDecl : UNION ID '{' ComponentTypes  '}' ';'"""
    p[0] = UnionDecl(locFromTok(p, 1), p[2], p[4])

def p_ComponentTypes(p):
    """ComponentTypes : ComponentTypes Type ';'
                      | Type ';'"""
    if 3 == len(p):
        p[0] = [ p[1] ]
    else:
        p[1].append(p[2])
        p[0] = p[1]

def p_ProtocolDefn(p):
    """ProtocolDefn : OptionalProtocolSendSemanticsQual PROTOCOL ID '{' ProtocolBody '}' ';'"""
    protocol = p[5]
    protocol.loc = locFromTok(p, 2)
    protocol.name = p[3]
    protocol.priorityRange = p[1][0]
    protocol.sendSemantics = p[1][1]
    p[0] = protocol

    if Parser.current.type == 'header':
        _error(protocol.loc, 'can\'t define a protocol in a header.  Do it in a protocol spec instead.')


def p_ProtocolBody(p):
    """ProtocolBody : SpawnsStmtsOpt"""
    p[0] = p[1]

##--------------------
## spawns/bridges/opens stmts

def p_SpawnsStmtsOpt(p):
    """SpawnsStmtsOpt : SpawnsStmt SpawnsStmtsOpt
                      | BridgesStmtsOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].spawnsStmts.insert(0, p[1])
        p[0] = p[2]

def p_SpawnsStmt(p):
    """SpawnsStmt : PARENT SPAWNS ID AsOpt ';'
                  | CHILD SPAWNS ID AsOpt ';'"""
    p[0] = SpawnsStmt(locFromTok(p, 1), p[1], p[3], p[4])

def p_AsOpt(p):
    """AsOpt : AS PARENT
             | AS CHILD
             | """
    if 3 == len(p):
        p[0] = p[2]
    else:
        p[0] = 'child'

def p_BridgesStmtsOpt(p):
    """BridgesStmtsOpt : BridgesStmt BridgesStmtsOpt
                       | OpensStmtsOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].bridgesStmts.insert(0, p[1])
        p[0] = p[2]

def p_BridgesStmt(p):
    """BridgesStmt : BRIDGES ID ',' ID ';'"""
    p[0] = BridgesStmt(locFromTok(p, 1), p[2], p[4])

def p_OpensStmtsOpt(p):
    """OpensStmtsOpt : OpensStmt OpensStmtsOpt
                     | ManagersStmtOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].opensStmts.insert(0, p[1])
        p[0] = p[2]

def p_OpensStmt(p):
    """OpensStmt : PARENT OPENS ID ';'
                 | CHILD OPENS ID ';'"""
    p[0] = OpensStmt(locFromTok(p, 1), p[1], p[3])

##--------------------
## manager/manages stmts

def p_ManagersStmtOpt(p):
    """ManagersStmtOpt : ManagersStmt ManagesStmtsOpt
                       | ManagesStmtsOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].managers = p[1]
        p[0] = p[2]

def p_ManagersStmt(p):
    """ManagersStmt : MANAGER ManagerList ';'"""
    if 1 == len(p):
        p[0] = [ ]
    else:
        p[0] = p[2]

def p_ManagerList(p):
    """ManagerList : ID
                   | ManagerList OR ID"""
    if 2 == len(p):
        p[0] = [ Manager(locFromTok(p, 1), p[1]) ]
    else:
        p[1].append(Manager(locFromTok(p, 3), p[3]))
        p[0] = p[1]

def p_ManagesStmtsOpt(p):
    """ManagesStmtsOpt : ManagesStmt ManagesStmtsOpt
                       | MessageDeclsOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].managesStmts.insert(0, p[1])
        p[0] = p[2]

def p_ManagesStmt(p):
    """ManagesStmt : MANAGES ID ';'"""
    p[0] = ManagesStmt(locFromTok(p, 1), p[2])


##--------------------
## Message decls

def p_MessageDeclsOpt(p):
    """MessageDeclsOpt : MessageDeclThing MessageDeclsOpt
                       | TransitionStmtsOpt"""
    if 2 == len(p):
        p[0] = p[1]
    else:
        p[2].messageDecls.insert(0, p[1])
        p[0] = p[2]

def p_MessageDeclThing(p):
    """MessageDeclThing : MessageDirectionLabel ':' MessageDecl ';'
                        | MessageDecl ';'"""
    if 3 == len(p):
        p[0] = p[1]
    else:
        p[0] = p[3]

def p_MessageDirectionLabel(p):
    """MessageDirectionLabel : PARENT
                             | CHILD
                             | BOTH"""
    if p[1] == 'parent':
        Parser.current.direction = IN
    elif p[1] == 'child':
        Parser.current.direction = OUT
    elif p[1] == 'both':
        Parser.current.direction = INOUT
    else:
        assert 0

def p_MessageDecl(p):
    """MessageDecl : OptionalSendSemanticsQual MessageBody"""
    msg = p[2]
    msg.priority = p[1][0]
    msg.sendSemantics = p[1][1]

    if Parser.current.direction is None:
        _error(msg.loc, 'missing message direction')
    msg.direction = Parser.current.direction

    p[0] = msg

def p_MessageBody(p):
    """MessageBody : MessageId MessageInParams MessageOutParams OptionalMessageCompress"""
    # FIXME/cjones: need better loc info: use one of the quals
    loc, name = p[1]
    msg = MessageDecl(loc)
    msg.name = name
    msg.addInParams(p[2])
    msg.addOutParams(p[3])
    msg.compress = p[4]

    p[0] = msg

def p_MessageId(p):
    """MessageId : ID
                 | __DELETE__
                 | DELETE
                 | '~' ID"""
    loc = locFromTok(p, 1)
    if 3 == len(p):
        _error(loc, "sorry, `%s()' destructor syntax is a relic from a bygone era.  Declare `__delete__()' in the `%s' protocol instead", p[1]+p[2], p[2])
    elif 'delete' == p[1]:
        _error(loc, "`delete' is a reserved identifier")
    p[0] = [ loc, p[1] ]

def p_MessageInParams(p):
    """MessageInParams : '(' ParamList ')'"""
    p[0] = p[2]

def p_MessageOutParams(p):
    """MessageOutParams : RETURNS '(' ParamList ')'
                        | """
    if 1 == len(p):
        p[0] = [ ]
    else:
        p[0] = p[3]

def p_OptionalMessageCompress(p):
    """OptionalMessageCompress : MessageCompress
                               | """
    if 1 == len(p):
        p[0] = ''
    else:
        p[0] = p[1]

def p_MessageCompress(p):
    """MessageCompress : COMPRESS
                       | COMPRESSALL"""
    p[0] = p[1]

##--------------------
## State machine

def p_TransitionStmtsOpt(p):
    """TransitionStmtsOpt : TransitionStmt TransitionStmtsOpt
                          |"""
    if 1 == len(p):
        # we fill in |loc| in the Protocol rule
        p[0] = Protocol(None)
    else:
        p[2].transitionStmts.insert(0, p[1])
        p[0] = p[2]

def p_TransitionStmt(p):
    """TransitionStmt : OptionalStart STATE State ':' Transitions"""
    p[3].start = p[1]
    p[0] = TransitionStmt(locFromTok(p, 2), p[3], p[5])

def p_OptionalStart(p):
    """OptionalStart : START
                     | """
    p[0] = (len(p) == 2)                # True iff 'start' specified

def p_Transitions(p):
    """Transitions : Transitions Transition
                   | Transition"""
    if 3 == len(p):
        p[1].append(p[2])
        p[0] = p[1]
    else:
        p[0] = [ p[1] ]

def p_Transition(p):
    """Transition : Trigger ID GOTO StateList ';'
                  | Trigger __DELETE__ ';'
                  | Trigger DELETE ';'"""
    if 'delete' == p[2]:
        _error(locFromTok(p, 1), "`delete' is a reserved identifier")
    
    loc, trigger = p[1]
    if 6 == len(p):
        nextstates = p[4]
    else:
        nextstates = [ State.DEAD ]
    p[0] = Transition(loc, trigger, p[2], nextstates)

def p_Trigger(p):
    """Trigger : SEND
               | RECV
               | CALL
               | ANSWER"""
    p[0] = [ locFromTok(p, 1), Transition.nameToTrigger(p[1]) ]

def p_StateList(p):
    """StateList : StateList OR State
                 | State"""
    if 2 == len(p):
        p[0] = [ p[1] ]
    else:
        p[1].append(p[3])
        p[0] = p[1]

def p_State(p):
    """State : ID"""
    p[0] = State(locFromTok(p, 1), p[1])

##--------------------
## Minor stuff
def p_Priority(p):
    """Priority : NORMAL
                | HIGH
                | URGENT"""
    prios = {'normal': 1,
             'high': 2,
             'urgent': 3}
    p[0] = prios[p[1]]

def p_OptionalSendSemanticsQual(p):
    """OptionalSendSemanticsQual : SendSemanticsQual
                                 | """
    if 2 == len(p): p[0] = p[1]
    else:           p[0] = [ NORMAL_PRIORITY, ASYNC ]

def p_SendSemanticsQual(p):
    """SendSemanticsQual : ASYNC
                         | SYNC
                         | PRIO '(' Priority ')' ASYNC
                         | PRIO '(' Priority ')' SYNC
                         | INTR"""
    if p[1] == 'prio':
        mtype = p[5]
        prio = p[3]
    else:
        mtype = p[1]
        prio = NORMAL_PRIORITY

    if mtype == 'async': mtype = ASYNC
    elif mtype == 'sync': mtype = SYNC
    elif mtype == 'intr': mtype = INTR
    else: assert 0

    p[0] = [ prio, mtype ]

def p_OptionalProtocolSendSemanticsQual(p):
    """OptionalProtocolSendSemanticsQual : ProtocolSendSemanticsQual
                                         | """
    if 2 == len(p): p[0] = p[1]
    else:           p[0] = [ (NORMAL_PRIORITY, NORMAL_PRIORITY), ASYNC ]

def p_ProtocolSendSemanticsQual(p):
    """ProtocolSendSemanticsQual : ASYNC
                                 | SYNC
                                 | PRIO '(' Priority UPTO Priority ')' ASYNC
                                 | PRIO '(' Priority UPTO Priority ')' SYNC
                                 | PRIO '(' Priority UPTO Priority ')' INTR
                                 | INTR"""
    if p[1] == 'prio':
        mtype = p[7]
        prio = (p[3], p[5])
    else:
        mtype = p[1]
        prio = (NORMAL_PRIORITY, NORMAL_PRIORITY)

    if mtype == 'async': mtype = ASYNC
    elif mtype == 'sync': mtype = SYNC
    elif mtype == 'intr': mtype = INTR
    else: assert 0

    p[0] = [ prio, mtype ]

def p_ParamList(p):
    """ParamList : ParamList ',' Param
                 | Param
                 | """
    if 1 == len(p):
        p[0] = [ ]
    elif 2 == len(p):
        p[0] = [ p[1] ]
    else:
        p[1].append(p[3])
        p[0] = p[1]

def p_Param(p):
    """Param : Type ID"""
    p[0] = Param(locFromTok(p, 1), p[1], p[2])

def p_Type(p):
    """Type : MaybeNullable BasicType"""
    # only actor types are nullable; we check this in the type checker
    p[2].nullable = p[1]
    p[0] = p[2]

def p_BasicType(p):
    """BasicType : ScalarType
                 | ScalarType '[' ']'"""
    if 4 == len(p):
        p[1].array = 1
    p[0] = p[1]

def p_ScalarType(p):
    """ScalarType : ActorType
                  | CxxID"""    # ID == CxxType; we forbid qnames here,
                                # in favor of the |using| declaration
    if isinstance(p[1], TypeSpec):
        p[0] = p[1]
    else:
        loc, id = p[1]
        p[0] = TypeSpec(loc, QualifiedId(loc, id))

def p_ActorType(p):
    """ActorType : ID ':' State"""
    loc = locFromTok(p, 1)
    p[0] = TypeSpec(loc, QualifiedId(loc, p[1]), state=p[3])
 
def p_MaybeNullable(p):
    """MaybeNullable : NULLABLE
                     | """
    p[0] = (2 == len(p))

##--------------------
## C++ stuff
def p_CxxType(p):
    """CxxType : QualifiedID
               | CxxID"""
    if isinstance(p[1], QualifiedId):
        p[0] = TypeSpec(p[1].loc, p[1])
    else:
        loc, id = p[1]
        p[0] = TypeSpec(loc, QualifiedId(loc, id))

def p_QualifiedID(p):
    """QualifiedID : QualifiedID COLONCOLON CxxID
                   | CxxID COLONCOLON CxxID"""
    if isinstance(p[1], QualifiedId):
        loc, id = p[3]
        p[1].qualify(id)
        p[0] = p[1]
    else:
        loc1, id1 = p[1]
        _, id2 = p[3]
        p[0] = QualifiedId(loc1, id2, [ id1 ])

def p_CxxID(p):
    """CxxID : ID
             | CxxTemplateInst"""
    if isinstance(p[1], tuple):
        p[0] = p[1]
    else:
        p[0] = (locFromTok(p, 1), str(p[1]))

def p_CxxTemplateInst(p):
    """CxxTemplateInst : ID '<' ID '>'"""
    p[0] = (locFromTok(p, 1), str(p[1]) +'<'+ str(p[3]) +'>')

def p_error(t):
    lineno, value = _safeLinenoValue(t)
    _error(Loc(Parser.current.filename, lineno),
           "bad syntax near `%s'", value)
