diff --git a/README.md b/README.md index 8033b96..af12d91 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,24 @@ condition. Exit the loop with `ERVMPE` (or `REDI` from inside a function). > V ``` +## Error handling + +Errors can be caught using `TEMPTA` (temptare = to try) and `CAPE` (capere = to catch). The `CAPE` block binds the error message to a variable as a string. + +``` +TEMPTA { + DESIGNA x VT I / NVLLVS +} CAPE error { + DICE(error) +} +``` + +``` +> Division by zero +``` + +If the try block succeeds, the catch block is skipped. If an error occurs in the catch block, it propagates up. `TEMPTA`/`CAPE` blocks can be nested. + ## Functions Functions are defined with the `DEFINI` and `VT` keywords. The `REDI` keyword is used to return. `REDI` can also be used to end the program, if used outside of a function. diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 85b59a6..99fc79e 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -1045,6 +1045,45 @@ class PerStatement(Node): return vtable, last_val +class TemptaStatement(Node): + def __init__(self, try_statements, error_var, catch_statements) -> None: + self.try_statements = try_statements + self.error_var = error_var + self.catch_statements = catch_statements + + def __eq__(self, other): + return (type(self) == type(other) + and self.try_statements == other.try_statements + and self.error_var == other.error_var + and self.catch_statements == other.catch_statements) + + def __repr__(self) -> str: + try_stmts = f"try([{rep_join(self.try_statements)}])" + catch_stmts = f"catch([{rep_join(self.catch_statements)}])" + tempta_string = rep_join([try_stmts, repr(self.error_var), catch_stmts]) + return f"Tempta({tempta_string})" + + def print(self): + try_body = "\n".join(s.print() for s in self.try_statements) + catch_body = "\n".join(s.print() for s in self.catch_statements) + return f"TEMPTA {{\n{try_body}\n}} CAPE {self.error_var.print()} {{\n{catch_body}\n}}" + + def _eval(self, vtable): + last_val = ValNul() + try: + for statement in self.try_statements: + vtable, last_val = statement.eval(vtable) + if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]: + return vtable, last_val + except CentvrionError as e: + vtable[self.error_var.name] = ValStr(str(e)) + for statement in self.catch_statements: + vtable, last_val = statement.eval(vtable) + if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]: + return vtable, last_val + return vtable, last_val + + class Invoca(Node): def __init__(self, callee, parameters) -> None: self.callee = callee diff --git a/centvrion/compiler/emit_stmt.py b/centvrion/compiler/emit_stmt.py index 9f71a45..340b45d 100644 --- a/centvrion/compiler/emit_stmt.py +++ b/centvrion/compiler/emit_stmt.py @@ -1,6 +1,7 @@ from centvrion.ast_nodes import ( Designa, DesignaIndex, DesignaDestructure, SiStatement, DumStatement, - PerStatement, Defini, Redi, Erumpe, Continva, ExpressionStatement, ID, + PerStatement, TemptaStatement, Defini, Redi, Erumpe, Continva, + ExpressionStatement, ID, ) from centvrion.compiler.emit_expr import emit_expr @@ -125,6 +126,24 @@ def emit_stmt(node, ctx): if isinstance(node, Continva): return ["continue;"] + if isinstance(node, TemptaStatement): + lines = [ + "_cent_try_depth++;", + "if (setjmp(_cent_try_stack[_cent_try_depth - 1]) == 0) {", + ] + try_lines = _emit_body(node.try_statements, ctx) + lines += [f" {l}" for l in try_lines] + lines += [ + " _cent_try_depth--;", + "} else {", + " _cent_try_depth--;", + f' cent_scope_set(&_scope, "{node.error_var.name}", cent_str(_cent_error_msg));', + ] + catch_lines = _emit_body(node.catch_statements, ctx) + lines += [f" {l}" for l in catch_lines] + lines += ["}"] + return lines + if isinstance(node, ExpressionStatement): lines, _ = emit_expr(node.expression, ctx) return lines diff --git a/centvrion/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c index 0fa1b51..52cd419 100644 --- a/centvrion/compiler/runtime/cent_runtime.c +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -11,6 +11,10 @@ CentArena *cent_arena; int cent_magnvm = 0; +jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX]; +int _cent_try_depth = 0; +const char *_cent_error_msg = NULL; + /* ------------------------------------------------------------------ */ /* Arena allocator */ /* ------------------------------------------------------------------ */ @@ -50,11 +54,19 @@ void *cent_arena_alloc(CentArena *a, size_t n) { /* ------------------------------------------------------------------ */ void cent_type_error(const char *msg) { + if (_cent_try_depth > 0) { + _cent_error_msg = msg; + longjmp(_cent_try_stack[_cent_try_depth - 1], 1); + } fprintf(stderr, "CENTVRION type error: %s\n", msg); exit(1); } void cent_runtime_error(const char *msg) { + if (_cent_try_depth > 0) { + _cent_error_msg = msg; + longjmp(_cent_try_stack[_cent_try_depth - 1], 1); + } fprintf(stderr, "CENTVRION error: %s\n", msg); exit(1); } diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h index 21f2d27..27d67cd 100644 --- a/centvrion/compiler/runtime/cent_runtime.h +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -3,6 +3,7 @@ #include #include +#include /* ------------------------------------------------------------------ */ /* Types */ @@ -145,8 +146,13 @@ static inline CentValue cent_dict_val(CentValue *keys, CentValue *vals, int len, /* Error handling */ /* ------------------------------------------------------------------ */ -void cent_type_error(const char *msg); /* type mismatch → exit(1) */ -void cent_runtime_error(const char *msg); /* runtime fault → exit(1) */ +#define CENT_TRY_STACK_MAX 64 +extern jmp_buf _cent_try_stack[]; +extern int _cent_try_depth; +extern const char *_cent_error_msg; + +void cent_type_error(const char *msg); /* type mismatch → longjmp or exit(1) */ +void cent_runtime_error(const char *msg); /* runtime fault → longjmp or exit(1) */ /* ------------------------------------------------------------------ */ /* Truthiness — conditions must be booleans; anything else is a fault */ diff --git a/centvrion/lexer.py b/centvrion/lexer.py index cafa264..f13c209 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -6,6 +6,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "AETERNVM", "ALVID", "AVGE", + "CAPE", "AVT", "DEFINI", "DESIGNA", @@ -32,6 +33,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "SI", "TVNC", "TABVLA", + "TEMPTA", "VSQVE", "VT", "VERITAS", diff --git a/centvrion/parser.py b/centvrion/parser.py index e787084..3f4649d 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -163,6 +163,7 @@ class Parser(): @self.pg.production('statement : dum_statement') @self.pg.production('statement : donicum_statement') @self.pg.production('statement : si_statement') + @self.pg.production('statement : tempta_statement') def nested_statements(tokens): return tokens[0] @@ -203,6 +204,10 @@ class Parser(): def per(tokens): return ast_nodes.PerStatement(tokens[3], tokens[1], tokens[6]) + @self.pg.production('tempta_statement : KEYWORD_TEMPTA SYMBOL_LCURL statements SYMBOL_RCURL KEYWORD_CAPE id SYMBOL_LCURL statements SYMBOL_RCURL') + def tempta(tokens): + return ast_nodes.TemptaStatement(tokens[2], tokens[5], tokens[7]) + @self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_FACE SYMBOL_LCURL statements SYMBOL_RCURL') def donicum(tokens): range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5]) diff --git a/language/main.tex b/language/main.tex index b58bb9f..95ed1fc 100644 --- a/language/main.tex +++ b/language/main.tex @@ -41,7 +41,10 @@ \languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{FACE} \textit{scope}} \\ \languageline{statement}{\texttt{REDI(} \textit{optional-expressions} \texttt{)}} \\ \languageline{statement}{\texttt{ERVMPE}} \\ - \languageline{statement}{\texttt{CONTINVA}} \\ \hline + \languageline{statement}{\texttt{CONTINVA}} \\ + \languageline{statement}{\textit{try-statement}} \\ \hline + + \languageline{try-statement}{\texttt{TEMPTA} \textit{scope} \texttt{CAPE} \textbf{id} \textit{scope}} \\ \hline \languageline{if-statement}{\texttt{SI} \textit{expression} \texttt{TVNC} \textit{scope}} \\ \languageline{if-statement}{\texttt{SI} \textit{expression} \texttt{TVNC} \textit{scope} \textit{optional-newline} \textit{else-statement}} \\ \hline diff --git a/snippets/syntaxes/centvrion.sublime-syntax b/snippets/syntaxes/centvrion.sublime-syntax index a5cd68d..4783cf0 100644 --- a/snippets/syntaxes/centvrion.sublime-syntax +++ b/snippets/syntaxes/centvrion.sublime-syntax @@ -78,7 +78,7 @@ contexts: scope: support.class.module.centvrion keywords: - - match: '\b(AETERNVM|ALVID|AVGE|AVT|CONTINVA|DEFINI|DESIGNA|DISPAR|DONICVM|DVM|ERVMPE|EST|ET|FACE|FVNCTIO|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TVNC|VSQVE|VT|CVM)\b' + - match: '\b(AETERNVM|ALVID|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DONICVM|DVM|ERVMPE|EST|ET|FACE|FVNCTIO|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b' scope: keyword.control.centvrion operators: diff --git a/tests.py b/tests.py index 9fee0f3..d0979f0 100644 --- a/tests.py +++ b/tests.py @@ -15,8 +15,8 @@ from centvrion.ast_nodes import ( Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, - String, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, fraction_to_frac, - num_to_int, int_to_num, make_string, + String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, + fraction_to_frac, num_to_int, int_to_num, make_string, ) from centvrion.compiler.emitter import compile_program from centvrion.errors import CentvrionError @@ -686,6 +686,7 @@ error_tests = [ ("CVM FRACTIO\n[I, II, III][IIIS VSQVE III]", CentvrionError), # slice with fractional lower bound ("CVM FRACTIO\n[I, II, III][I VSQVE IIIS]", CentvrionError), # slice with fractional upper bound ("CVM FRACTIO\n[I, II, III][I / II VSQVE III]", CentvrionError), # slice with division-fraction lower bound + ("TEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA y VT I / NVLLVS\n}", CentvrionError), # uncaught error in catch block propagates ] class TestErrors(unittest.TestCase): @@ -2642,5 +2643,122 @@ class TestScripta(unittest.TestCase): os.unlink(path) +# --- Tempta/Cape (try/catch) --- + +tempta_tests = [ + # Try block succeeds — catch not entered + ( + "TEMPTA {\nDESIGNA r VT I\n} CAPE e {\nDESIGNA r VT II\n}\nr", + Program([], [ + TemptaStatement( + [Designa(ID("r"), Numeral("I"))], + ID("e"), + [Designa(ID("r"), Numeral("II"))], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(1), + ), + # Try block errors — caught by catch + ( + "TEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT II\n}\nr", + Program([], [ + TemptaStatement( + [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], + ID("e"), + [Designa(ID("r"), Numeral("II"))], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(2), + ), + # Error variable contains the error message + ( + 'DESIGNA e VT NVLLVS\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nNVLLVS\n}\ne', + Program([], [ + Designa(ID("e"), Nullus()), + TemptaStatement( + [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], + ID("e"), + [ExpressionStatement(Nullus())], + ), + ExpressionStatement(ID("e")), + ]), + ValStr("Division by zero"), + ), + # Nested tempta — inner catches, outer unaffected + ( + "DESIGNA r VT NVLLVS\nTEMPTA {\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\n}\n} CAPE e {\nDESIGNA r VT II\n}\nr", + Program([], [ + Designa(ID("r"), Nullus()), + TemptaStatement( + [TemptaStatement( + [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], + ID("e"), + [Designa(ID("r"), Numeral("I"))], + )], + ID("e"), + [Designa(ID("r"), Numeral("II"))], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(1), + ), + # REDI inside catch block + ( + "DEFINI f () VT {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nREDI (III)\n}\nREDI (IV)\n}\nINVOCA f ()", + Program([], [ + Defini(ID("f"), [], [ + TemptaStatement( + [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], + ID("e"), + [Redi([Numeral("III")])], + ), + Redi([Numeral("IV")]), + ]), + ExpressionStatement(Invoca(ID("f"), [])), + ]), + ValInt(3), + ), + # ERVMPE inside catch block (inside a loop) + ( + "DESIGNA r VT NVLLVS\nDVM r EST I FACE {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\nERVMPE\n}\n}\nr", + Program([], [ + Designa(ID("r"), Nullus()), + DumStatement( + BinOp(ID("r"), Numeral("I"), "KEYWORD_EST"), + [TemptaStatement( + [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], + ID("e"), + [Designa(ID("r"), Numeral("I")), Erumpe()], + )], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(1), + ), + # Statement after error in try block is not executed + ( + "DESIGNA r VT NVLLVS\nTEMPTA {\nDESIGNA x VT I / NVLLVS\nDESIGNA r VT III\n} CAPE e {\nDESIGNA r VT II\n}\nr", + Program([], [ + Designa(ID("r"), Nullus()), + TemptaStatement( + [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE")), + Designa(ID("r"), Numeral("III"))], + ID("e"), + [Designa(ID("r"), Numeral("II"))], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(2), + ), +] + +class TestTempta(unittest.TestCase): + @parameterized.expand(tempta_tests) + def test_tempta(self, source, nodes, value, output=""): + run_test(self, source, nodes, value, output) + + if __name__ == "__main__": unittest.main()