diff --git a/cent b/cent new file mode 100755 index 0000000..f96e715 --- /dev/null +++ b/cent @@ -0,0 +1,39 @@ +#! /home/nikolaj/.pyenv/shims/python +""" +Usage: + cent (-h|--help) + cent -i FILE + cent -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 centvrion.lexer import Lexer +from centvrion.parser import Parser +from centvrion.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) + + program = parser.parse(tokens) + + if isinstance(program, Program): + program.eval() + else: + raise Exception("Output not of type 'Program'", type(program)) + +if __name__ == "__main__": + main() diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 821f3bd..a429361 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -1,17 +1,23 @@ +import re import random -import roman -from rply.token import BaseBox as BB +from rply.token import BaseBox -NUMERALS = [ - ("I", 1), - ("V", 5), - ("X", 10), - ("L", 50), - ("C", 100), - ("D", 500), - ("M", 1000) -] +NUMERALS = { + "I": 1, + "IV": 4, + "V": 5, + "IX": 9, + "X": 10, + "XL": 40, + "L": 50, + "XC": 90, + "C": 100, + "CD": 400, + "D": 500, + "CM": 900, + "M": 1000 +} def rep_join(l): format_string = ',\n'.join( @@ -23,24 +29,85 @@ def rep_join(l): return format_string -# TODO: Magnum -def num_to_int(n): - return roman.fromRoman(n) +def single_num_to_int(i, m): + if i[-1] == "_": + if not m: + raise Exception( + "Cannot calculate numbers above 3999 without 'MAGNVM' module" + ) -def make_string(n): + if i[0] != "I": + raise Exception( + "Cannot use 'I' with thousands operator, use 'M' instead" + ) + + return 1000 * single_num_to_int(i[:-1], m) + else: + return NUMERALS[i] + +def num_to_int(n, m): + chars = re.findall(r"[IVXLCDM]_*", n) + nums = [single_num_to_int(i, m) for i in chars] + new_nums = nums.copy() + for x, num in enumerate(nums[:-3]): + if all(num == nums[x+i] for i in range(0,3)): + raise Exception(n, "is not a valid roman numeral") + + while True: + for x, num in enumerate(nums[:-1]): + if num < nums[x+1]: + if (not nums[x+1] % 5) and (num in [nums[x+1]/5, nums[x+1]/10]): + new_nums[x] = nums[x+1] - num + new_nums[x+1] = 0 + break + else: + raise Exception(n, "is not a valid roman numeral") + + if new_nums != nums: + nums = [i for i in new_nums if i != 0] + new_nums = nums + else: + break + + if nums != sorted(nums)[::-1]: + raise Exception(n, "is not a valid roman numeral") + + return sum(nums) + +def int_to_num(n, m): + if n > 3999: + if not m: + raise Exception( + "Cannot display numbers above 3999 without 'MAGNVM' module" + ) + thousands_chars = re.findall(r"[IVXLCDM]_*", int_to_num(n//1000, m)) + thousands = ''.join([ + "M" if i == "I" else i+"_" + for i in thousands_chars + ]) + + return thousands + int_to_num(n % 1000, m) + else: + nums = [] + while n > 0: + for num, i in list(NUMERALS.items())[::-1]: + if n >= i: + nums.append(num) + n -= i + break + + return ''.join(nums) + +def make_string(n, m): if isinstance(n, str): return n elif isinstance(n, int): - return roman.toRoman(n) + return int_to_num(n, m) elif isinstance(n, list): - return f"[{' '.join([make_string(i) for i in n])}]" + return f"[{' '.join([make_string(i, m) for i in n])}]" else: raise Exception(n) -class BaseBox(BB): - def eval(self, vtable, ftable, modules): - return None - class ExpressionStatement(BaseBox): def __init__(self, expression) -> None: self.expression = expression @@ -94,8 +161,8 @@ class Numeral(BaseBox): def __repr__(self): return f"Numeral({self.value})" - def eval(self, *_): - return num_to_int(self.value) + def eval(self, *args): + return num_to_int(self.value, "MAGNVM" in args[2]) class Bool(BaseBox): def __init__(self, value) -> None: @@ -354,15 +421,20 @@ class BuiltIn(BaseBox): match self.builtin: case "AVDI_NVMERVS": - return num_to_int(input()) + return num_to_int(input(), "MAGNVM" in modules) case "DICE": - print(' '.join(make_string(i) for i in parameters)) + print(' '.join( + make_string(i, "MAGNVM" in modules) for i in parameters) + ) return None case "ERVMPE": vtable["ERVMPE"] = True return None case "FORTIS_NVMERVS": - # TODO: Fors + if "FORS" not in modules: + raise Exception( + "Cannot use 'FORTIS_NVMERVS' without module 'FORS'" + ) return random.randint(parameters[0], parameters[1]) case _: raise Exception(self.builtin) diff --git a/centvrion/parser.py b/centvrion/parser.py index 0d8333a..90a26b4 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -17,20 +17,28 @@ class Parser(): ) def parse(self, tokens_input) -> ast_nodes.BaseBox: - @self.pg.production('program : opt_newline module_calls statements') + @self.pg.production('program : opt_newline opt_module_calls opt_newline opt_statements opt_newline') def program(tokens): - return ast_nodes.Program(tokens[-2], tokens[-1]) + return ast_nodes.Program(tokens[1], tokens[3]) @self.pg.production('opt_newline : ') @self.pg.production('opt_newline : NEWLINE') def opt_newline(_): return None - @self.pg.production('module_calls : ') + @self.pg.production('opt_module_calls : ') + @self.pg.production('opt_module_calls : module_calls') + def opt_module_calls(calls): + if len(calls) == 0: + return calls + else: + return calls[0] + + @self.pg.production('module_calls : module_call NEWLINE ') @self.pg.production('module_calls : module_call NEWLINE module_calls') def module_calls(calls): - if len(calls) == 0: - return [] + if len(calls) == 2: + return [calls[0]] else: return [calls[0]] + calls[2] @@ -38,11 +46,20 @@ class Parser(): def module_call(tokens): return ast_nodes.ModuleCall(tokens[1].value) - @self.pg.production('statements : ') + + @self.pg.production('opt_statements : ') + @self.pg.production('opt_statements : statements') + def opt_statements(calls): + if len(calls) == 0: + return calls + else: + return calls[0] + + @self.pg.production('statements : statement NEWLINE ') @self.pg.production('statements : statement NEWLINE statements') def statements(calls): - if len(calls) == 0: - return [] + if len(calls) == 2: + return [calls[0]] else: return [calls[0]] + calls[2] @@ -74,7 +91,7 @@ class Parser(): def expression_id(tokens): return tokens[0] - @self.pg.production('statement : KEYWORD_DEFINI id ids KEYWORD_VT SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL') + @self.pg.production('statement : KEYWORD_DEFINI id ids KEYWORD_VT SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL') def defini(tokens): return ast_nodes.Defini(tokens[1], tokens[2], tokens[6]) @@ -135,19 +152,19 @@ class Parser(): def erumpe(_): return ast_nodes.Erumpe() - @self.pg.production('si_statement : KEYWORD_SI expression KEYWORD_TVNC SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL opt_newline aluid_statement') + @self.pg.production('si_statement : KEYWORD_SI expression KEYWORD_TVNC SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL opt_newline aluid_statement') def si(tokens): return ast_nodes.SiStatement(tokens[1], tokens[5], tokens[9]) - @self.pg.production('dum_statement : KEYWORD_DVM expression KEYWORD_FACE SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL') + @self.pg.production('dum_statement : KEYWORD_DVM expression KEYWORD_FACE SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL') def dum(tokens): return ast_nodes.DumStatement(tokens[1], tokens[5]) - @self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FACE SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL') + @self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FACE SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL') def per(tokens): return ast_nodes.PerStatement(tokens[3], tokens[1], tokens[7]) - @self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_FACE SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL') + @self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_FACE SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL') def donicum(tokens): range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5]) return ast_nodes.PerStatement(range_array, tokens[1], tokens[9]) @@ -160,7 +177,7 @@ class Parser(): def aluid_si(tokens): return [tokens[1]] - @self.pg.production('aluid_statement : KEYWORD_ALVID SYMBOL_LCURL opt_newline statements opt_newline SYMBOL_RCURL aluid_statement') + @self.pg.production('aluid_statement : KEYWORD_ALVID SYMBOL_LCURL opt_newline opt_statements opt_newline SYMBOL_RCURL aluid_statement') def aluid(tokens): return tokens[3] @@ -176,9 +193,9 @@ class Parser(): def range_array(tokens): return ast_nodes.DataRangeArray(tokens[1], tokens[3]) - @self.pg.error - def error_handle(token): - raise ValueError(token) + # @self.pg.error + # def error_handle(token): + # raise ValueError(token) parser = self.pg.build() return parser.parse(tokens_input) diff --git a/main.py b/main.py deleted file mode 100644 index 6c102d0..0000000 --- a/main.py +++ /dev/null @@ -1,37 +0,0 @@ -from centvrion.lexer import Lexer -from centvrion.parser import Parser -from centvrion.ast_nodes import Program - -text_input = """ -CVM FORS - -DESIGNA correct VT FORTIS_NVMERVS I CXXVIII -DESIGNA gvess VT NVLLVS - -DVM FALSITAS FACE { - DESIGNA gvess VT AVDI_NVMERVS - SI gvess MINVS correct TVNC { - DICE "Too low!" - } ALVID SI gvess PLVS correct TVNC { - DICE "Too high!" - } ALVID { - ERVMPE - } -} - -DICE "You guessed correctly!" -""" - -lexer = Lexer().get_lexer() -parser = Parser() - -tokens = lexer.lex(text_input) -#for token in tokens: -# print(token) - -program = parser.parse(tokens) -#print(x) -if isinstance(program, Program): - program.eval() -else: - raise Exception("Output not of type 'Program'", type(program)) diff --git a/tests/fib.cent b/tests/fib.cent new file mode 100644 index 0000000..a31cf0b --- /dev/null +++ b/tests/fib.cent @@ -0,0 +1,11 @@ +DEFINI fib x VT { + SI x EST NVLLVS TVNC { + REDI NVLLVS + } ALVID SI x EST I TVNC { + REDI I + } ALVID { + REDI ((INVOCA fib (x-II)) + (INVOCA fib (x-I))) + } +} + +DICE INVOCA fib (AVDI_NVMERVS) \ No newline at end of file