Interactive grammar editing

This commit is contained in:
John Doty 2024-05-29 19:28:42 -07:00
parent 45a9303a27
commit 71078f76b4
2 changed files with 106 additions and 29 deletions

View file

@ -82,10 +82,10 @@ class FineGrammar(Grammar):
@rule @rule
def _file_statement_list(self): def _file_statement_list(self):
return self.file_statement | (self._file_statement_list + self.file_statement) return self._file_statement | (self._file_statement_list + self._file_statement)
@rule @rule
def file_statement(self): def _file_statement(self):
return ( return (
self.import_statement | self.class_declaration | self.export_statement | self.statement self.import_statement | self.class_declaration | self.export_statement | self.statement
) )
@ -100,14 +100,14 @@ class FineGrammar(Grammar):
@rule @rule
def class_body(self): def class_body(self):
return seq(LCURLY, RCURLY) | seq(LCURLY, self.class_members, RCURLY) return seq(LCURLY, RCURLY) | seq(LCURLY, self._class_members, RCURLY)
@rule @rule
def class_members(self): def _class_members(self):
return self.class_member | seq(self.class_members, self.class_member) return self._class_member | seq(self._class_members, self._class_member)
@rule @rule
def class_member(self): def _class_member(self):
return self.field_declaration | self.function_declaration return self.field_declaration | self.function_declaration
@rule @rule

View file

@ -1,11 +1,15 @@
import bisect import bisect
from dataclasses import dataclass import importlib
import inspect
import enum import enum
import os
import select import select
import sys import sys
import termios import termios
import time
import tty import tty
import typing import typing
from dataclasses import dataclass
import grammar import grammar
import parser import parser
@ -143,7 +147,7 @@ def CSI(x: bytes) -> bytes:
return ESC(b"[" + x) return ESC(b"[" + x)
CLEAR = CSI(b"H") + CSI(b"0m") CLEAR = CSI(b"2J")
def enter_alt_screen(): def enter_alt_screen():
@ -159,11 +163,10 @@ class Harness:
table: parser.ParseTable | None table: parser.ParseTable | None
tree: Tree | None tree: Tree | None
def __init__(self, lexer_func, grammar_func, start_rule, source_path): def __init__(self, lexer_func, start_rule, source_path):
# self.generator = parser.GenerateLR1 # self.generator = parser.GenerateLR1
self.generator = parser.GenerateLALR self.generator = parser.GenerateLALR
self.lexer_func = lexer_func self.lexer_func = lexer_func
self.grammar_func = grammar_func
self.start_rule = start_rule self.start_rule = start_rule
self.source_path = source_path self.source_path = source_path
@ -173,6 +176,11 @@ class Harness:
self.tree = None self.tree = None
self.errors = None self.errors = None
self.grammar_file_name = "./grammar.py"
self.last_grammar_time = None
self.grammar_module = None
self.grammar_name = None
def run(self): def run(self):
while True: while True:
i, _, _ = select.select([sys.stdin], [], [], 1) i, _, _ = select.select([sys.stdin], [], [], 1)
@ -183,38 +191,108 @@ class Harness:
self.update() self.update()
def update(self): # def should_reload_grammar(self):
if self.table is None:
self.table = self.grammar_func().build_table( def load_grammar(self) -> parser.ParseTable:
start=self.start_rule, generator=self.generator st = os.stat(self.grammar_file_name)
) if self.last_grammar_time == st.st_mtime:
assert self.table is not None assert self.table is not None
return self.table
self.table = None
if self.grammar_module is None:
mod_name = inspect.getmodulename(self.grammar_file_name)
if mod_name is None:
raise Exception(f"{self.grammar_file_name} does not seem to be a module")
self.grammar_module = importlib.import_module(mod_name)
else:
importlib.reload(self.grammar_module)
def is_grammar(cls):
if not inspect.isclass(cls):
return False
assert self.grammar_module is not None
if cls.__module__ != self.grammar_module.__name__:
return False
if getattr(cls, "build_table", None):
return True
return False
if self.grammar_name is None:
classes = inspect.getmembers(self.grammar_module, is_grammar)
if len(classes) == 0:
raise Exception(f"No grammars found in {self.grammar_file_name}")
if len(classes) > 1:
raise Exception(
f"{len(classes)} grammars found in {self.grammar_file_name}: {', '.join(c[0] for c in classes)}"
)
grammar_func = classes[0][1]
else:
cls = getattr(self.grammar_module, self.grammar_name)
if cls is None:
raise Exception(f"Cannot find {self.grammar_name} in {self.grammar_file_name}")
if not is_grammar(cls):
raise Exception(
f"{self.grammar_name} in {self.grammar_file_name} does not seem to be a grammar"
)
grammar_func = cls
self.table = grammar_func().build_table(start=self.start_rule, generator=self.generator)
self.last_grammar_time = st.st_mtime
assert self.table is not None
return self.table
def update(self):
start_time = time.time()
try:
table = self.load_grammar()
if self.tokens is None:
with open(self.source_path, "r", encoding="utf-8") as f: with open(self.source_path, "r", encoding="utf-8") as f:
self.source = f.read() self.source = f.read()
self.tokens = self.lexer_func(self.source)
# print(f"{tokens.lines}") self.tokens = self.lexer_func(self.source)
# tokens.dump(end=5) lex_time = time.time()
if self.tree is None and self.errors is None:
(tree, errors) = parse(self.table, self.tokens, trace=None) # print(f"{tokens.lines}")
self.tree = tree # tokens.dump(end=5)
self.errors = errors (tree, errors) = parse(table, self.tokens, trace=None)
parse_time = time.time()
self.tree = tree
self.errors = errors
parse_elapsed = parse_time - lex_time
except Exception as e:
self.tree = None
self.errors = [f"Error loading grammar: {e}"]
parse_elapsed = time.time() - start_time
table = None
sys.stdout.buffer.write(CLEAR) sys.stdout.buffer.write(CLEAR)
rows, cols = termios.tcgetwinsize(sys.stdout.fileno()) rows, cols = termios.tcgetwinsize(sys.stdout.fileno())
states = self.table.states if table is not None:
average_entries = sum(len(row) for row in states) / len(states) states = table.states
max_entries = max(len(row) for row in states) average_entries = sum(len(row) for row in states) / len(states)
print(f"{len(states)} states - {average_entries} average, {max_entries} max\r") max_entries = max(len(row) for row in states)
print(
f"{len(states)} states - {average_entries:.3} average, {max_entries} max - {parse_elapsed:.3}s \r"
)
else:
print("No table\r\n")
if self.tree is not None: if self.tree is not None:
lines = [] lines = []
self.format_node(lines, self.tree) self.format_node(lines, self.tree)
for line in lines[: rows - 2]: for line in lines[: rows - 2]:
print(line[:cols] + "\r") print(line[:cols] + "\r")
else:
for error in self.errors[: rows - 2]:
print(error[:cols] + "\r")
sys.stdout.flush() sys.stdout.flush()
sys.stdout.buffer.flush() sys.stdout.buffer.flush()
@ -243,7 +321,6 @@ if __name__ == "__main__":
h = Harness( h = Harness(
lexer_func=grammar.FineTokens, lexer_func=grammar.FineTokens,
grammar_func=grammar.FineGrammar,
start_rule="file", start_rule="file",
source_path=source_path, source_path=source_path,
) )