Compiler

### Introduction

In this code, we have implemented a simple programming language called KYky. The language has a Python-like syntax and supports basic arithmetic and logic operations, loops, conditional statements, and function definitions. Our implementation includes a lexer and parser using the Ply library.

To ensure clarity and maintainability, we have used good coding practices such as custom exceptions, type hints, and Enums to define the token types and bytecode instructions. We have also implemented a bytecode generator that produces bytecode for the input program.

However, we acknowledge that the code could benefit from additional documentation and comments to improve its readability. Additionally, there are some incomplete parser rules, so the language implementation may not be fully functional. Despite these limitations, our implementation provides a solid foundation for further development of the KYky programming language.

### PROTOTYPE (Python)

```
import unittest
from enum import Enum
import ply.lex as lex
import ply.yacc as yacc
from typing import Dict, Union

# Define custom exceptions
class InvalidSyntaxError(Exception):
    pass

# Define the syntax rules
class TokenType(Enum):
    KEYWORD = 1
    IDENTIFIER = 2
    LITERAL = 3
    OPERATOR = 4

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

TOKEN_TYPES: Dict[Union[str, int], TokenType] = {
    **{keyword: TokenType.KEYWORD for keyword in KEYWORDS},
    **{operator: TokenType.OPERATOR for operator in OPERATORS},
}

# Define the bytecode instructions
class Bytecode(Enum):
    LOAD_CONST = 1
    LOAD_NAME = 2
    STORE_NAME = 3
    BINARY_ADD = 23
    BINARY_SUBTRACT = 24
    BINARY_MULTIPLY = 20
    BINARY_DIVIDE = 21
    BINARY_MODULO = 22
    COMPARE_OP = 107
    JUMP_FORWARD = 110
    JUMP_IF_FALSE_OR_POP = 111
    POP_JUMP_IF_FALSE = 114
    POP_JUMP_IF_TRUE = 115
    CALL_FUNCTION = 131
    RETURN_VALUE = 83

# Define the bytecode generator
        self.bytecode: list[int] = []
        self.labels: Dict[int, Union[int, None]] = {}

    def emit(self, opcode: Bytecode, arg: Union[int, None]=None) -> None:
        self.bytecode.append(opcode.value)
        if arg is not None:
            self.bytecode.append(arg)

    def make_label(self) -> int:
        label = len(self.bytecode)
        self.labels[label] = None
        return label

    def set_label(self, label: int) -> None:
        self.labels[label] = len(self.bytecode)

                raise InvalidSyntaxError(f"Undefined label {label}")
            self.bytecode[label] = offset
        return bytes(self.bytecode)

# Define the lexer tokens
tokens = [
    "NAME",
    "NUMBER",
    "STRING",
    "PLUS",
    "MINUS",
    "TIMES",
    "DIVIDE",
    "MODULO",
    "LPAREN",
    "RPAREN",
    "LBRACKET",
    "RBRACKET",
    "LBRACE",
    "RBRACE",
    "COMMA",
    "COLON",
    "SEMICOLON",
    "EQ",
    "NEQ",
    "LT",
    "GT",
    "LE",
    "GE",
    "AND",
    "OR",
    "NOT",
    "IF",
    "ELSE",
    "WHILE",
    "FOR",
    "IN",
    "RETURN"
]

# Define the lexer rules
t_PLUS: str = r"\+"
t_MINUS: str = r"-"
t_TIMES: str = r"\*"
t_DIVIDE: str = r"/"
t_MODULO: str = r"%"
t_LPAREN: str = r"\("
t_RPAREN: str = r"\)"
t_LBRACKET: str = r"\["
t_RBRACKET: str = r"\]"
t_LBRACE: str = r"\{"
t_RBRACE: str = r"\}"
t_COMMA: str = r","
t_COLON: str = r":"
t_SEMICOLON: str = r";"
t_EQ: str = r"=="
t_NEQ: str = r"!="
t_LT: str = r"<"
t_GT: str = r">"
t_LE: str = r"<="
t_GE: str = r">="
t_AND: str = r"and"
t_OR: str = r"or"
t_NOT: str = r"not"

def t_NAME(t):
    r"[a-zA-Z_][a-zA-Z0-9_]*"
    t.type = TOKEN_TYPES.get(t.value, TokenType.IDENTIFIER)
    return t

def t_NUMBER(t):
    r"\d+"
    t.value = int(t.value)
    t.type = TokenType.LITERAL
    return t

def t_STRING(t):
    r"\"[^\"]*\""
    t.value = str(t.value)[1:-1]
    t.type = TokenType.LITERAL
    return t

def t_newline(t):
    r"\n+"
    t.lexer.lineno += len(t.value)

t_ignore = "".join(WHITESPACE)

def t_error(t):
    raise InvalidSyntaxError(f"Invalid character: {t.value[0]}")

lexer = lex.lex()

# Define the parser rules
precedence = (
    ("left", "OR"),
    ("left", "AND"),
    ("left", "NOT"),
    ("nonassoc", "LT", "LE", "GT", "GE", "EQ", "NEQ"),
    ("left", "PLUS", "MINUS"),
    ("left", "TIMES", "DIVIDE", "MODULO"),
)

def p_program(p):
    """program : statement_list"""
    p[0] = p[1]

def p_statement_list(p):
    """statement_list : statement
    if len(p) == 2:
        p[0] = [p[1]]
    else:
        p[1].append(p[2])
        p[0] = p[1]

def p_statement(p):
    """statement : expression_statement
    p[0] = p[1]

def p_expression_statement(p):
    """expression_statement : expression SEMICOLON"""
    p[0] = ("EXPR", p[1])

def p_if_statement(p):
    """if_statement : IF LPAREN expression RPAREN LBRACE statement_list RBRACE
    if len(p) == 8:
        p[0] = ("IF", p[3], p[6], [])
    else:
        p[0] = ("IF", p[3], p[6], p[10])

def p_while_statement(p):
    """while_statement : WHILE LPAREN expression RPAREN LBRACE statement_list RBRACE"""
    p[0] = ("WHILE", p[3], p[6])
```

### Testing the Compiler

To test your KYky compiler with the provided code, you can follow these steps:

1. Save the following code as a file, for example `test_compiler.py`.
```
lass TestCompiler(unittest.TestCase):
    def test_compile_arithmetic_expression(self):
        source_code = "2 + 3 * 4"
        expected_bytecode = bytes([
            Bytecode.LOAD_CONST.value, 2,
            Bytecode.LOAD_CONST.value, 3,
            Bytecode.LOAD_CONST.value, 4,
            Bytecode.BINARY_MULTIPLY.value,
            Bytecode.BINARY_ADD.value,
            Bytecode.RETURN_VALUE.value,
        ])
        compiler = Compiler()
        bytecode = compiler.compile(source_code)
        self.assertEqual(bytecode, expected_bytecode)

    def test_compile_boolean_expression(self):
        source_code = "not (True or False) and True"
        expected_bytecode = bytes([
            Bytecode.LOAD_CONST.value, True,
            Bytecode.LOAD_CONST.value, False,
            Bytecode.COMPARE_OP.value, Operator.OR.value,
            Bytecode.NOT.value,
            Bytecode.LOAD_CONST.value, True,
            Bytecode.COMPARE_OP.value, Operator.AND.value,
            Bytecode.RETURN_VALUE.value,
        ])
        compiler = Compiler()
        bytecode = compiler.compile(source_code)
        self.assertEqual(bytecode, expected_bytecode)

    def test_compile_if_statement(self):
        source_code = "if x < 0:\n    return -x\nelse:\n    return x"
        expected_bytecode = bytes([
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.LOAD_CONST.value, 0,
            Bytecode.COMPARE_OP.value, Operator.LT.value,
            Bytecode.POP_JUMP_IF_FALSE.value, 12,
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.UNARY_NEGATIVE.value,
            Bytecode.RETURN_VALUE.value,
            Bytecode.JUMP_FORWARD.value, 5,
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.RETURN_VALUE.value,
        ])
        compiler = Compiler()
        bytecode = compiler.compile(source_code)
        self.assertEqual(bytecode, expected_bytecode)

    def test_compile_function_definition(self):
        source_code = "def add(x, y):\n    return x + y"
        expected_bytecode = bytes([
            Bytecode.LOAD_CONST.value, <code object add>,
            Bytecode.LOAD_CONST.value, "add",
            Bytecode.MAKE_FUNCTION.value, 0,
            Bytecode.STORE_NAME.value, "add",
            Bytecode.LOAD_NAME.value, "add",
            Bytecode.RETURN_VALUE.value,
        ])
        compiler = Compiler()
        bytecode = compiler.compile(source_code)
        self.assertEqual(bytecode, expected_bytecode)

    def test_compile_while_loop(self):
        source_code = "while x < 10:\n    x += 1\nreturn x"
        expected_bytecode = bytes([
            Bytecode.SETUP_LOOP.value, 15,
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.LOAD_CONST.value, 10,
            Bytecode.COMPARE_OP.value, Operator.LT.value,
            Bytecode.POP_JUMP_IF_FALSE.value, 21,
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.LOAD_CONST.value, 1,
            Bytecode.INPLACE_ADD.value,
            Bytecode.STORE_NAME.value, "x",
            Bytecode.JUMP_ABSOLUTE.value, 7,
            Bytecode.POP_BLOCK.value,
            Bytecode.LOAD_NAME.value, "x",
            Bytecode.RETURN_VALUE.value,
        ])
        compiler = Compiler()
        bytecode = compiler.compile(source_code)
        self.assertEqual(bytecode, expected_bytecode)
```
2. Install the `unittest` module, if it's not already installed. You can do this by running `pip install unittest`.
3. Import the `Compiler` class and the `Bytecode` and `Operator` enumerations into the `test_compiler.py` file:
```
from kyky.compiler import Compiler, Bytecode, Operator
```
4. Add the following lines to the bottom of the `test_compiler.py` file:
```
if __name__ == '__main__':
    unittest.main()
```
5. Open a terminal or command prompt in the same directory as `test_compiler.py`.
6. Run the command `python test_compiler.py`. This should execute the test cases and output the results to the terminal or command prompt. If any of the test cases fail, you will see an error message indicating which assertion failed and what the expected and actual results were. Make sure that the `Compiler` class and the `Bytecode` and `Operator` enumerations are properly defined and imported before running the tests. If they are defined in a separate file or module, make sure that you import them correctly.