"""
Parser for the Alloy Programming Language

This module provides the parser for the Alloy language.
It uses PLY (Python Lex-Yacc) to define the grammar and generate ASTs.
"""

import ply.yacc as yacc
from typing import Any, List, Optional, Union
from .lexer import AlloyLexer


class AlloyParser:
    """
    Parser for the Alloy programming language.
    
    Defines the grammar rules and builds abstract syntax trees (ASTs)
    from tokenized Alloy source code.
    """
    
    # Precedence rules to resolve shift/reduce conflicts
    precedence = (
        ('left', 'PLUS', 'MINUS'),    # Addition and subtraction
        ('left', 'MULTIPLY'),         # Multiplication has higher precedence than addition
        ('left', 'GT', 'GE'),         # Comparison operators - higher than arithmetic
        ('right', 'UMINUS'),          # Unary minus has high precedence
        ('left', 'LPAREN'),           # Shift LPAREN tokens rather than reducing IDENTIFIER
        ('left', 'TOOL', 'AGENT', 'COMMAND', 'MEANS'),  # Force shift of keyword tokens
        ('right', 'AGENT_COMMAND'),   # Agent commands - but lower than assignments
        ('right', 'STRING'),          # Force shift of STRING tokens in agent commands
        ('left', 'EQ'),               # Assignment precedence - higher than agent commands
        ('right', 'MUST', 'SHOULD'),  # Prefer longer agent statement rules
        ('right', 'ALLOY'),           # Force shift when seeing ALLOY
        ('nonassoc', 'THEN'),         # Non-associative for then to avoid conflicts
        ('nonassoc', 'OTHERWISE'),    # Non-associative for otherwise
        ('right', 'LBRACE'),          # Highest precedence for agent blocks and tool bodies
    )
    
    def __init__(self, lexer: Optional[AlloyLexer] = None):
        """
        Initialize the parser.
        
        Args:
            lexer: Lexer instance to use (creates new one if None)
        """
        self.lexer = lexer or AlloyLexer()
        self.tokens = self.lexer.tokens  # Required by PLY
        self.parser = None
        self.build()

    # Grammar rules start here
    def p_program(self, p):
        '''program : statement_list'''
        p[0] = ('program', p[1])

    def p_statement_list(self, p):
        '''statement_list : statement statement_list
                          | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        else:
            p[0] = []

    def p_statement(self, p):
        '''statement : assignment
                     | tuple_assignment
                     | agent_definition
                     | agent_block
                     | command_definition
                     | tool_def
                     | control_flow
                     | expression'''
        p[0] = p[1]

    def p_assignment(self, p):
        '''assignment : IDENTIFIER EQ agent_block
                      | IDENTIFIER EQ expression %prec EQ'''
        if len(p) == 4:
            # Standard assignment: var = expression or agent_block
            p[0] = ('assign', p[1], p[3])
    

    def p_tuple_assignment(self, p):
        '''tuple_assignment : multi_identifier_list EQ agent_block
                             | multi_identifier_list EQ expression %prec EQ'''
        # Tuple assignment - agent commands handled as expressions
        p[0] = ('tuple_assign', p[1], p[3])

    def p_multi_identifier_list(self, p):
        '''multi_identifier_list : IDENTIFIER COMMA identifier_list'''
        p[0] = [p[1]] + p[3]

    def p_identifier_list(self, p):
        '''identifier_list : IDENTIFIER COMMA identifier_list
                           | IDENTIFIER'''
        if len(p) == 4:
            p[0] = [p[1]] + p[3]
        else:
            p[0] = [p[1]]

    def p_agent_definition(self, p):
        '''agent_definition : IDENTIFIER EQ AGENT STRING
                            | IDENTIFIER EQ AGENT STRING WITH TOOLS tool_list
                            | IDENTIFIER EQ AGENT STRING WITH system_prompt_block'''
        if len(p) == 5:
            # Simple agent definition: claude = agent "gpt-4o-mini"
            p[0] = ('agent_def', p[1], p[4], [], [])
        elif len(p) == 8 and p[6] == 'tools':
            # Agent with tools: researcher = agent "gpt-4o-mini" with tools web_search, read_files
            p[0] = ('agent_def', p[1], p[4], p[7], [])
        else:
            # Agent with system prompt: assistant = agent "gpt-4o-mini" with { must/should }
            p[0] = ('agent_def', p[1], p[4], [], p[6])

    def p_command_definition(self, p):
        '''command_definition : COMMAND IDENTIFIER MEANS STRING'''
        p[0] = ('command_def', p[2], p[4])

    def p_tool_list(self, p):
        '''tool_list : IDENTIFIER COMMA tool_list
                     | IDENTIFIER'''
        if len(p) == 4:
            p[0] = [p[1]] + p[3]
        else:
            p[0] = [p[1]]

    def p_system_prompt_block(self, p):
        '''system_prompt_block : LBRACE system_prompt_list RBRACE'''
        p[0] = p[2]

    def p_system_prompt_list(self, p):
        '''system_prompt_list : system_prompt_statement system_prompt_list
                              | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        else:
            p[0] = []

    def p_system_prompt_statement(self, p):
        '''system_prompt_statement : MUST STRING
                                   | SHOULD STRING'''
        p[0] = (p[1], p[2])

    def p_tool_def(self, p):
        '''tool_def : TOOL IDENTIFIER TAKES param_list RETURNS IDENTIFIER tool_body'''
        p[0] = ('tool', p[2], p[4], p[6], p[7])
    
    def p_tool_body(self, p):
        '''tool_body : LBRACE tool_statement_list RBRACE %prec LBRACE'''
        p[0] = ('block', p[2])
    
    def p_tool_statement_list(self, p):
        '''tool_statement_list : tool_statement tool_statement_list
                               | tool_statement
                               | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        elif len(p) == 2:
            p[0] = [p[1]]
        else:
            p[0] = []
    
    def p_tool_statement(self, p):
        '''tool_statement : tool_assignment
                          | tool_tuple_assignment
                          | tool_control_flow'''
        p[0] = p[1]
    
    def p_tool_tuple_assignment(self, p):
        '''tool_tuple_assignment : multi_identifier_list EQ tool_identifier_or_call
                                  | multi_identifier_list EQ tool_expression'''
        # For tool contexts, we only allow function calls and expressions, no agent commands
        p[0] = ('tuple_assign', p[1], p[3])
    
    def p_tool_assignment(self, p):
        '''tool_assignment : IDENTIFIER EQ tool_expression'''
        p[0] = ('assign', p[1], p[3])
    
    def p_tool_expression(self, p):
        '''tool_expression : tool_when_expr
                           | tool_then_expr'''
        p[0] = p[1]
    
    def p_tool_when_expr(self, p):
        '''tool_when_expr : WHEN tool_or_expr THEN tool_when_then OTHERWISE tool_when_else'''
        p[0] = ('when', p[2], p[4], p[6])
        
    def p_tool_when_then(self, p):
        '''tool_when_then : tool_or_expr
                          | tool_expression_block'''
        p[0] = p[1]
        
    def p_tool_when_else(self, p):
        '''tool_when_else : tool_or_expr
                          | tool_expression_block'''
        p[0] = p[1]
    
    def p_tool_expression_block(self, p):
        '''tool_expression_block : LBRACE tool_expression_statement_list RBRACE'''
        p[0] = ('block', p[2])
    
    def p_tool_expression_statement_list(self, p):
        '''tool_expression_statement_list : tool_expression_statement tool_expression_statement_list
                                           | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        else:
            p[0] = []
    
    def p_tool_expression_statement(self, p):
        '''tool_expression_statement : tool_assignment
                                      | tuple_assignment'''
        p[0] = p[1]
    
    def p_tool_then_expr(self, p):
        '''tool_then_expr : tool_or_expr THEN tool_then_expr
                          | tool_or_expr'''
        if len(p) == 4:
            p[0] = ('chain', p[1], p[3])
        else:
            p[0] = p[1]
    
    def p_tool_or_expr(self, p):
        '''tool_or_expr : tool_and_expr'''
        p[0] = p[1]
        
    def p_tool_and_expr(self, p):
        '''tool_and_expr : tool_compare_expr'''
        p[0] = p[1]
    
    def p_tool_compare_expr(self, p):
        '''tool_compare_expr : tool_add_expr GT tool_add_expr
                             | tool_add_expr GE tool_add_expr
                             | tool_add_expr LT tool_add_expr
                             | tool_add_expr LE tool_add_expr
                             | tool_add_expr EQEQ tool_add_expr
                             | tool_add_expr NE tool_add_expr
                             | tool_add_expr'''
        if len(p) == 4:
            p[0] = ('compare', p[2], p[1], p[3])
        else:
            p[0] = p[1]
    
    def p_tool_add_expr(self, p):
        '''tool_add_expr : tool_mul_expr PLUS tool_add_expr
                         | tool_mul_expr MINUS tool_add_expr
                         | tool_mul_expr'''
        if len(p) == 4:
            if p[2] == '+':
                p[0] = ('binary', 'add', p[1], p[3])
            else:  # p[2] == '-'
                p[0] = ('binary', 'sub', p[1], p[3])
        else:
            p[0] = p[1]
    
    def p_tool_mul_expr(self, p):
        '''tool_mul_expr : tool_unary_expr MULTIPLY tool_mul_expr
                         | tool_unary_expr'''
        if len(p) == 4:
            p[0] = ('binary', 'mul', p[1], p[3])
        else:
            p[0] = p[1]
    
    def p_tool_unary_expr(self, p):
        '''tool_unary_expr : MINUS tool_unary_expr %prec UMINUS
                           | tool_atom_expr'''
        if len(p) == 3:
            p[0] = ('unary', 'neg', p[2])
        else:
            p[0] = p[1]
    
    def p_tool_atom_expr(self, p):
        '''tool_atom_expr : agent_block
                          | tool_primary_expr'''
        p[0] = p[1]
    
    def p_tool_primary_expr(self, p):
        '''tool_primary_expr : literal
                             | tool_identifier_or_call
                             | tool_identifier_expr'''
        p[0] = p[1]
    
    def p_tool_identifier_or_call(self, p):
        '''tool_identifier_or_call : IDENTIFIER LPAREN expression_list RPAREN'''
        # This is a tool call in tool context
        p[0] = ('tool_call', p[1], p[3])
        
    def p_tool_identifier_expr(self, p):
        '''tool_identifier_expr : IDENTIFIER'''
        # This is just an identifier in tool context
        p[0] = p[1]
    
    def p_tool_control_flow_when(self, p):
        '''tool_control_flow : WHEN tool_or_expr THEN tool_statement_or_block OTHERWISE tool_statement_or_block'''
        p[0] = ('when', p[2], p[4], p[6])

    def p_tool_control_flow_for(self, p):
        '''tool_control_flow : FOR IDENTIFIER IN tool_or_expr DO tool_statement_or_block'''
        p[0] = ('for', p[2], p[4], p[6])

    def p_tool_control_flow_while(self, p):
        '''tool_control_flow : WHILE tool_or_expr DO tool_statement_or_block'''
        p[0] = ('while', p[2], p[4])

    def p_tool_statement_or_block(self, p):
        '''tool_statement_or_block : tool_statement
                                   | tool_block'''
        p[0] = p[1]
    
    def p_tool_block(self, p):
        '''tool_block : LBRACE tool_statement_list RBRACE'''
        p[0] = ('block', p[2])

    def p_param_list(self, p):
        '''param_list : IDENTIFIER COMMA param_list
                      | IDENTIFIER
                      | empty'''
        if len(p) == 4:
            p[0] = [p[1]] + p[3]
        elif len(p) == 2:
            p[0] = [p[1]]
        else:
            p[0] = []

    def p_block(self, p):
        '''block : LBRACE statement_list RBRACE'''
        p[0] = ('block', p[2])
    
    def p_expression_block(self, p):
        '''expression_block : LBRACE expression_statement_list RBRACE'''
        p[0] = ('block', p[2])
    
    def p_expression_statement_list(self, p):
        '''expression_statement_list : expression_statement expression_statement_list
                                     | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        else:
            p[0] = []
    
    def p_expression_statement(self, p):
        '''expression_statement : assignment
                                | tuple_assignment
                                | agent_definition
                                | agent_block
                                | command_definition
                                | tool_def
                                | control_flow
                                | expression'''
        p[0] = p[1]

    def p_control_flow_when(self, p):
        '''control_flow : WHEN expression THEN statement_or_block OTHERWISE statement_or_block'''
        p[0] = ('when', p[2], p[4], p[6])

    def p_control_flow_for(self, p):
        '''control_flow : FOR IDENTIFIER IN expression DO statement_or_block'''
        p[0] = ('for', p[2], p[4], p[6])

    def p_control_flow_while(self, p):
        '''control_flow : WHILE expression DO statement_or_block'''
        p[0] = ('while', p[2], p[4])

    def p_statement_or_block(self, p):
        '''statement_or_block : statement
                              | block'''
        p[0] = p[1]

    def p_expression(self, p):
        '''expression : when_expr
                      | then_expr'''
        p[0] = p[1]
    
    def p_then_expr(self, p):
        '''then_expr : or_expr THEN then_expr
                     | or_expr'''
        if len(p) == 4:
            p[0] = ('chain', p[1], p[3])
        else:
            p[0] = p[1]
    
    def p_or_expr(self, p):
        '''or_expr : and_expr'''
        p[0] = p[1]
        
    def p_and_expr(self, p):
        '''and_expr : compare_expr'''
        p[0] = p[1]
    
    def p_compare_expr(self, p):
        '''compare_expr : add_expr GT add_expr
                        | add_expr GE add_expr
                        | add_expr LT add_expr
                        | add_expr LE add_expr
                        | add_expr EQEQ add_expr
                        | add_expr NE add_expr
                        | add_expr'''
        if len(p) == 4:
            op = {'>' : 'gt', '>=': 'ge', '<': 'lt', '<=': 'le', '==': 'eq', '!=': 'ne'}[p[2]]
            p[0] = ('binary', op, p[1], p[3])
        else:
            p[0] = p[1]
            
    def p_add_expr(self, p):
        '''add_expr : mul_expr PLUS add_expr
                    | mul_expr MINUS add_expr
                    | mul_expr'''
        if len(p) == 4:
            op = {'+': 'add', '-': 'sub'}[p[2]]
            p[0] = ('binary', op, p[1], p[3])
        else:
            p[0] = p[1]
            
    def p_mul_expr(self, p):
        '''mul_expr : unary_expr MULTIPLY mul_expr
                    | unary_expr'''
        if len(p) == 4:
            p[0] = ('binary', 'mul', p[1], p[3])
        else:
            p[0] = p[1]
            
    def p_unary_expr(self, p):
        '''unary_expr : MINUS unary_expr %prec UMINUS
                      | atom_expr'''
        if len(p) == 3:
            p[0] = ('unary', 'neg', p[2])
        else:
            p[0] = p[1]
            
    def p_atom_expr(self, p):
        '''atom_expr : alloy_command
                     | agent_command
                     | agent_block
                     | primary_expr'''
        p[0] = p[1]

    def p_primary_expr(self, p):
        '''primary_expr : literal
                        | identifier_or_call
                        | identifier_expr'''
        p[0] = p[1]

    def p_identifier_or_call(self, p):
        '''identifier_or_call : IDENTIFIER LPAREN expression_list RPAREN'''
        # This is a tool call
        p[0] = ('tool_call', p[1], p[3])
        
    def p_identifier_expr(self, p):
        '''identifier_expr : IDENTIFIER'''
        # This is just an identifier
        p[0] = p[1]

    def p_literal_string(self, p):
        '''literal : STRING'''
        p[0] = ('string', p[1].strip('"\''))

    def p_literal_number(self, p):
        '''literal : NUMBER'''
        p[0] = ('number', p[1])

    def p_literal_bool(self, p):
        '''literal : BOOLEAN'''
        p[0] = ('bool', p[1] == 'true')
    
    def p_literal_null(self, p):
        '''literal : NULL'''
        p[0] = ('null', None)

    def p_literal_array(self, p):
        '''literal : LBRACKET expression_list RBRACKET'''
        p[0] = ('array', p[2])

    def p_literal_object(self, p):
        '''literal : LBRACE key_value_list RBRACE'''
        p[0] = ('object', dict(p[2]))
    
    def p_literal_tuple(self, p):
        '''literal : LPAREN expression_list RPAREN'''
        # If it's a single element without commas, it's just parentheses for grouping
        expr_list = p[2]
        if expr_list and len(expr_list) == 1:
            # Single expression in parentheses - just return the expression
            p[0] = expr_list[0]
        else:
            # Multiple expressions or empty - it's a tuple
            p[0] = ('tuple', expr_list)

    def p_expression_list(self, p):
        '''expression_list : expression COMMA expression_list
                           | expression
                           | empty'''
        if len(p) == 4:
            p[0] = [p[1]] + p[3]
        elif len(p) == 2:
            p[0] = [p[1]]
        else:
            p[0] = []

    def p_key_value_list(self, p):
        '''key_value_list : IDENTIFIER COLON expression COMMA key_value_list
                          | IDENTIFIER COLON expression
                          | empty'''
        if len(p) == 6:
            p[0] = [(p[1], p[3])] + p[5]
        elif len(p) == 4:
            p[0] = [(p[1], p[3])]
        else:
            p[0] = []


    def p_alloy_command_long(self, p):
        '''alloy_command : ALLOY IDENTIFIER IDENTIFIER FOR IDENTIFIER'''
        # alloy Review code for improvements
        p[0] = ('agent_command', 'alloy', f"{p[2]} {p[3]} for {p[5]}", None)
    
    def p_alloy_command_keywords(self, p):
        '''alloy_command : ALLOY IDENTIFIER TOOL
                         | ALLOY IDENTIFIER AGENT
                         | ALLOY IDENTIFIER COMMAND
                         | ALLOY IDENTIFIER MEANS'''
        # alloy Generate tool, etc.
        second_word = p[3] if isinstance(p[3], str) else str(p[3])
        p[0] = ('agent_command', 'alloy', f"{p[2]} {second_word}", None)
    
    def p_alloy_command_two_words(self, p):
        '''alloy_command : ALLOY IDENTIFIER IDENTIFIER'''
        # alloy Review code
        p[0] = ('agent_command', 'alloy', f"{p[2]} {p[3]}", None)
    
    def p_alloy_command_with_string(self, p):
        '''alloy_command : ALLOY IDENTIFIER STRING'''
        # alloy Review "tool_code"
        p[0] = ('agent_command', 'alloy', p[2], p[3])
    

    def p_agent_command_string(self, p):
        '''agent_command : IDENTIFIER STRING %prec AGENT_COMMAND'''
        # Direct command with string: claude "Analyze this data"
        agent_ref = ('agent_var', p[1])
        p[0] = ('agent_command', agent_ref, p[2], None)
    
    def p_agent_command_with_string_data(self, p):
        '''agent_command : IDENTIFIER IDENTIFIER STRING %prec AGENT_COMMAND'''  
        # Command with string data: claude Analyze "user data"
        agent_ref = ('agent_var', p[1])
        p[0] = ('agent_command', agent_ref, p[2], p[3])
    
    def p_agent_command_with_identifier_data(self, p):
        '''agent_command : IDENTIFIER IDENTIFIER IDENTIFIER %prec AGENT_COMMAND'''
        # Command with identifier data: claude Analyze user_data
        agent_ref = ('agent_var', p[1])
        p[0] = ('agent_command', agent_ref, p[2], p[3])
        
    def p_agent_command_two_word(self, p):
        '''agent_command : IDENTIFIER IDENTIFIER %prec AGENT_COMMAND'''
        # Two-word command: claude Analyze
        agent_ref = ('agent_var', p[1])
        p[0] = ('agent_command', agent_ref, p[2], None)
            

    def p_agent_block(self, p):
        '''agent_block : IDENTIFIER LBRACE agent_statement_list RBRACE %prec LBRACE
                       | agent_ref LBRACE agent_statement_list RBRACE %prec LBRACE'''
        if len(p) == 5 and isinstance(p[1], str):
            # IDENTIFIER agent block - reference to a defined agent variable
            agent_ref = ('agent_var', p[1])
            p[0] = ('agent_block', agent_ref, p[3])
        else:
            # agent_ref agent block (ALLOY or AGENT STRING)
            p[0] = ('agent_block', p[1], p[3])

    def p_agent_ref(self, p):
        '''agent_ref : AGENT STRING
                     | ALLOY'''
        if p[1] == 'alloy':
            p[0] = ('alloy')
        else:
            # Old syntax: agent "model-name"
            p[0] = ('agent', p[2])

    def p_agent_statement_list(self, p):
        '''agent_statement_list : agent_statement agent_statement_list
                                | empty'''
        if len(p) == 3:
            p[0] = [p[1]] + p[2]
        else:
            p[0] = []

    def p_agent_statement(self, p):
        '''agent_statement : agent_must_statement_with_data
                           | agent_must_statement_simple
                           | agent_should_statement
                           | agent_data_statement
                           | agent_simple_command'''
        p[0] = p[1]

    def p_agent_must_statement_with_data(self, p):
        '''agent_must_statement_with_data : MUST IDENTIFIER IDENTIFIER'''
        p[0] = ('must', p[2], p[3])
    
    def p_agent_must_statement_simple(self, p):
        '''agent_must_statement_simple : MUST STRING
                                       | MUST IDENTIFIER'''
        p[0] = ('must', p[2])
    
    def p_agent_should_statement(self, p):
        '''agent_should_statement : SHOULD STRING
                                   | SHOULD IDENTIFIER
                                   | SHOULD IDENTIFIER IDENTIFIER
                                   | SHOULD IDENTIFIER STRING'''
        if len(p) == 3:
            # SHOULD STRING or SHOULD IDENTIFIER case
            p[0] = ('should', p[2])
        else:
            # SHOULD IDENTIFIER expression case
            p[0] = ('should', p[2], p[3])
    
    def p_agent_simple_command(self, p):
        '''agent_simple_command : IDENTIFIER'''
        p[0] = ('simple_command', p[1])
    
    def p_agent_data_statement(self, p):
        '''agent_data_statement : IDENTIFIER COLON expression'''
        if p[1] == 'data':
            p[0] = ('data', p[3])
        else:
            p[0] = ('data', p[1], p[3])


    def p_when_expr(self, p):
        '''when_expr : WHEN when_cond THEN when_then OTHERWISE when_else'''
        p[0] = ('when', p[2], p[4], p[6])
        
    def p_when_cond(self, p):
        '''when_cond : or_expr'''
        p[0] = p[1]
        
    def p_when_then(self, p):
        '''when_then : or_expr
                     | expression_block'''
        p[0] = p[1]
        
    def p_when_else(self, p):
        '''when_else : or_expr
                     | expression_block'''
        p[0] = p[1]

    def p_empty(self, p):
        'empty :'
        pass

    def p_error(self, p):
        """Handle syntax errors"""
        if p:
            print(f"Syntax error at token '{p.value}' (line {p.lineno})")
        else:
            print("Syntax error at EOF")

    def build(self):
        """Build the parser"""
        # PLY will write parser.out and parsetab.py in the current directory
        # We can control this with outputdir parameter if needed
        import os
        outputdir = os.path.dirname(__file__)
        # Force regeneration by clearing any cached parser
        import ply.yacc as yacc
        if hasattr(yacc, '_lr_table'):
            del yacc._lr_table
        self.parser = yacc.yacc(module=self, debug=False, write_tables=True, outputdir=outputdir)
        return self.parser

    def parse(self, data: str, debug: bool = False):
        """
        Parse input data and return AST.
        
        Args:
            data: Input string to parse
            debug: Enable debug output
            
        Returns:
            Abstract syntax tree
        """
        if not self.parser:
            self.build()
        
        return self.parser.parse(data, lexer=self.lexer.lexer, debug=debug)

    def test(self, data: str):
        """
        Test the parser with input data and print AST.
        
        Args:
            data: Input string to parse
        """
        print(f"Parsing: {data}")
        print("-" * 40)
        
        try:
            ast = self.parse(data)
            print(f"AST: {ast}")
        except Exception as e:
            print(f"Parse error: {e}")


def create_parser(lexer: Optional[AlloyLexer] = None):
    """
    Factory function to create a new parser instance.
    
    Args:
        lexer: Lexer instance to use
        
    Returns:
        AlloyParser instance
    """
    return AlloyParser(lexer)


# Global parser instance for backward compatibility
_global_parser = None

def get_global_parser():
    """
    Get the global parser instance, creating it if necessary.
    
    Returns:
        PLY parser object for backward compatibility
    """
    global _global_parser
    if _global_parser is None:
        _global_parser = AlloyParser()
    return _global_parser.parser


# For backward compatibility - expose the PLY parser directly
parser = get_global_parser()


if __name__ == "__main__":
    # Test the parser
    test_code = '''
    tool add_numbers takes a, b returns sum {
        sum = a + b
        return sum
    }
    
    claude = agent "gpt-4o-mini"
    result, error = claude "analyze this data"
    '''
    
    alloy_parser = AlloyParser()
    alloy_parser.test(test_code)