commit c05fc0e1bac5f1f1d8a01b16de14be091e9c40d2 Author: acereca Date: Mon Oct 16 21:59:05 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d581a35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/* +.idea/* +*.egg-info/* +**/__pycache__/* +*.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3d70c12 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "main", + "type": "python", + "request": "launch", + "program": "/home/patrick/git/skill-ls/.venv/bin/skillls", + "python": "/home/patrick/git/skill-ls/.venv/bin/python" + } + ] +} diff --git a/examples/example.il b/examples/example.il new file mode 100644 index 0000000..f041d05 --- /dev/null +++ b/examples/example.il @@ -0,0 +1,12 @@ +examlpe = nil + +(procedure function(param1 (param2 t)) + + param1 = 1 + 3 + + + (call_to_other_function "arg1" t) + + c_stype_call("arg") + +) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4d8cae5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ + +[project] +name = "skillls" +version = "0.1.0" +dependencies = [ + "parsimonious~=0.10.0", + "pygls", + "rich" +] + +[project.optional-dependencies] +dev = [ + "black", + "mypy", + "ruff", + "pytest", + "types-parsimonious", +] + +[build-system] +build-backend = 'setuptools.build_meta' +requires = [ + 'setuptools', +] + +[project.scripts] +skillls = "skillls.main:main" + + +[tools.black] +line-length = 100 +target-version = "py311" +include = "skillls" diff --git a/skillls/__init__.py b/skillls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skillls/grammar.peg b/skillls/grammar.peg new file mode 100644 index 0000000..38d92fa --- /dev/null +++ b/skillls/grammar.peg @@ -0,0 +1,35 @@ + +skill = inline_expr+ +expr = (inline_expr / nl) + +inline_expr = (listraw / listc / listskill / inline_get / inline_op / inline_assign / ws) + +inline_assign = TOKEN ws* "=" ws* (inline_expr / LITERAL / TOKEN) + +inline_op = TOKEN ws* inline_op_symbol ws* (inline_expr / TOKEN / LITERAL) +inline_op_symbol = ~"[*-+/]" + +inline_get = TOKEN inline_get_symbol (inline_expr / TOKEN / LITERAL) +inline_get_symbol = ~"(~>|->)" + + +listraw = "'" list_start expr* list_end +listc = TOKEN list_start expr* list_end +listskill = list_start expr* list_end + +list_start = "(" +list_end = ")" + +TOKEN = ~"[a-zA-Z_][_a-zA-Z0-9]+" +LITERAL = L_num / L_t / L_nil / L_str + +L_num = ~"[0-9]+(\.[0-9]+)?" +L_t = "t" +L_nil = "nil" + +L_str = delim_str any_str delim_str +delim_str = "\"" +any_str = ~"[^\"]*" + +ws = ~"\\h" +nl = ~"\\n" diff --git a/skillls/main.py b/skillls/main.py new file mode 100644 index 0000000..0ea28ff --- /dev/null +++ b/skillls/main.py @@ -0,0 +1,83 @@ +from logging import INFO, basicConfig, getLogger +from pathlib import Path +from urllib.parse import unquote +from lsprotocol.types import ( + TEXT_DOCUMENT_DOCUMENT_SYMBOL, + DocumentSymbol, + DocumentSymbolParams, + Position, + Range, + SymbolKind, +) + +from pygls.server import LanguageServer +from parsimonious import Grammar +from pygls.uris import urlparse + +# from skillls.parsing.location import Range + +from .parsing.tokenize import Locator, SkillVisitor + + +example = """ +(skillist siomfpwqmqwepfomkjnbkjb + '(rawlist token) + clist(qwerfwf) +) +""" + +cache = {} + + +def parse(path: Path): + # path = Path(__file__).parent / "grammar.peg" + grammar = Grammar(path.read_text()) + + locator = Locator(example) + tree = grammar.parse(example) + + iv = SkillVisitor(locator) + output = iv.visit(tree) + + return output + + +def parse_and_cache(uri: str) -> list[DocumentSymbol]: + path = Path(unquote(urlparse(uri).path)) + if not path.exists(): + logger.error("could not find %s", path) + return [] + + if not cache.get(path): + logger.info("%s not yet cached, parsing...") + out = parse(path) + logger.info("%s", out) + + return [] + + +basicConfig(filename="skillls.log", level=INFO) + +logger = getLogger(__name__) +server = LanguageServer("skillls", "v0.1") + + +@server.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL) +def document_symbols(params: DocumentSymbolParams) -> list[DocumentSymbol]: + logger.info("requested document symbols for %s", params.text_document.uri) + doc = server.workspace.documents[params.text_document.uri] + return [ + DocumentSymbol( + "~global_scope", + kind=SymbolKind.Namespace, + range=Range( + start=Position(0, 0), + end=Position(len(doc.lines) - 1, len(doc.lines[-1])), + ), + selection_range=Range(Position(0, 0), Position(0, 0)), + ) + ] + + +def main(): + server.start_io() diff --git a/skillls/parsing/__init__.py b/skillls/parsing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skillls/parsing/context.py b/skillls/parsing/context.py new file mode 100644 index 0000000..4114a94 --- /dev/null +++ b/skillls/parsing/context.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from enum import Enum, auto +from typing import ClassVar, DefaultDict + +from .location import Range +from .tokenize import BaseToken + + +class ContextType(Enum): + Use = auto() + Assign = auto() + Declare = auto() + Unbind = auto() + + +@dataclass(frozen=True) +class Context: + lookup: ClassVar[dict[ContextType, list["Context"]]] = {} + + typ: ContextType + token: list[BaseToken] + + def __post_init__(self): + type(self).lookup.setdefault(self.typ, []) + type(self).lookup[self.typ].append(self) + + @property + def range(self) -> Range: + new_range = self.token[0].range + for token in self.token[1:]: + new_range += token.range + return new_range diff --git a/skillls/parsing/location.py b/skillls/parsing/location.py new file mode 100644 index 0000000..2aa66c0 --- /dev/null +++ b/skillls/parsing/location.py @@ -0,0 +1,71 @@ +from dataclasses import dataclass +from functools import cached_property +from typing import overload +from lsprotocol.types import Position, Range + +from parsimonious.nodes import Node + + +# @total_ordering +# class Position(NamedTuple): +# line: int +# char: int +# +# def __lt__(self, other: Self) -> bool: +# return (self.line < other.line) or ( +# (self.line == other.line) and (self.char < other.char) +# ) +# +# def __eq__(self, other: Self) -> bool: +# return (self.line == other.line) and (self.char == other.char) +# +# +# class Range(NamedTuple): +# start: Position +# end: Position +# +# def __add__(self, other: Self) -> Self: +# start = min(self.start, other.start) +# end = max(self.end, other.end) +# return Range(start, end) +# +# def contained_by(self, possibly_contained_by: Self) -> bool: +# return (self.start >= possibly_contained_by.start) and ( +# self.end <= possibly_contained_by.end +# ) +# +# def contains(self, possibly_contains: Self) -> bool: +# return (self.start <= possibly_contains.start) and ( +# self.end >= possibly_contains.end +# ) + + +@dataclass(frozen=True) +class Locator: + raw: str + + @cached_property + def newlines(self) -> tuple[int, ...]: + t = tuple(i for i, char in enumerate(self.raw) if char == "\n") + return t + + def _locate_pos(self, index: int) -> Position: + line = next(i for i, char in enumerate(self.newlines) if char >= index) + return Position(line - 1, index - (self.newlines[line - 1] if line > 0 else 0)) + + @overload + def locate(self, index: int) -> Position: + ... + + @overload + def locate(self, index: Node) -> Range: + ... + + def locate(self, index: int | Node) -> Position | Range: + if isinstance(index, int): + return self._locate_pos(index) + + start = self._locate_pos(index.start) + end = self._locate_pos(index.end) + + return Range(start, end) diff --git a/skillls/parsing/tokenize.py b/skillls/parsing/tokenize.py new file mode 100644 index 0000000..e660c35 --- /dev/null +++ b/skillls/parsing/tokenize.py @@ -0,0 +1,111 @@ +from typing import Any, Sequence +from lsprotocol.types import Range +from parsimonious import ParseError +from dataclasses import dataclass + +from parsimonious.nodes import Node, NodeVisitor + +from .location import Locator + + +@dataclass(frozen=True) +class BaseToken: + range: Range + + +@dataclass(frozen=True) +class Literal(BaseToken): + value: str | float | bool + + +@dataclass(frozen=True) +class Token(BaseToken): + value: str + + +@dataclass(frozen=True) +class List(BaseToken): + value: list[BaseToken] + + +@dataclass +class SkillVisitor(NodeVisitor): + locator: Locator + + def visit_skill(self, _: Node, visited_children: Sequence[Any]) -> list[BaseToken]: + children = [] + for childlist in visited_children: + for child in childlist: + if isinstance(child, BaseToken): + children.append(child) + + return children + + def visit_TOKEN(self, node: Node, _: Any) -> Token: + return Token(self.locator.locate(node), node.text) + + def visit_LITERAL(self, node: Node, visited_children: list[None | Node]) -> Literal: + value, *_ = visited_children + if value: + match value.expr_name: + case "L_t": + return Literal(self.locator.locate(node), True) + case "L_nil": + return Literal(self.locator.locate(node), False) + case "L_num": + return Literal(self.locator.locate(node), float(value.text)) + case "L_string": + return Literal(self.locator.locate(node), value.text) + case _: + pass + + raise ParseError("something went wrong during literal parsing") + + def visit_listraw( + self, node: Node, visited_children: list[list[list[Any]]] + ) -> List: + rest = visited_children[2] + + children = [] + + for child in rest: + for part in child: + if isinstance(part, BaseToken): + children.append(part) + + return List(self.locator.locate(node), children) + + def visit_listc(self, node: Node, visited_children: list[list[list[Any]]]) -> List: + rest = ([[visited_children[0]]], visited_children[2]) + + children = [] + + for child_list in rest: + for child in child_list: + for part in child: + if isinstance(part, BaseToken): + children.append(part) + + return List(self.locator.locate(node), children) + + def visit_listskill( + self, node: Node, visited_children: list[list[list[Any]]] + ) -> List: + rest = visited_children[1] + + children = [] + + for child in rest: + for part in child: + if isinstance(part, BaseToken): + children.append(part) + + return List(self.locator.locate(node), children) + + def visit_inline_assign(self, node: Node, visited_children: Sequence[Any]): + print(node) + + def generic_visit( + self, node: Node, visited_children: Sequence[Any] + ) -> Node | Sequence[None | Node]: + return visited_children or node