src/ail_ref/parser.py

Read-only source from the unchanged 0.3.4 kit. Existing license terms apply.

from __future__ import annotations

from decimal import Decimal, InvalidOperation

from .ast import (
    Alias,
    Atom,
    Boolean,
    Call,
    Document,
    ListValue,
    Number,
    ProtocolUnit,
    Record,
    String,
    Unknown,
    Value,
    Variable,
)
from .lexer import LexError, Token, tokenize


class ParseError(ValueError):
    pass


class Parser:
    def __init__(self, text: str):
        try:
            self.tokens = tokenize(text)
        except LexError as exc:
            raise ParseError(str(exc)) from exc
        self.index = 0

    def peek(self, kind: str | None = None) -> Token | bool:
        token = self.tokens[self.index]
        return token if kind is None else token.kind == kind

    def take(self, kind: str) -> Token:
        token = self.tokens[self.index]
        if token.kind != kind:
            raise ParseError(
                f"expected {kind}, received {token.kind} {token.value!r} at offset {token.offset}"
            )
        self.index += 1
        return token

    def parse_document(self) -> Document:
        units: list[ProtocolUnit] = []
        while not self.peek("EOF"):
            header = self.take("HEADER").value
            value = self.parse_value()
            units.append(ProtocolUnit(header, value))
        if not units:
            raise ParseError("document contains no protocol units")
        return Document(tuple(units))

    def parse_value(self) -> Value:
        token = self.peek()
        assert isinstance(token, Token)
        if token.kind == "VARIABLE":
            self.index += 1
            return Variable(token.value)
        if token.kind == "UNKNOWN":
            self.index += 1
            return Unknown()
        if token.kind == "STRING":
            self.index += 1
            return String(token.value)
        if token.kind == "NUMBER":
            self.index += 1
            try:
                return Number(Decimal(token.value))
            except InvalidOperation as exc:
                raise ParseError(f"invalid number {token.value!r} at offset {token.offset}") from exc
        if token.kind == "BOOLEAN":
            self.index += 1
            return Boolean(token.value == "TRUE")
        if token.kind == "[":
            return self.parse_list()
        if token.kind == "IDENT":
            name = self.take("IDENT").value
            if self.peek("{"):
                return self.parse_record(name)
            if self.peek("("):
                return self.parse_call(name)
            atom = Atom(name)
            if self.peek("~"):
                self.take("~")
                label_token = self.peek()
                assert isinstance(label_token, Token)
                if label_token.kind not in {"IDENT", "STRING"}:
                    raise ParseError(f"expected alias label at offset {label_token.offset}")
                self.index += 1
                return Alias(atom, label_token.value)
            return atom
        raise ParseError(f"unexpected token {token.kind} {token.value!r} at offset {token.offset}")

    def parse_list(self) -> ListValue:
        self.take("[")
        items: list[Value] = []
        if not self.peek("]"):
            while True:
                items.append(self.parse_value())
                if not self.peek(","):
                    break
                self.take(",")
        self.take("]")
        return ListValue(tuple(items))

    def parse_call(self, name: str) -> Call:
        self.take("(")
        args: list[Value] = []
        if not self.peek(")"):
            while True:
                args.append(self.parse_value())
                if not self.peek(","):
                    break
                self.take(",")
        self.take(")")
        return Call(name, tuple(args))

    def parse_record(self, name: str) -> Record:
        self.take("{")
        fields: list[tuple[str, Value]] = []
        seen: set[str] = set()
        while not self.peek("}"):
            key = self.take("IDENT").value
            if key in seen:
                token = self.peek()
                assert isinstance(token, Token)
                raise ParseError(f"duplicate field {key!r} near offset {token.offset}")
            seen.add(key)
            self.take("=")
            value = self.parse_value()
            self.take(";")
            fields.append((key, value))
        self.take("}")
        return Record(name, tuple(fields))


def parse(text: str) -> Document:
    return Parser(text).parse_document()