Newer
Older
NewLang / src / parse.py
# SPDX-License-Identifier: LGPL-2.1-only
# Copyright 2022 Jookia <contact@jookia.org>

import enum
from src.syntax import Syntax, SyntaxStream, SyntaxType


# Tasks that happen during parsing
class ParseTask(enum.Enum):
    TEST_TASK = enum.auto()  # pragma: no mutate
    PARSE_NOTE = enum.auto()  # pragma: no mutate
    CLEAR_NOTES = enum.auto()  # pragma: no mutate
    PARSE_TEXT = enum.auto()  # pragma: no mutate
    PARSE_BOOL = enum.auto()  # pragma: no mutate


# Context used for parse error exception
class ParseContext:
    def __init__(self, task, syntax, parent):
        self.task = task
        self.syntax = syntax
        self.parent = parent

    def __repr__(self):
        return (
            "ParseContext(task %s, syntax %s, parent %s)"  # pragma: no mutate
            % (  # pragma: no mutate
                self.task,
                self.syntax,
                self.parent,
            )
        )

    def __eq__(self, other):
        return (
            self.task == other.task
            and self.syntax == other.syntax
            and self.parent == other.parent
        )


# Errors that can happen when parsing
class ParseError(enum.Enum):
    TEST_ERROR = enum.auto()  # pragma: no mutate
    NO_TOKEN = enum.auto()  # pragma: no mutate
    NOT_TOKEN = enum.auto()  # pragma: no mutate
    WRONG_TOKEN = enum.auto()  # pragma: no mutate
    FOUND_STARTTEXT = enum.auto()  # pragma: no mutate
    FOUND_STARTNOTE = enum.auto()  # pragma: no mutate
    NOT_BOOL = enum.auto()  # pragma: no mutate
    FOUND_ENDNOTE = enum.auto()  # pragma: no mutate


# Exception thrown when a parse error is encountered
class ParseErrorException(BaseException):
    def __init__(self, error, syntax, expected, context):
        self.error = error
        self.syntax = syntax
        self.expected = expected
        self.context = context

    def __repr__(self):
        return (
            "ParseErrorException(error %s, syntax %s, expected %s, context %s)"  # pragma: no mutate
            % (  # pragma: no mutate
                self.error,
                self.syntax,
                self.expected,
                self.context,
            )
        )

    def __eq__(self, other):
        return (
            self.error == other.error
            and self.syntax == other.syntax
            and self.expected == other.expected
            and self.context == other.context
        )


# Reads a token, possibly of a certain value
def read_token(stream, value, context):
    s = stream.pop()
    if s is None:
        raise ParseErrorException(ParseError.NO_TOKEN, None, None, context)
    elif s.type != SyntaxType.TOKEN:
        raise ParseErrorException(ParseError.NOT_TOKEN, s, None, context)
    elif value is not None and s.value != value:
        raise ParseErrorException(ParseError.WRONG_TOKEN, s, value, context)
    return s


# The note skipper in a wrapper class for easy testing
class NoteSkipper:
    # Skip a note
    def skip_note(self, stream, parent_context):
        context = ParseContext(ParseTask.PARSE_NOTE, stream.peek(), parent_context)
        read_token(stream, "StartNote", context)
        while True:
            s = read_token(stream, None, context)
            # Don't allow StartNote in notes
            if s.value in ["StartNote"]:
                raise ParseErrorException(ParseError.FOUND_STARTNOTE, s, None, context)
            # EndNote found, end things
            elif s.value == "EndNote":
                break
        return None

    # Clear notes
    def clear_notes(self, stream, parent_context):
        context = ParseContext(ParseTask.CLEAR_NOTES, stream.peek(), parent_context)
        tokens = []
        token = stream.peek()
        while token is not None:
            # Found a note, skip it
            if token.value == "StartNote":
                self.skip_note(stream, context)
            # EndNote found outside note
            elif token.value == "EndNote":
                raise ParseErrorException(
                    ParseError.FOUND_ENDNOTE, token, None, context
                )
            # Add the token if it's not note related
            else:
                tokens.append(stream.pop())
            token = stream.peek()
        return tokens


# The recursive descent parser in a wrapper class for easy testing
class Parser:
    # Parses a text syntax node
    def parse_text(self, stream, parent_context):
        context = ParseContext(ParseTask.PARSE_TEXT, stream.peek(), parent_context)
        buffer = ""
        s = read_token(stream, "StartText", context)
        location = s.location
        # Parse following tokens
        while True:
            s = read_token(stream, None, context)
            # Don't allow StartText in text
            if s.value in ["StartText"]:
                raise ParseErrorException(ParseError.FOUND_STARTTEXT, s, None, context)
            # EndText found, end things
            elif s.value == "EndText":
                break
            else:
                buffer += s.value + " "
        type = SyntaxType.TEXT
        value = buffer[:-1]  # Drop trailing space
        return Syntax(value, location, type)

    # Parses a boolean syntax node
    def parse_bool(self, stream, parent_context):
        context = ParseContext(ParseTask.PARSE_BOOL, stream.peek(), parent_context)
        s = read_token(stream, None, context)
        if s.value == "True":
            return Syntax(True, s.location, SyntaxType.BOOL)
        elif s.value == "False":
            return Syntax(False, s.location, SyntaxType.BOOL)
        else:
            raise ParseErrorException(ParseError.NOT_BOOL, s, None, context)


# Parses tokens
def parse(tokens, context):
    stream = SyntaxStream(tokens)
    cleared = NoteSkipper().clear_notes(stream, context)
    return cleared