From f8e167817aa9ec4a7f7e49b47ae6ef513ed5b81d Mon Sep 17 00:00:00 2001 From: Ian Fisher Date: Fri, 8 Feb 2019 10:31:59 -0500 Subject: [PATCH] Detect type errors even after parse errors --- CHANGELOG.md | 1 + hera/checker.py | 4 +- hera/loader.py | 12 ++++-- hera/parser.py | 16 +++++++- test/assets/error/mega_error.hera | 20 ++++++++++ test/test_error.py | 62 +++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 test/assets/error/mega_error.hera diff --git a/CHANGELOG.md b/CHANGELOG.md index 775409e..920ec15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Execution times of long-running programs has been cut down by roughly 50%. - Use of `SWI` and `RTI` operations now result in parse-time errors instead of run-time warnings. - HERA files may no longer contain non-ASCII bytes. +- Type errors are detected even in files with parse errors. ## [0.5.2] - 2019-02-02 ### Added diff --git a/hera/checker.py b/hera/checker.py index 5ea8e77..49e4879 100644 --- a/hera/checker.py +++ b/hera/checker.py @@ -23,10 +23,10 @@ def check( oplist: List[AbstractOperation], settings: Settings -) -> Tuple[Optional[Program], Messages]: +) -> Tuple[Program, Messages]: symbol_table, messages = typecheck(oplist, settings=settings) if messages.errors: - return (None, messages) + return (Program([], [], {}), messages) oplist, preprocess_messages = convert_ops(oplist, symbol_table) messages.extend(preprocess_messages) diff --git a/hera/loader.py b/hera/loader.py index 414abc6..ab6dddc 100644 --- a/hera/loader.py +++ b/hera/loader.py @@ -18,8 +18,10 @@ def load_program(text: str, settings=Settings()) -> Program: The return value of this function is valid input to the VirtualMachine.run method. """ - oplist = handle_messages(settings, parse(text, settings=settings)) - return handle_messages(settings, check(oplist, settings)) + oplist, parse_messages = parse(text, settings=settings) + program, check_messages = check(oplist, settings=settings) + handle_messages(settings, parse_messages.extend(check_messages)) + return program def load_program_from_file(path: str, settings=Settings()) -> Program: @@ -48,5 +50,7 @@ def load_program_from_file(path: str, settings=Settings()) -> Program: except HERAError as e: handle_messages(settings, Messages(str(e) + ".")) - oplist = handle_messages(settings, parse(text, path=path, settings=settings)) - return handle_messages(settings, check(oplist, settings)) + oplist, parse_messages = parse(text, path=path, settings=settings) + program, check_messages = check(oplist, settings=settings) + handle_messages(settings, parse_messages.extend(check_messages)) + return program diff --git a/hera/parser.py b/hera/parser.py index 526f82b..c70aa59 100644 --- a/hera/parser.py +++ b/hera/parser.py @@ -100,6 +100,9 @@ def match_op(self, name_tkn: Token) -> Optional[AbstractOperation]: self.lexer.next_token() args = self.match_optional_arglist() self.lexer.next_token() + if args is None: + return None + try: cls = name_to_class[name_tkn.value] except KeyError: @@ -110,13 +113,20 @@ def match_op(self, name_tkn: Token) -> Optional[AbstractOperation]: VALUE_TOKENS = {Token.INT, Token.REGISTER, Token.SYMBOL, Token.STRING, Token.CHAR} - def match_optional_arglist(self) -> List[Token]: + def match_optional_arglist(self) -> Optional[List[Token]]: + """Match zero or more comma-separated values. Exits with the right parenthesis + as the current token. Make sure to distinguish between a None return value (the + arglist could not be parsed) and a [] return value (an empty arglist was parsed + successfully). + """ if self.lexer.tkn.type == Token.RPAREN: return [] args = [] + hit_error = False while True: if not self.expect(self.VALUE_TOKENS, "expected value"): + hit_error = True self.skip_until({Token.COMMA, Token.RPAREN}) if self.lexer.tkn.type == Token.COMMA: self.lexer.next_token() @@ -131,6 +141,7 @@ def match_optional_arglist(self) -> List[Token]: if self.lexer.tkn.type == Token.RPAREN: break elif self.lexer.tkn.type != Token.COMMA: + hit_error = True self.err("expected comma or right parenthesis") self.skip_until({Token.COMMA, Token.RPAREN}) if ( @@ -140,7 +151,8 @@ def match_optional_arglist(self) -> List[Token]: break else: self.lexer.next_token() - return args + + return args if not hit_error else None def match_value(self) -> Token: if self.lexer.tkn.type == Token.INT: diff --git a/test/assets/error/mega_error.hera b/test/assets/error/mega_error.hera new file mode 100644 index 0000000..f04926b --- /dev/null +++ b/test/assets/error/mega_error.hera @@ -0,0 +1,20 @@ +// A parse error (missing comma) +SET(R1 40) + +// A type error +FOFF(R1) + +// Multiple type errors on one line +ADD('c', "abc") + +// Warning for zero-prefixed octal literal +SET(R2, 01) + +// Error for over-long character literal +SETLO(R7, 'ab') + +// Warning for invalid backslash escape, and error for data after code +LP_STRING("\y") + +// Finish off with an unclosed string literal +SET(R1, " diff --git a/test/test_error.py b/test/test_error.py index 2c748b4..66e9001 100644 --- a/test/test_error.py +++ b/test/test_error.py @@ -415,3 +415,65 @@ def test_error_for_interrupt_instructions(capsys): """ ) + + +def test_mega_error(capsys): + with pytest.raises(SystemExit): + main(["test/assets/error/mega_error.hera"]) + + captured = capsys.readouterr().err + assert ( + captured + == """\ +Warning: consider using "0o" prefix for octal numbers, line 11 col 9 of test/assets/error/mega_error.hera + + SET(R2, 01) + ^ + +Warning: unrecognized backslash escape, line 17 col 13 of test/assets/error/mega_error.hera + + LP_STRING("\\y") + ^ + +Error: expected comma or right parenthesis, line 2 col 8 of test/assets/error/mega_error.hera + + SET(R1 40) + ^ + +Error: over-long character literal, line 14 col 11 of test/assets/error/mega_error.hera + + SETLO(R7, 'ab') + ^ + +Error: unclosed string literal, line 20 col 9 of test/assets/error/mega_error.hera + + SET(R1, " + ^ + +Error: expected integer, line 5 col 6 of test/assets/error/mega_error.hera + + FOFF(R1) + ^ + +Error: too few args to ADD (expected 3), line 8 col 1 of test/assets/error/mega_error.hera + + ADD('c', "abc") + ^ + +Error: expected register, line 8 col 6 of test/assets/error/mega_error.hera + + ADD('c', "abc") + ^ + +Error: expected register, line 8 col 10 of test/assets/error/mega_error.hera + + ADD('c', "abc") + ^ + +Error: data statement after code, line 17 col 1 of test/assets/error/mega_error.hera + + LP_STRING("\\y") + ^ + +""" + )