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()