#! /usr/bin/env python3
import sys
from typing import Iterator

DEBUG = False

class StackUnderflowError(BaseException):
    pass

# data stack
stack = []
word_gen = None

def push(i):
    """Push a given int onto the stack

    Args:
        i (int): the number to push onto the stack

    Returns:
        str: "ok" if successful
    """
    if DEBUG:
        print(f"Pushed {i} to the stack")
    stack.append(i)

# BUILT-IN WORDS
def pop():
    """Pops a number from the stack
    """
    try:
        return stack.pop()
    except IndexError:
        raise StackUnderflowError

def print_stack():
    """Prints the entire stack
    """
    print(stack)

def plus():
    """Adds the top two numbers on the stack and pushes the result to the stack
    """
    try:
        a = stack.pop() + stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def minus():
    """Pops the top two numbers from the stack and subtracts the second from the first and pushes the result back to the stack
    """
    try:
        a = stack.pop() - stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def times():
    """Multiplies the top two numbers on the stack and pushes the result to the stack
    """
    try:
        a = stack.pop() * stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def divide():
    """Pops the top two numbers from the stack, divides the first by the second and pushes the result back to the stack
    """
    try:
        a = stack.pop() // stack.pop()
        stack.append(a)
    except IndexError:
        raise "Stack Underflow!"

def exit_forth():
    """Print "Goodbye!" and exit forth with code 0
    """
    print("Goodbye!")
    exit(0)

def comment():
    global word_gen

    while True:
        word = next(word_gen)

        # End of file while comment
        if word is None:
            raise EOFError
        
        if word in word_dict:
            if word_dict[word][0] == 'end_comment':
                break

def compile():
    global word_gen
    
    # get the word that is to be defined
    new_word = next(word_gen)
    
    # If it is None then EOF
    if new_word is None:
        raise EOFError

    # create new entry
    word_dict[new_word] = []

    while True:
        # get the next word
        word = next(word_gen)

        # EOF case
        if word is None:
            # EOF while parsing
            raise EOFError

        if word in word_dict:
            funcs = word_dict[word]
            # TODO add functionality for definitions inside definitions
            if compile in funcs:
                print("Cannot define words that use compile word")
                exit(1)

            if funcs[0] == 'end_compile':
                break
            
            word_dict[word].append(funcs)
            
    return


word_dict = {
    ".": [pop],
    ".s": [print_stack],
    "exitforth": [exit_forth],
    "+": [plus],
    "-": [minus],
    "*": [times],
    "/": [divide],
    ":": [compile],
    ";": ["end_compile"],
    "(": [comment],
    ")": ['end_comment']
}
                   
def get_next_word_interactive(buff: list) -> str:
    """Gets the next word from user input

    Args:
        buff (list): a buffer of words that has already been read in

    Returns:
        str: the next word
    """
    if len(buff) == 0:
        line = input(">>> ")
        buff.append(line.split())
    
    return buff.pop(0)

def get_next_word(file_name: str) -> Iterator[str]:
    """Iterator function that yields the next word in the given forth file

    Args:
        file_name (str): the file to yield words from 

    Yields:
        Iterator[str]: the next word read from `file_name` or None if EOF is reached
    """
    with open(file_name, 'r') as f:
        for row in f:
            words = row.split()
            for word in words:
                yield word.strip()
    yield None

def digit(i):
    try:
        i = int(i)
        return i
    except ValueError:
        return None

def run(file_name):
    global word_gen

    # Create generator for next word
    if file_name is None:
        word_gen = get_next_word_interactive()
    else:
        word_gen = get_next_word(file_name)
    
    # Loop until EOF is reached
    while True:
        # get the next word
        word = next(word_gen)

        # EOF case
        if word is None:
            break
            
        # If the word is in the word dict, then perform its function(s)
        if word in word_dict:
            funcs = word_dict[word]
            for func in funcs:
                # Try for StackUnderflowError
                try:
                    func()
                except StackUnderflowError:
                    print("Stack Underflow!")
                    exit(1)
        else:
            # if the word is an int, push to stack
            i = digit(word)
            if i is not None:
                push(i)
                print("ok")
            else:
                print("Not a word")

def main():
    args = sys.argv
    for arg in args:
        arg = arg.strip()
        if arg == "-d":
            DEBUG = True
    
    #interactive()
    run("test.forth")


if __name__ == "__main__":
    main()
#! /usr/bin/env python3
import sys
from typing import Iterator

DEBUG = False

class StackUnderflowError(BaseException):
    pass

# data stack
stack = []
word_gen = None

def push(i):
    """Push a given int onto the stack

    Args:
        i (int): the number to push onto the stack

    Returns:
        str: "ok" if successful
    """
    if DEBUG:
        print(f"Pushed {i} to the stack")
    stack.append(i)

# BUILT-IN WORDS
def pop():
    """Pops a number from the stack
    """
    try:
        return stack.pop()
    except IndexError:
        raise StackUnderflowError

def print_stack():
    """Prints the entire stack
    """
    print(stack)

def plus():
    """Adds the top two numbers on the stack and pushes the result to the stack
    """
    try:
        a = stack.pop() + stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def minus():
    """Pops the top two numbers from the stack and subtracts the second from the first and pushes the result back to the stack
    """
    try:
        a = stack.pop() - stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def times():
    """Multiplies the top two numbers on the stack and pushes the result to the stack
    """
    try:
        a = stack.pop() * stack.pop()
        stack.append(a)
    except IndexError:
        raise StackUnderflowError

def divide():
    """Pops the top two numbers from the stack, divides the first by the second and pushes the result back to the stack
    """
    try:
        a = stack.pop() // stack.pop()
        stack.append(a)
    except IndexError:
        raise "Stack Underflow!"

def exit_forth():
    """Print "Goodbye!" and exit forth with code 0
    """
    print("Goodbye!")
    exit(0)

def comment():
    global word_gen

    while True:
        word = next(word_gen)

        # End of file while comment
        if word is None:
            raise EOFError
        
        if word in word_dict:
            if word_dict[word][0] == 'end_comment':
                break

def compile():
    global word_gen
    
    # get the word that is to be defined
    new_word = next(word_gen)
    
    # If it is None then EOF
    if new_word is None:
        raise EOFError

    # create new entry
    word_dict[new_word] = []

    while True:
        # get the next word
        word = next(word_gen)

        # EOF case
        if word is None:
            # EOF while parsing
            raise EOFError

        if word in word_dict:
            funcs = word_dict[word]
            # TODO add functionality for definitions inside definitions
            if compile in funcs:
                print("Cannot define words that use compile word")
                exit(1)

            if funcs[0] == 'end_compile':
                break
            
            word_dict[word].append(funcs)
            
    return


word_dict = {
    ".": [pop],
    ".s": [print_stack],
    "exitforth": [exit_forth],
    "+": [plus],
    "-": [minus],
    "*": [times],
    "/": [divide],
    ":": [compile],
    ";": ["end_compile"],
    "(": [comment],
    ")": ['end_comment']
}
                   
def get_next_word_interactive(buff: list) -> str:
    """Gets the next word from user input

    Args:
        buff (list): a buffer of words that has already been read in

    Returns:
        str: the next word
    """
    if len(buff) == 0:
        line = input(">>> ")
        buff.append(line.split())
    
    return buff.pop(0)

def get_next_word(file_name: str) -> Iterator[str]:
    """Iterator function that yields the next word in the given forth file

    Args:
        file_name (str): the file to yield words from 

    Yields:
        Iterator[str]: the next word read from `file_name` or None if EOF is reached
    """
    with open(file_name, 'r') as f:
        for row in f:
            words = row.split()
            for word in words:
                yield word.strip()
    yield None

def digit(i):
    try:
        i = int(i)
        return i
    except ValueError:
        return None

def run(file_name):
    global word_gen

    # Create generator for next word
    if file_name is None:
        word_gen = get_next_word_interactive()
    else:
        word_gen = get_next_word(file_name)
    
    # Loop until EOF is reached
    while True:
        # get the next word
        word = next(word_gen)

        # EOF case
        if word is None:
            break
            
        # If the word is in the word dict, then perform its function(s)
        if word in word_dict:
            funcs = word_dict[word]
            for func in funcs:
                # Try for StackUnderflowError
                try:
                    func()
                except StackUnderflowError:
                    print("Stack Underflow!")
                    exit(1)
        else:
            # if the word is an int, push to stack
            i = digit(word)
            if i is not None:
                push(i)
                print("ok")
            else:
                print("Not a word")

def main():
    args = sys.argv
    for arg in args:
        arg = arg.strip()
        if arg == "-d":
            DEBUG = True
    
    #interactive()
    run("test.forth")


if __name__ == "__main__":
    main()
#! /usr/bin/env python3 # data stack Push a given int onto the stack      Args:         i (int): the number to push onto the stack      Returns:         str: "ok" if successful # BUILT-IN WORDS Pops a number from the stack Prints the entire stack Adds the top two numbers on the stack and pushes the result to the stack Pops the top two numbers from the stack and subtracts the second from the first and pushes the result back to the stack Multiplies the top two numbers on the stack and pushes the result to the stack Pops the top two numbers from the stack, divides the first by the second and pushes the result back to the stack Print "Goodbye!" and exit forth with code 0 # End of file while comment # get the word that is to be defined # If it is None then EOF # create new entry # get the next word # EOF case # EOF while parsing # TODO add functionality for definitions inside definitions Gets the next word from user input      Args:         buff (list): a buffer of words that has already been read in      Returns:         str: the next word Iterator function that yields the next word in the given forth file      Args:         file_name (str): the file to yield words from       Yields:         Iterator[str]: the next word read from `file_name` or None if EOF is reached # Create generator for next word # Loop until EOF is reached # get the next word # EOF case # If the word is in the word dict, then perform its function(s) # Try for StackUnderflowError # if the word is an int, push to stack #interactive()