commit de0377397f6adecb9e3ebc20d0d0e3a8c458be1a Author: Nikolaj Gade Date: Mon Feb 19 08:29:15 2024 +0100 :tada: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c40d955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +**/.vscode/ +**/__pycache__/ +**/*.aux +**/*.fdb_latexmk +**/*.fls +**/*.log +**/*.out +**/*.synctex.gz +**/*.xdv \ No newline at end of file diff --git a/documentation/plthy.pdf b/documentation/plthy.pdf new file mode 100644 index 0000000..abb02c2 Binary files /dev/null and b/documentation/plthy.pdf differ diff --git a/documentation/plthy.tex b/documentation/plthy.tex new file mode 100644 index 0000000..05d9301 --- /dev/null +++ b/documentation/plthy.tex @@ -0,0 +1,35 @@ +\documentclass[a4paper]{paper} +\usepackage[margin=2.5cm]{geometry} + +\begin{document} + \begin{center} + \begin{tabular}{|lcl|} + \hline + \textit{program} & $\rightarrow$ & \texttt{hello} \texttt{|} \textit{statements} \texttt{goodbye} \texttt{|} \\ \hline \hline + \textit{statements} & $\rightarrow$ & \\ \hline + \textit{statements} & $\rightarrow$ & \textit{statement} \texttt{|} \textit{statements} \\ \hline\hline + \textit{statement} & $\rightarrow$ & \\ \hline + \textit{statement} & $\rightarrow$ & \texttt{maybe} \textit{statement}\\ \hline + \textit{statement} & $\rightarrow$ & \texttt{do} \textit{command}\\ \hline + \textit{statement} & $\rightarrow$ & \texttt{[} \textit{statements} \texttt{]}\\ \hline + \textit{statement} & $\rightarrow$ & \textit{statement} \texttt{if} \textit{expression} \\ \hline + \textit{statement} & $\rightarrow$ & \textit{statement} \texttt{because} \textit{expression} \\ \hline + \textit{statement} & $\rightarrow$ & \texttt{until} \textit{expression} \textit{statement} \\ \hline + \textit{statement} & $\rightarrow$ & \textit{expression} \texttt{->} \textbf{id} \\ \hline + \textit{statement} & $\rightarrow$ & \texttt{define} \textbf{function} \texttt{<} \textbf{numeral} \texttt{>} \texttt{as} \textit{statement} \\ \hline + \textit{statement} & $\rightarrow$ & \texttt{return} \textit{expression} \\ \hline\hline + \textit{command} & $\rightarrow$ & \textbf{builtin} \texttt{<} \textit{expressions} \texttt{>} \\ \hline + \textit{command} & $\rightarrow$ & \texttt{"} \textbf{function} \texttt{" <} \textit{expressions} \texttt{>} \\ \hline\hline + \textit{expressions} & $\rightarrow$ & \\ \hline + \textit{expressions} & $\rightarrow$ & \textit{expression} \texttt{;} \textit{expressions} \\ \hline\hline + \textit{expression} & $\rightarrow$ & \textbf{string} \\ \hline + \textit{expression} & $\rightarrow$ & \textbf{numeral} \\ \hline + \textit{expression} & $\rightarrow$ & \textbf{boolean} \\ \hline + \textit{expression} & $\rightarrow$ & \texttt{\{} \textit{expressions} \texttt{\}} \\ \hline + \textit{expression} & $\rightarrow$ & \texttt{(} \textit{expression} \texttt{)} \\ \hline + \textit{expression} & $\rightarrow$ & \textit{expression} \textbf{binop} \textit{expression} \\ \hline + \textit{expression} & $\rightarrow$ & \texttt{variable} \textbf{id}\\ \hline + \textit{expression} & $\rightarrow$ & \textit{statement} \\ \hline + \end{tabular} + \end{center} +\end{document} \ No newline at end of file diff --git a/plthy b/plthy new file mode 100755 index 0000000..d036cea --- /dev/null +++ b/plthy @@ -0,0 +1,43 @@ +#! /home/nikolaj/.pyenv/shims/python +""" +Usage: + plthy (-h| --help) + plthy (-i| -c) FILE + +Options: + -h --help Print this help screen + -i Run the interpreter + -c Run the compiler + FILE The file to compile/interpret +""" +from docopt import docopt + +from plthy_impl.lexer import Lexer +from plthy_impl.parser import Parser +from plthy_impl.ast_nodes import Program + +def main(): + args = docopt(__doc__) + file_path = args["FILE"] + with open(file_path, "r", encoding="utf-8") as file_pointer: + program_text = file_pointer.read() + "\n" + + lexer = Lexer().get_lexer() + parser = Parser() + + tokens = lexer.lex(program_text) + # for t in tokens: + # print(t) + + program = parser.parse(tokens) + + if isinstance(program, Program): + if args["-i"]: + program.eval() + else: + raise Exception("Compiler not implemented") + else: + raise Exception("Output not of type 'Program'", type(program)) + +if __name__ == "__main__": + main() diff --git a/plthy_impl/ast_nodes.py b/plthy_impl/ast_nodes.py new file mode 100644 index 0000000..acc2a45 --- /dev/null +++ b/plthy_impl/ast_nodes.py @@ -0,0 +1,224 @@ +import random + +from rply.token import BaseBox + +def rep_join(l): + format_string = ',\n'.join( + [repr(i) if not isinstance(i, str) else i for i in l] + ).replace('\n', '\n ') + + if format_string != "": + format_string = f"\n {format_string}\n" + + return format_string + +class Exp(BaseBox): + def eval(self, vtable, ftable): + return vtable, ftable, None + +class ExpNumeral(Exp): + def __init__(self, value: int): + self.value = value + + def eval(self, vtable, ftable): + return vtable, ftable, self.value + +class ExpString(Exp): + def __init__(self, value: str): + self.value = value + + def eval(self, vtable, ftable): + return vtable, ftable, self.value + +class ExpVariable(Exp): + def __init__(self, variable_id: str): + self.variable_name = variable_id + + def eval(self, vtable, ftable): + return vtable, ftable, vtable[self.variable_name] + +class ExpABinop(Exp): + def __init__(self,op: str,exp1: Exp, exp2: Exp): + self.op = op + self.exp1 = exp1 + self.exp2 = exp2 + + def eval(self, vtable, ftable): + if self.op == "+": + vtable, ftable, r1 = self.exp1.eval(vtable, ftable) + vtable, ftable, r2 = self.exp2.eval(vtable, ftable) + return vtable, ftable, r1+r2 + elif self.op == "-": + vtable, ftable, r1 = self.exp1.eval(vtable, ftable) + vtable, ftable, r2 = self.exp2.eval(vtable, ftable) + return vtable, ftable, r1-r2 + elif self.op == "*": + vtable, ftable, r1 = self.exp1.eval(vtable, ftable) + vtable, ftable, r2 = self.exp2.eval(vtable, ftable) + return vtable, ftable, r1*r2 + elif self.op == "/": + vtable, ftable, r1 = self.exp1.eval(vtable, ftable) + vtable, ftable, r2 = self.exp2.eval(vtable, ftable) + return vtable, ftable, r1/r2 + elif self.op == "=": + vtable, ftable, r1 = self.exp1.eval(vtable, ftable) + vtable, ftable, r2 = self.exp2.eval(vtable, ftable) + return vtable, ftable, r1 == r2 + else: + raise Exception(f"Binop {self.op} not implemented") + +class ExpArg(Exp): + def __init__(self, arg: int): + self.arg = arg + + def eval(self, vtable, ftable): + n = vtable["#ARGS"] - self.arg + + return vtable, ftable, vtable[f"#{n}"] + +class Command(BaseBox): + def eval(self, vtable, ftable): + return vtable, ftable, None + +class Builtin(Command): + def __init__(self, builtin: str, args: list[Exp]): + self.builtin = builtin + self.args = args + + def eval(self, vtable, ftable): + if self.builtin == "print": + prints = [] + for arg in self.args: + vtable, ftable, result = arg.eval(vtable, ftable) + prints.append(str(result)) + + print(" ".join(prints)) + + return vtable, ftable, None + else: + raise Exception(f"Unknown builtin {self.builtin}") + +class Do(BaseBox): + def __init__(self, command: Command): + self.command = command + + def eval(self, vtable, ftable): + return self.command.eval(vtable, ftable) + +class Statement(BaseBox): + def eval(self, vtable, ftable): + return vtable, ftable, None + +class StatementSet(Statement): + def __init__(self, expression: Exp, variable: str): + self.expression = expression + self.variable_name = variable + + def eval(self, vtable, ftable): + vtable, ftable, result = self.expression.eval( + vtable, ftable + ) + vtable[self.variable_name] = result + return vtable, ftable, None + +class StatementDefine(Statement): + def __init__(self, function_name : str, parameters: int, statement: Statement): + self.function_name = function_name + self.statement = statement + self.parameters = parameters + + def eval(self, vtable, ftable): + ftable[self.function_name] = (self.parameters, self.statement) + return vtable, ftable, None + +class StatementReturn(Statement): + def __init__(self, expression: Exp): + self.value = expression + + def eval(self, vtable, ftable): + vtable, ftable, result = self.value.eval(vtable, ftable) + vtable["#return"] = result + return vtable, ftable, result + +class Scope(Statement): + def __init__(self, statements: list[Statement]): + self.statements = statements + + def eval(self, vtable, ftable): + result = None + for statement in self.statements: + vtable, ftable, _ = statement.eval(vtable, ftable) + if "#return" in vtable: + result = vtable["#return"] + del vtable["#return"] + break + + return vtable, ftable, result + +class Call(Statement): + def __init__(self, function_name: str, arguments: list[Exp]): + self.function_name = function_name + self.arguments = arguments + + def eval(self, vtable, ftable): + assert len(self.arguments) == ftable[self.function_name][0] + + args = [] + for argument in self.arguments: + vtable, ftable, result = argument.eval(vtable,ftable) + args.append(result) + + n = vtable["#ARGS"]-1 + + for i, arg in enumerate(args): + vtable[f"#{n+len(args)-i}"] = arg + + vtable["#ARGS"] += len(args) + + vtable, ftable, result = ftable[self.function_name][1].eval(vtable, ftable) + + if "#return" in vtable: + del vtable["#return"] + + vtable["#ARGS"] -= len(args) + + return vtable, ftable, result + +class StatementIf(Statement): + def __init__(self, statement: Statement, condition): + self.statement = statement + self.condition = condition + + def eval(self, vtable, ftable): + vtable, ftable, result = self.condition.eval(vtable, ftable) + if result: + return self.statement.eval(vtable, ftable) + else: + return vtable, ftable, None + +class Maybe(BaseBox): + def __init__(self, statement : Statement): + self.statement = statement + + def eval(self, vtable, ftable): + if random.randint(0,1) == 1: + return self.statement.eval(vtable,ftable) + else: + return vtable, ftable, None + +class Program(BaseBox): + def __init__(self, statements: list[Statement]) -> None: + self.statements = statements + + def __repr__(self) -> str: + statements_string = f"statements([{rep_join(self.statements)}])" + return statements_string + + def eval(self, *_): + vtable = {"#ARGS": 0} + ftable = {} + + for statement in self.statements: + vtable, ftable, _ = statement.eval(vtable, ftable) + if "#return" in vtable: + break diff --git a/plthy_impl/lexer.py b/plthy_impl/lexer.py new file mode 100644 index 0000000..e68666e --- /dev/null +++ b/plthy_impl/lexer.py @@ -0,0 +1,74 @@ +from string import ascii_letters, digits +from rply import LexerGenerator + +VALID_CHARACTERS = ascii_letters+"_"+digits + +KEYWORD_TOKENS = [("KEYWORD_"+i.upper(), i) for i in [ + "hello", + "goodbye", + "maybe", + "do", + "if", + "because", + "until", + "define", + "as", + "variable", + "return", + "argument" +]] + +BUILTIN_TOKENS = [("BUILTIN", i) for i in [ + "print" +]] + +DATA_TOKENS = [ + ("DATA_STRING", r"\'.*?\'"), + ("DATA_NUMERAL", r"\d+(\.\d+)?") +] + +SYMBOL_TOKENS = [ + ("SYMBOL_SET", r"\-\>"), + ("SYMBOL_LPARENS", r"\("), + ("SYMBOL_RPARENS", r"\)"), + ("SYMBOL_LBRACKET", r"\["), + ("SYMBOL_RBRACKET", r"\]"), + ("SYMBOL_LCURL", r"\{"), + ("SYMBOL_RCURL", r"\}"), + ("SYMBOL_PLUS", r"\+"), + ("SYMBOL_MINUS", r"\-"), + ("SYMBOL_TIMES", r"\*"), + ("SYMBOL_DIVIDE", r"\/"), + ("SYMBOL_COMMA", r"\,"), + ("SYMBOL_COLON", r"\:"), + ("SYMBOL_SEMICOLON", r"\;"), + ("SYMBOL_PIPE", r"\|"), + ("SYMBOL_QUOTE", r"\""), + ("SYMBOL_LT", r"\<"), + ("SYMBOL_GT", r"\>"), + ("SYMBOL_EQUALS", r"\="), + ("SYMBOL_DOLLAR", r"\$") +] + +ALL_TOKENS = ( + KEYWORD_TOKENS + + BUILTIN_TOKENS + + DATA_TOKENS + + SYMBOL_TOKENS + + [("ARG", r"\#\d+")] + + [("ID", f"[{VALID_CHARACTERS}]+")] +) + + +class Lexer(): + def __init__(self): + self.lexer = LexerGenerator() + + def _add_tokens(self): + for token in ALL_TOKENS: + self.lexer.add(*token) + self.lexer.ignore(r"[\s\n]+|//.*\n") + + def get_lexer(self): + self._add_tokens() + return self.lexer.build() diff --git a/plthy_impl/parser.py b/plthy_impl/parser.py new file mode 100644 index 0000000..908c6db --- /dev/null +++ b/plthy_impl/parser.py @@ -0,0 +1,118 @@ +from rply import ParserGenerator + +from plthy_impl.lexer import ALL_TOKENS +from plthy_impl import ast_nodes + +class Parser(): + def __init__(self): + self.pg = ParserGenerator( + [i[0] for i in ALL_TOKENS], + precedence=[ + ('left', ["KEYWORD_MAYBE", "KEYWORD_RETURN"]), + ('left', ["KEYWORD_IF", "KEYWORD_DEFINE", "KEYWORD_AS"]), + ('left', ["KEYWORD_DO", "BUILTIN"]), + ('left', ["SYMBOL_EQUALS", "SYMBOL_SET"]), + ('left', ["SYMBOL_PLUS", "SYMBOL_MINUS"]), + ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]) + ] + ) + + def parse(self, token_input) -> ast_nodes.BaseBox: + # Top-level program stuff + @self.pg.production('program : KEYWORD_HELLO SYMBOL_PIPE statements KEYWORD_GOODBYE SYMBOL_PIPE') + def program(tokens): + return ast_nodes.Program(tokens[2]) + + ## statements ## + @self.pg.production('statements : ') + def statements_none(_): + return [] + + @self.pg.production('statements : statement SYMBOL_PIPE statements') + def statements(tokens): + return [tokens[0]] + tokens[2] + + ## statement ## + @self.pg.production('statement : SYMBOL_DOLLAR expression SYMBOL_SET ID', precedence="SYMBOL_SET") + def statement_set(tokens): + return ast_nodes.StatementSet(tokens[1], tokens[3].value) + + @self.pg.production('statement : KEYWORD_DO command') + def statement_do(tokens): + return ast_nodes.Do(tokens[1]) + + @self.pg.production('statement : KEYWORD_MAYBE statement') + def statement_maybe(tokens): + return ast_nodes.Maybe(tokens[1]) + + @self.pg.production('statement : statement KEYWORD_IF expression') + def statement_if(tokens): + return ast_nodes.StatementIf(tokens[0], tokens[2]) + + @self.pg.production('statement : KEYWORD_DEFINE ID SYMBOL_LT DATA_NUMERAL SYMBOL_GT KEYWORD_AS statement', precedence="KEYWORD_DEFINE") + def statement_define(tokens): + return ast_nodes.StatementDefine(tokens[1].value, int(tokens[3].value), tokens[6]) + + @self.pg.production('statement : KEYWORD_RETURN expression') + def statement_return(tokens): + return ast_nodes.StatementReturn(tokens[1]) + + @self.pg.production('statement : SYMBOL_LBRACKET statements SYMBOL_RBRACKET') + def statement_scope(tokens): + return ast_nodes.Scope(tokens[1]) + + ## command ## + @self.pg.production('command : BUILTIN SYMBOL_LT expressions SYMBOL_GT') + def command_builtin(tokens): + return ast_nodes.Builtin(tokens[0].value, tokens[2]) + + @self.pg.production('command : SYMBOL_QUOTE ID SYMBOL_QUOTE SYMBOL_LT expressions SYMBOL_GT') + def command_call(tokens): + return ast_nodes.Call(tokens[1].value,tokens[4]) + + ## expressions ## + @self.pg.production('expressions : ') + def expressions_none(_): + return [] + + @self.pg.production('expressions : expression SYMBOL_SEMICOLON expressions ') + def expressions(tokens): + return [tokens[0]] + tokens[2] + + ## expression ## + @self.pg.production('expression : DATA_NUMERAL') + def exp_numeral(tokens): + return ast_nodes.ExpNumeral(float(tokens[0].value)) + + @self.pg.production('expression : DATA_STRING') + def exp_string(tokens): + return ast_nodes.ExpString(tokens[0].value[1:-1]) + + @self.pg.production('expression : statement', precedence="KEYWORD_IF") + def exp_statement(tokens): + return(tokens[0]) + + @self.pg.production('expression : KEYWORD_VARIABLE ID') + def exp_variable(tokens): + return ast_nodes.ExpVariable(tokens[1].value) + + @self.pg.production('expression : expression SYMBOL_PLUS expression') + @self.pg.production('expression : expression SYMBOL_MINUS expression') + @self.pg.production('expression : expression SYMBOL_TIMES expression') + @self.pg.production('expression : expression SYMBOL_DIVIDE expression') + @self.pg.production('expression : expression SYMBOL_EQUALS expression') + def exp_a_binop(tokens): + return ast_nodes.ExpABinop(tokens[1].value,tokens[0],tokens[2]) + + @self.pg.production('expression : KEYWORD_ARGUMENT ARG') + def exp_arg(tokens): + return ast_nodes.ExpArg(int(tokens[1].value[1:])) + + ## Error Handling ## + @self.pg.error + def error_handle(token): + raise Exception(token.name, token.value, token.source_pos) + + ## Finish ## + parser = self.pg.build() + return parser.parse(token_input) diff --git a/tests/fib.plthy b/tests/fib.plthy new file mode 100644 index 0000000..3fd522a --- /dev/null +++ b/tests/fib.plthy @@ -0,0 +1,12 @@ +hello| +define fib<1> as [ + return 1 if argument #1 = 1| + return 1 if argument #1 = 2| + return do "fib" + do "fib"| +]| +do print;>| +do print;>| +do print;>| +do print;>| +do print;>| +goodbye| \ No newline at end of file diff --git a/tests/function.plthy b/tests/function.plthy new file mode 100644 index 0000000..405d73e --- /dev/null +++ b/tests/function.plthy @@ -0,0 +1,4 @@ +hello| +define five<0> as return 5| +do print;>| +goodbye| \ No newline at end of file diff --git a/tests/if.plthy b/tests/if.plthy new file mode 100644 index 0000000..b826fe4 --- /dev/null +++ b/tests/if.plthy @@ -0,0 +1,5 @@ +hello| +$2 -> x| +do print<'a';> if variable x = 1| +do print<'b';> if variable x = 2| +goodbye| \ No newline at end of file diff --git a/tests/math.plthy b/tests/math.plthy new file mode 100644 index 0000000..2e1cd5a --- /dev/null +++ b/tests/math.plthy @@ -0,0 +1,10 @@ +hello| +$1 -> x| +$2 -> y| +$5 -> z| +$variable x+variable y -> z| // 3 +$5-variable z -> z| // 2 +$2*variable z -> x| // 4 +$variable x/variable y -> y| // 2 +do print| +goodbye| \ No newline at end of file diff --git a/tests/maybe.plthy b/tests/maybe.plthy new file mode 100644 index 0000000..ab547bd --- /dev/null +++ b/tests/maybe.plthy @@ -0,0 +1,5 @@ +hello| +$1 -> x| +maybe $variable x + 1 -> x| +do print| +goodbye| \ No newline at end of file diff --git a/tests/none.plthy b/tests/none.plthy new file mode 100644 index 0000000..f749478 --- /dev/null +++ b/tests/none.plthy @@ -0,0 +1,2 @@ +hello| +goodbye| \ No newline at end of file diff --git a/tests/precedence.plthy b/tests/precedence.plthy new file mode 100644 index 0000000..34c6ade --- /dev/null +++ b/tests/precedence.plthy @@ -0,0 +1,6 @@ +hello| +$ 1 + 2 * 3 -> x| +do print| +$ 5 -> y if variable x = 7| +do print| +goodbye| \ No newline at end of file diff --git a/tests/scope.plthy b/tests/scope.plthy new file mode 100644 index 0000000..a17f1a2 --- /dev/null +++ b/tests/scope.plthy @@ -0,0 +1,6 @@ +hello| +[ + $5 -> x| + do print| +]| +goodbye| \ No newline at end of file diff --git a/tests/variable.plthy b/tests/variable.plthy new file mode 100644 index 0000000..b4e1676 --- /dev/null +++ b/tests/variable.plthy @@ -0,0 +1,4 @@ +hello| +$2 -> x| +do print| +goodbye| \ No newline at end of file