Example #1
0
    def run(self, source: str):
        logger.debug("Running line", source=source)

        scanner = Scanner(source, on_error=self.error)
        tokens = scanner.scan_tokens()

        for token in tokens:
            logger.debug("Running token", token=token)

        parser = Parser(tokens, on_token_error=self.token_error)
        statements = parser.parse()

        if self.had_error:
            logger.debug("Error after parsing")
            return

        resolver = Resolver(interpreter=self.interpreter,
                            on_error=self.token_error)
        resolver.resolve(statements)
        # Stop if there was a resolution error.
        if self.had_error:
            logger.debug("Error after resolving")
            return

        self.interpreter.interpret(statements, on_error=self.runtime_error)
Example #2
0
    def test_scanner_with_number(self, mocker):
        source = "123 12.23 3+5 13."

        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)

        tokens = scanner.scan_tokens()
        assert tokens[0].token_type == TokenType.NUMBER
        assert tokens[0].literal == 123.0

        assert tokens[1].token_type == TokenType.NUMBER
        assert tokens[1].literal == 12.23

        assert tokens[2].token_type == TokenType.NUMBER
        assert tokens[2].literal == 3.0

        assert tokens[3].token_type == TokenType.PLUS

        assert tokens[4].token_type == TokenType.NUMBER
        assert tokens[4].literal == 5.0

        assert tokens[5].token_type == TokenType.NUMBER
        assert tokens[5].literal == 13.0

        assert not on_error_mock.called
Example #3
0
    def test_scanner_invalid_identifier(self, mocker):
        # The bit of source code below is completely wrong, and identifies and
        # numbers in here will not result in valid tokens, but not the tokens you
        # would expect. This is not a problem of the scanner, it just does as it's
        # told.
        source = "123foo_bar bar-stool spam_egg_1.3_chickens"

        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)

        tokens = scanner.scan_tokens()

        assert tokens[0].literal == 123.0

        assert tokens[1].lexeme == "foo_bar"
        assert tokens[1].token_type == TokenType.IDENTIFIER

        assert tokens[2].lexeme == "bar"
        assert tokens[2].token_type == TokenType.IDENTIFIER

        assert tokens[3].token_type == TokenType.MINUS

        assert tokens[4].lexeme == "stool"
        assert tokens[5].lexeme == "spam_egg_1"
        assert tokens[6].token_type == TokenType.DOT

        # This token did not consume the 1 before, since that was still part of the
        # valid identifier. The dot broke the identifier, and then a number started
        assert tokens[7].token_type == TokenType.NUMBER
        assert tokens[7].literal == 3.0

        assert tokens[8].lexeme == "_chickens"

        assert not on_error_mock.called
Example #4
0
    def test_assignment(self, mocker):
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()
        on_interpret_error_mock = mocker.MagicMock()

        lines = [
            "var a = 0;",
            "var c = a;",
            "var b;",
            "a = 3 + 6;",
            "b = 3 / 6;",
            "a = a + b;",
            "print(a);",
            "a;",
        ]

        expression = "\n".join(lines)

        scanner = Scanner(expression, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        result = Interpreter().interpret(statements,
                                         on_error=on_interpret_error_mock)

        assert result == 9.5
Example #5
0
    def test_nested_binary_expr(self, create_token_factory, mocker):
        """ Test nested binary expressions, 4 * 6 / 2 """
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        test_string = "4 * 6 / 2;"
        scanner = Scanner(test_string, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()
        expr: Expression = statements[0].expression

        assert isinstance(expr, Binary)
        assert isinstance(expr.left, Binary)
        assert isinstance(expr.right, Literal)

        assert expr.operator.token_type == TokenType.SLASH
        assert expr.right.value == 2.0
        # Left will be 4 * 6
        assert expr.left.operator.token_type == TokenType.STAR
        assert expr.left.left.value == 4
        assert expr.left.right.value == 6

        result = Interpreter().visit_binary_expr(expr)

        assert result == 12
Example #6
0
    def test_parser_synchronize(self, mocker):
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        lines = [
            "var a = 4;",
            "print (4+3;",
            "a = 5;",
            "",
            "var a = (3/4",
            "print (a);",
        ]
        source = "\n".join(lines)
        scanner = Scanner(source, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        # We validate that the error is called, the real values are tested in
        # test_missing_closing_bracket
        assert not on_scanner_error_mock.called
        assert on_parser_error_mock.called

        # Assert that the third statement is sane and that the parser continues after
        # errors
        assert isinstance(statements[1], Expression)
        assert statements[1].expression.name.line == 3
        assert statements[1].expression.name.lexeme == "a"
Example #7
0
    def test_for_invalid_for_loop_parser(self, mocker):
        """ Test that we parse the for loop correctly """

        source = "for (var a = 0; a <= 5; a = a + 1) {}"
        left_missing = source.replace("(", "")
        right_missing = source.replace(")", "")

        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        scanner = Scanner(source, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        assert statements
        assert not on_scanner_error_mock.called
        assert not on_parser_error_mock.called

        # Test a invalid statement, missing (
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()
        scanner = Scanner(left_missing, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        parser.parse()

        assert not on_scanner_error_mock.called
        assert on_parser_error_mock.called
        assert "Expect '(' after 'for'" in str(on_parser_error_mock.call_args_list[0])

        # Test a invalid statement, missing )
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()
        scanner = Scanner(right_missing, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        parser.parse()

        assert not on_scanner_error_mock.called
        assert on_parser_error_mock.called
        assert "Expect ')' after for clauses" in str(
            on_parser_error_mock.call_args_list[0]
        )
Example #8
0
    def test_scanner_operator(self, source, expected_output_list, mocker):
        on_error_mock = mocker.MagicMock()

        scanner = Scanner(source, on_error=on_error_mock)
        tokens = scanner.scan_tokens()

        # Retrieve the token_types from the created tokens
        token_types = [token.token_type for token in tokens]

        assert token_types == expected_output_list
        assert not on_error_mock.called
Example #9
0
    def test_interpret(self, mocker, expression, result):
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        scanner = Scanner(expression, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        result = Interpreter().interpret(statements)

        assert result == result
Example #10
0
    def test_scanner_with_string(self, mocker):
        source = '+"This is a String"'
        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)
        tokens = scanner.scan_tokens()

        assert tokens[0].token_type == TokenType.PLUS
        assert tokens[1].token_type == TokenType.STRING
        assert tokens[1].literal == "This is a String"
        assert tokens[2].token_type == TokenType.EOF

        assert not on_error_mock.called
Example #11
0
    def test_scanner_bad_char(self, mocker):
        source = "@"
        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)
        tokens = scanner.scan_tokens()

        # Invalid character is not added, but the EOF marker is added
        assert tokens[0].token_type == TokenType.EOF
        assert tokens[0].line == 1

        # Assert that our error code has been called
        assert on_error_mock.called
        on_error_mock.assert_called_once_with(1, "Unexpected character: @")
Example #12
0
    def test_scanner_with_multiline_string(self, mocker):
        source = "This is an \nMulti-\nline-string"
        source_input = f'"{source}"'

        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source_input, on_error=on_error_mock)

        tokens = scanner.scan_tokens()
        assert tokens[0].token_type == TokenType.STRING
        assert tokens[0].literal == source
        assert not on_error_mock.called

        # We have traveled three lines
        assert scanner.line == 3
Example #13
0
    def test_scanner_with_unterminated_string(self, mocker):
        source = '+"This is a Unterminated String'
        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)

        tokens = scanner.scan_tokens()

        # Validate that all the default tokens have been added
        assert tokens[0].token_type == TokenType.PLUS
        assert tokens[1].token_type == TokenType.EOF

        # Assert that our error code has been called
        assert on_error_mock.called
        on_error_mock.assert_called_once_with(1, "Unterminated string.")
Example #14
0
    def test_scanner(self, mocker):
        on_error_mock = mocker.MagicMock()

        source = "+-\n*"
        scanner = Scanner(source, on_error=on_error_mock)
        tokens = scanner.scan_tokens()
        assert tokens[0].token_type == TokenType.PLUS
        assert tokens[1].token_type == TokenType.MINUS
        assert tokens[2].token_type == TokenType.STAR

        # the EOF is automatically added
        assert tokens[3].token_type == TokenType.EOF

        # The newline char (\n) doesn't add a token, but increments the line counter
        assert scanner.line == 2

        # There mustn't be an error
        assert not on_error_mock.called
Example #15
0
    def test_interpret_error(self, mocker):
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()
        on_interpret_error_mock = mocker.MagicMock()

        expression = '0 + "Foo";'

        scanner = Scanner(expression, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        Interpreter().interpret(statements, on_error=on_interpret_error_mock)

        # There will be an error
        assert on_interpret_error_mock.called
        assert "Operands must be two numbers or two strings" in str(
            on_interpret_error_mock.call_args)
Example #16
0
    def test_for_parser(self, mocker):
        """ Test that we parse the for loop correctly """

        source = "for(var a = 0; ; a = a + 1) {}"

        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        scanner = Scanner(source, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        assert statements
        # Because the condition is missing, it must alway be true:
        assert statements[0].statements[1].condition.value is True

        assert not on_scanner_error_mock.called
        assert not on_parser_error_mock.called
Example #17
0
    def test_missing_closing_bracket(self, mocker):
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        source = "(23+34"

        scanner = Scanner(source, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        # There will be an error. Scanner must be fine, but the error will be in the
        # parser. There will be no statements generated
        assert statements == []
        assert not on_scanner_error_mock.called
        assert on_parser_error_mock.called

        on_parser_error_mock.assert_called_once_with(
            tokens[-1], "Expect ')' after expression."
        )
Example #18
0
    def test_scanner_identifier(self, mocker):
        source = "appelflap or nil if while _foo_bar_1_2"

        on_error_mock = mocker.MagicMock()
        scanner = Scanner(source, on_error=on_error_mock)

        tokens = scanner.scan_tokens()

        assert tokens[0].token_type == TokenType.IDENTIFIER
        assert tokens[0].lexeme == "appelflap"

        assert tokens[1].token_type == TokenType.OR
        assert tokens[2].token_type == TokenType.NIL
        assert tokens[3].token_type == TokenType.IF
        assert tokens[4].token_type == TokenType.WHILE

        assert tokens[5].token_type == TokenType.IDENTIFIER
        assert tokens[5].lexeme == "_foo_bar_1_2"

        assert not on_error_mock.called
Example #19
0
    def test_invalid_assignment(self, mocker):
        """
        An identifiers must start with a letter [a-z], numbers are invalid and
        must raise an error
        """
        on_scanner_error_mock = mocker.MagicMock()
        on_parser_error_mock = mocker.MagicMock()

        expression = "var 0123foobar = 34;"

        scanner = Scanner(expression, on_error=on_scanner_error_mock)
        tokens = scanner.scan_tokens()
        parser = Parser(tokens, on_token_error=on_parser_error_mock)
        statements = parser.parse()

        assert statements == []
        assert not on_scanner_error_mock.called
        assert on_parser_error_mock.called

        on_parser_error_mock.assert_called_once_with(tokens[1], "Expect variable name.")
Example #20
0
 def test_scanner_bad_char_without_callback(self):
     source = "@"
     scanner = Scanner(source)
     with pytest.raises(KeyError):
         scanner.scan_tokens()