skill-ls/skillls/parsing/iterative.py

134 lines
4.0 KiB
Python

from dataclasses import dataclass, field
from enum import Enum, auto
from logging import getLogger
from typing import NamedTuple, Self
from lsprotocol.types import Diagnostic, DiagnosticSeverity, Position, Range
logger = getLogger(__name__)
class Pair(NamedTuple):
start: str
end: str
class SyntaxPair(Enum):
Paren = Pair("(", ")")
Square = Pair("[", "]")
@classmethod
def by_start_elem(cls, start: str) -> Self:
for option in cls:
if option.value[0] == start:
return option
raise ValueError(f"`{start}` not a valid start character")
@classmethod
def by_end_elem(cls, end: str) -> Self:
for option in cls:
if option.value[1] == end:
return option
raise ValueError(f"`{end}` not a valid end character")
def char_range(line: int, char: int) -> Range:
return Range(Position(line, char), Position(line, char + 1))
def pair_mismatch(line: int, char: int, msg: str) -> Diagnostic:
return Diagnostic(
char_range(line, char),
msg,
severity=DiagnosticSeverity.Error,
)
class StackElement(NamedTuple):
range: Range
elem: SyntaxPair
@dataclass()
class IterativeParser:
_stack: list[StackElement] = field(default_factory=list)
def peek(self) -> StackElement:
return self._stack[-1]
def pop(self) -> StackElement:
return self._stack.pop()
def push(self, pair: StackElement) -> None:
return self._stack.append(pair)
def __call__(self, raw: list[str]) -> list[Diagnostic]:
in_string = False
errs = []
for line, raw_line in enumerate(raw):
for char, raw_char in enumerate(raw_line):
match raw_char:
case ";":
if not in_string:
break
case '"':
in_string = not in_string
case "(" | "[":
if not in_string:
self.push(
StackElement(
char_range(line, char),
SyntaxPair.by_start_elem(raw_char),
)
)
case "]" | ")":
if not in_string:
if not self._stack:
errs.append(
pair_mismatch(
line, char, f"one {raw_char} too much"
)
)
continue
expected = SyntaxPair.by_end_elem(raw_char)
elem = self._stack.pop()
if elem.elem == expected:
continue
if self._stack and self._stack[-1].elem == expected:
errs.append(
pair_mismatch(
line, char, f"unclosed {elem.elem.value.start}"
)
)
self._stack.pop()
self._stack.append(elem)
else:
errs.append(
pair_mismatch(
line, char, f"one {raw_char} too much"
)
)
self._stack.append(elem)
for rest in self._stack:
errs.append(
Diagnostic(
rest.range,
f"unclosed {rest.elem.value.start}",
severity=DiagnosticSeverity.Error,
)
)
self._stack = []
return errs
if __name__ == "__main__":
p = IterativeParser()
print(p(["((([]]))"]))