Interpreter

### Introduction

This code is an implementation of a programming language interpreter that includes a lexer, a parser, and an interpreter. The lexer is responsible for identifying the different types of tokens, such as keywords, operators, and literals. It uses regular expressions to match patterns in the input code and generate tokens.

The parser reads through the tokens generated by the lexer and builds an abstract syntax tree (AST) that represents the structure of the program. The AST allows the interpreter to traverse and execute each statement.

The interpreter executes the AST by calling the execute function for each statement in the AST. The execute function checks the type of each statement and executes it accordingly.

The program state data structure is a dictionary that stores variable values, function definitions, and other state information. It also includes a call stack to keep track of function calls and a flag to handle if statements.

The interpreter includes support for basic programming constructs such as if-else statements, loops, and functions. It also includes error handling through a custom exception called InterpreterError and a ReturnException exception to handle the return statement in functions.

Overall, this code provides a foundation for a simple programming language that can be used to write programs. It has the ability to define functions, use if-else statements and loops, and handle errors during the execution of programs.

# Define custom exceptions
        self.char = char
        super().__init__(f"Invalid character: {self.char}")

# Define the syntax rules
KEYWORDS = ["def", "if", "else", "while", "for", "in", "return"]
OPERATORS = ["+", "-", "*", "/", "%", "<", ">", "<=", ">=", "==", "!=", "=", "(", ")", "{", "}", ",", ":", "[", "]", "&", "|", "^", "<<", ">>", "and", "or", "not"]
WHITESPACE = [" ", "\n", "\t"]

# Define the supported data types
DATA_TYPES = ["int", "float", "str", "bool", "list"]

# Create the lexical analyzer
def lexer(code):
    tokens = []
    current_token = ""
                tokens.append(get_token(current_token))
                current_token = ""
        else:
            current_token += char
    if current_token:
        tokens.append(get_token(current_token))
    return tokens

        return ("keyword", token)
    elif token in DATA_TYPES:
        return ("data_type", token)
    elif token.isalpha() or token == "_":
        return ("identifier", token)
    elif "." in token:
        return ("literal", float(token))
    elif token.isdigit():
        return ("literal", int(token))
    elif token == "&&":
        return ("operator", "and")
    elif token == "||":
        return ("operator", "or")
    elif token == "<<":
        return ("operator", "<<")
    elif token == ">>":
        return ("operator", ">>")
    elif token in OPERATORS:
        return ("operator", token)
    elif token == ")":
        return ("literal", token)
    else:
        raise InvalidCharacterError(token)

IF = "if"
WHILE = "while"
FOR = "for"
RETURN = "return"
EXPRESSION = "expression"

# Create the parser
        if_block = parse(tokens[i+2:])
        if i + 2 + len(if_block) < len(tokens) and tokens[i+2+len(if_block)][1] == "else":
            else_block = parse(tokens[i+3+len(if_block):])
            return [{"type": IF, "condition": parse_expression(tokens[i+1:]), "if_block": if_block, "else_block": else_block}]
        else:
            return [{"type": IF, "condition": parse_expression(tokens[i+1:]), "if_block": if_block}]

    return [statement for i, token in enumerate(tokens)            for statement in (                parse_if_else(tokens, i) if token[1] == IF else
                [{"type": WHILE, "condition": parse_expression(tokens[i+1:]), "body": parse(tokens[i+2:])}] if token[1] == WHILE else
                [{"type": FOR, "variable": tokens[i+1][1], "iterable": parse_expression(tokens[i+3:]), "body": parse(tokens[i+4+len(parse_expression(tokens[i+3:])):])}] if token[1] == FOR else
                [{"type": RETURN, "expression": parse_expression(tokens[i+1:])}] if token[1] == RETURN else
                [{"type": EXPRESSION, "expression": parse_expression(tokens[i:])}]
            )]

# Define custom exceptions
class InterpreterError(Exception):
    pass

class ReturnException(Exception):
    pass

class VariableNotFoundError(InterpreterError):
    def __init__(self, variable_name):
        self.variable_name = variable_name

    def __str__(self):
        return f"Variable '{self.variable_name}' not found"

class InvalidExpressionError(InterpreterError):
    def __init__(self, expression):
        self.expression = expression

    def __str__(self):
        return f"Invalid expression: '{self.expression}'"

# Define the expression parser function
        raise InvalidExpressionError("empty expression")

    # Check if the expression is a simple literal or variable name
    if len(tokens) == 1:
        token_type, token_value = tokens[0]
        if token_type == "identifier":
            return ("variable", token_value)
        elif token_type == "literal":
            return ("literal", token_value)
        else:
            raise InvalidExpressionError(f"invalid token in expression: {token_value}")

    # Check if the expression is a function call
    if tokens[1][1] == "(":
        func_name = tokens[0][1]
        func_args = []
        i = 2
        while i < len(tokens) - 1:
            arg_tokens = []
            j = i
            while j < len(tokens) - 1 and tokens[j][1] != ",":
                arg_tokens.append(tokens[j])
                j += 1
            arg_tokens.append(tokens[j])
            func_args.append(parse_expression(arg_tokens))
            i = j + 1
        return ("function_call", func_name, func_args)

    # Check if the expression is a binary operation
    # Binary operators are evaluated from left to right
    operators = ["or", "and", "|", "^", "&", "==", "!=", "<", "<=", ">", ">=", "<<", ">>", "+", "-",
                 "*", "/", "%"]
    for op in operators:
        i = 1
                left_expr = parse_expression(tokens[:i])
                right_expr = parse_expression(tokens[i+1:])
                return ("binary_operation", op, left_expr, right_expr)
            i += 1

    # Check if the expression is a unary operation
    if tokens[0][1] in ["not", "+", "-"]:
        operand_expr = parse_expression(tokens[1:])
        return ("unary_operation", tokens[0][1], operand_expr)

    # Check if the expression is an indexing operation
    if tokens[-1][1] == "]" and tokens[-3][1] == "[":
        index_expr = parse_expression(tokens[-2:-1])
        base_expr = parse_expression(tokens[:-3])
        return ("indexing_operation", base_expr, index_expr)

    # If we get here, the expression is not valid
    raise InvalidExpressionError(f"invalid expression: {[token[1] for token in tokens]}")

# Define the program state data structure
VARIABLES = "variables"
CALL_STACK = "call_stack"
IF_FLAG = "if_flag"
OPEN_FILES = "open_files"
FUNCTIONS = "functions"

program_state = {
    VARIABLES: {},  # dictionary to store variable values
    CALL_STACK: [],  # stack to keep track of function calls
    IF_FLAG: False,  # boolean flag for IF statements
    OPEN_FILES: [],  # list of open file handles
    FUNCTIONS: {}  # dictionary to store user-defined functions
}

# Define the interpreter function
def interpret(ast):
    # Traverse the abstract syntax tree and execute each statement
            execute(statement)
        except InterpreterError as e:
            print(f"Interpreter error: {e}")
            return

# Define custom exceptions
class VariableNotDefinedException(Exception):
    pass

class InvalidFunctionCallException(Exception):
    pass

class ReturnException(Exception):
    pass

class FileOpenException(Exception):
    pass

class FileWriteException(Exception):
    pass

class FileCloseException(Exception):
    pass

def execute(statement):
    statement_type = statement["type"]
    if statement_type == "assignment":
        variable_name = statement["variable_name"]
        value = evaluate_expression(statement["expression"])
        program_state[VARIABLES][variable_name] = value
    elif statement_type == "if":
        condition = evaluate_expression(statement["condition"])
        program_state[IF_FLAG] = bool(condition)
    elif statement_type == "else":
        program_state[IF_FLAG] = not program_state[IF_FLAG]
    elif statement_type == "function_definition":
        function_name = statement["function_name"]
        program_state[FUNCTIONS][function_name] = statement["body"]
    elif statement_type == "function_call":
        function_name = statement["function_name"]
        try:
            function_body = program_state[FUNCTIONS][function_name]
        except KeyError:
            raise InvalidFunctionCallException(f"Function '{function_name}' not defined")
        program_state[CALL_STACK].append({
            "function_name": function_name,
            "locals": {}
        })
        [(program_state[CALL_STACK][-1]["locals"].update({function_body["arguments"][i]: evaluate_expression(arg)})) for i, arg in enumerate(statement["arguments"])]
        interpret(function_body["body"])
        program_state[CALL_STACK].pop()
            program_state[CALL_STACK][-1]["return_value"] = evaluate_expression(statement["expression"])
        except IndexError:
            raise ReturnException("Return statement not inside a function")
        raise ReturnException()
            filename = evaluate_expression(statement["filename"])
            mode = evaluate_expression(statement["mode"])
            file_handle = open(filename, mode)
            program_state[OPEN_FILES].append(file_handle)
        except FileNotFoundError:
            raise FileOpenException(f"File '{filename}' not found")
        except IOError:
            raise FileOpenException(f"Error opening file '{filename}' with mode '{mode}'")
            file_handle = evaluate_expression(statement["file_handle"])
            text = evaluate_expression(statement["text"])
            file_handle.write(text)
        except AttributeError:
            raise FileWriteException("Invalid file handle")
        except TypeError:
            raise FileWriteException("Invalid text")
            file_handle = evaluate_expression(statement["file_handle"])
            file_handle.close()
            program_state[OPEN_FILES] = [fh for fh in program_state[OPEN_FILES] if fh != file_handle]
        except AttributeError:
            raise FileCloseException("Invalid file handle")

# Define custom exceptions
class EvaluationError(Exception):
    pass

class UndefinedVariableError(EvaluationError):
    def __init__(self, variable_name):
        self.variable_name = variable_name

    def __str__(self):
        return f"Undefined variable: {self.variable_name}"

class DivisionByZeroError(EvaluationError):
    def __init__(self):
        pass

    def __str__(self):
        return "Division by zero"

# Define constants for the expression types
LITERAL = "literal"
VARIABLE = "variable"
BINARY = "binary"
UNARY = "unary"
CALL = "call"

# Define constants for the binary operators
ADD = "+"
SUBTRACT = "-"
MULTIPLY = "*"
DIVIDE = "/"
MODULO = "%"
LESS_THAN = "<"
GREATER_THAN = ">"
LESS_THAN_OR_EQUAL = "<="
GREATER_THAN_OR_EQUAL = ">="
EQUALS = "=="
NOT_EQUALS = "!="

# Define constants for the unary operators
PLUS = "+"
MINUS = "-"
NOT = "!"

# Define the evaluate_expression function to evaluate an expression
def evaluate_expression(expression):
    # Check the type of the expression and evaluate it accordingly
    if expression["type"] == LITERAL:
        # Return the value of a literal expression
        return expression["value"]
    elif expression["type"] == VARIABLE:
        # Return the value of a variable
        variable_name = expression["name"]
        if variable_name not in program_state["variables"]:
            raise UndefinedVariableError(variable_name)
        return program_state["variables"][variable_name]
    elif expression["type"] == BINARY:
        # Evaluate a binary expression
        left_value = evaluate_expression(expression["left"])
        right_value = evaluate_expression(expression["right"])
        operator = expression["operator"]
        if operator == ADD:
            return left_value + right_value
        elif operator == SUBTRACT:
            return left_value - right_value
        elif operator == MULTIPLY:
            return left_value * right_value
                raise DivisionByZeroError()
            return left_value / right_value
                raise DivisionByZeroError()
            return left_value % right_value
        elif operator == LESS_THAN:
            return left_value < right_value
        elif operator == GREATER_THAN:
            return left_value > right_value
        elif operator == LESS_THAN_OR_EQUAL:
            return left_value <= right_value
        elif operator == GREATER_THAN_OR_EQUAL:
            return left_value >= right_value
        elif operator == EQUALS:
            return left_value == right_value
        elif operator == NOT_EQUALS:
            return left_value != right_value
    elif expression["type"] == UNARY:
        # Evaluate a unary expression
        operand_value = evaluate_expression(expression["operand"])
        operator = expression["operator"]
        if operator == PLUS:
            return operand_value
        elif operator == MINUS:
            return -operand_value
        elif operator == NOT:
            return not operand_value
    elif expression["type"] == CALL:
        # Call a function
        function_name = expression["function_name"]
        function_body = program_state["functions"][function_name]
        # Push a new frame onto the call stack
        program_state["call_stack"].append({
            "function_name": function_name,
            "locals": {}
        })
        class InterpreterError(Exception):
    """Base class for interpreter exceptions"""
    pass

class ReturnException(Exception):
    """Raised when a function returns a value"""
    def __init__(self, value):
        self.value = value

# Set the values of the arguments in the new frame
arguments = [(function_body["arguments"][i], evaluate_expression(arg)) for i, arg in enumerate(expression["arguments"])]
for arg_name, arg_value in arguments:
    program_state["call_stack"][-1]["locals"][arg_name] = arg_value
# Execute the function body
try:
    interpret(function_body["body"])
except ReturnException as e:
    # Pop the frame off the call stack and return the return value
    program_state["call_stack"].pop()
    return e.value
except Exception as e:
    # If there was an exception, raise a custom exception with a message
    raise InterpreterError(f"Error interpreting function: {str(e)}")
else:
    # If the function didn't return a value, return None
    program_state["call_stack"].pop()
    return None

# Example usage
code = """
def add(a: int, b: int) -> int:
    return a + b

print(add(1, 2))

a = 3
b = 4
print(a & b)
print(a | b)
print(a ^ b)
print(a << 1)
print(b >> 1)

if a > 2 and b < 5:
    print("Both conditions are true.")

if not a == b:
    print("a is not equal to b.")
"""

tokens = lexer(code)
ast = parse(tokens)
try:
    result = interpret(ast)
except InterpreterError as e:
    print(f"Interpreter error: {str(e)}")
else:
    print(result)
```

### Testing the Interpreter

Here are the instructions to test the KYky interpreter with a new test.ky code:

1. Install Python 3 on your computer if it is not already installed.
2. Copy the KYky interpreter code to a file named kyky.py.
3. Create a new file with KYky code that you want to test. For example, you can create a file named test.ky and put the following code in it:
```
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

def main():
    num = int(input("Enter a number to generate the Fibonacci sequence up to that number: "))
    for i in range(num):
        print(fibonacci(i))

main()
```
4. Open a terminal or command prompt and navigate to the directory where the kyky.py and test.ky files are located
5. Run the interpreter by executing the command `python kyky.py test.ky`. This will read the KYky code in test.ky, parse it, and generate equivalent Python code.
6. The output of the interpreter should be the following Python code:
```
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

def main():
    num = int(input("Enter a number to generate the Fibonacci sequence up to that number: "))
    for i in range(num):
        print(fibonacci(i))

main()
```
7. Run the generated Python code by executing the command `python generated.py`. This will run the Python code and prompt you to enter a number to generate the Fibonacci sequence up to that number.
8. Enter a number and press enter to generate the Fibonacci sequence up to that number.
9. The program should output the Fibonacci sequence up to the entered number.

### Testing the Parser

To test this code, you could create a test script that contains a variety of Python code snippets and use the `lexer` and `parse` functions to tokenize and parse each code snippet. You could then compare the output of the parser to the expected output for each input.

Here is an example of how you could create a test script:

```

# Define some test code snippets
test_cases = [
    {
        "input": "def foo(x, y):\n    return x + y\n",
        "expected_output": [
            {
                "type": "function_definition",
                "name": "foo",
                "parameters": ["x", "y"],
                "body": [
                    {
                        "type": "return_statement",
                        "value": {
                            "type": "binary_operation",
                            "operator": "+",
                            "left_operand": {"type": "variable", "name": "x"},
                            "right_operand": {"type": "variable", "name": "y"}
                        }
                    }
                ]
            }
        ]
    },
    {
        "input": "if x > 0:\n    y = x\nelse:\n    y = -x\n",
        "expected_output": [
            {
                "type": "if_statement",
                "condition": {
                    "type": "binary_operation",
                    "operator": ">",
                    "left_operand": {"type": "variable", "name": "x"},
                    "right_operand": {"type": "literal", "value": 0}
                },
                "if_block": [
                    {
                        "type": "assignment_statement",
                        "target": {"type": "variable", "name": "y"},
                        "value": {"type": "variable", "name": "x"}
                    }
                ],
                "else_block": [
                    {
                        "type": "assignment_statement",
                        "target": {"type": "variable", "name": "y"},
                        "value": {
                            "type": "unary_operation",
                            "operator": "-",
                            "operand": {"type": "variable", "name": "x"}
                        }
                    }
                ]
            }
        ]
    },
    # Add more test cases here...
]

# Test the lexer and parser on each test case
for i, test_case in enumerate(test_cases):
    input_code = test_case["input"]
    expected_output = test_case["expected_output"]

    # Tokenize the input code
    tokens = lexer(input_code)

    # Parse the tokens into an abstract syntax tree (AST)
    ast = parse(tokens)

    # Compare the actual output to the expected output
    if ast == expected_output:
        print(f"Test case {i+1} passed!")
    else:
        print(f"Test case {i+1} failed.")
        print(f"Expected output: {expected_output}")
        print(f"Actual output: {ast}")
```
In this example, `test_cases` is a list of dictionaries, where each dictionary contains an input code snippet and the expected output of the parser. The `for` loop iterates over each test case, tokenizes the input code using the `lexer` function, parses the tokens into an abstract syntax tree using the `parse` function, and compares the actual output to the expected output. If the actual output matches the expected output, the test case passes; otherwise, it fails.