diff --git a/dingus/dingus.js b/dingus/dingus.js index 7aab450..d019ac6 100644 --- a/dingus/dingus.js +++ b/dingus/dingus.js @@ -88,7 +88,7 @@ function render_state(state, input_editor) { } if (state.output_mode === "errors") { - const error_node = document.createElement("pre"); + const error_node = document.createElement("div"); error_node.classList.add("error-panel"); if (state.errors.length == 0) { if (state.tree) { @@ -97,7 +97,13 @@ function render_state(state, input_editor) { error_node.innerText = "No errors."; } } else { - error_node.innerText = state.errors.join("\n"); + const ul = document.createElement("ul"); + ul.replaceChildren(...state.errors.map(e => { + const li = document.createElement("li"); + li.innerText = e; + return li; + })); + error_node.appendChild(ul); } OUTPUT.replaceChildren(error_node); diff --git a/dingus/srvit.py b/dingus/srvit.py index 31b0c81..e4d8a72 100755 --- a/dingus/srvit.py +++ b/dingus/srvit.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -import os +import pathlib +import socket from http.server import test, SimpleHTTPRequestHandler, ThreadingHTTPServer @@ -35,22 +36,9 @@ if __name__ == "__main__": metavar="ADDRESS", help="bind to this address " "(default: all interfaces)", ) - parser.add_argument( - "-d", - "--directory", - default=os.getcwd(), - help="serve this directory " "(default: current directory)", - ) - parser.add_argument( - "-p", - "--protocol", - metavar="VERSION", - default="HTTP/1.0", - help="conform to this HTTP version " "(default: %(default)s)", - ) parser.add_argument( "port", - default=8000, + default=8086, type=int, nargs="?", help="bind to this port " "(default: %(default)s)", @@ -58,6 +46,8 @@ if __name__ == "__main__": args = parser.parse_args() handler_class = MyHTTPRequestHandler + directory = pathlib.Path(__file__).parent + # ensure dual-stack is not disabled; ref #38907 class DualStackServer(ThreadingHTTPServer): @@ -68,12 +58,12 @@ if __name__ == "__main__": return super().server_bind() def finish_request(self, request, client_address): - self.RequestHandlerClass(request, client_address, self, directory=args.directory) + self.RequestHandlerClass(request, client_address, self, directory=str(directory)) test( HandlerClass=handler_class, ServerClass=DualStackServer, port=args.port, bind=args.bind, - protocol=args.protocol, + protocol="HTTP/1.0", ) diff --git a/makefile b/makefile index 3b9c4fb..a6d04c0 100644 --- a/makefile +++ b/makefile @@ -27,6 +27,7 @@ clean: .PHONY: dingus dingus: dingus/wheel/lrparsers-$(VERSION)-py3-none-any.whl + python3 ./dingus/srvit.py dingus/wheel/lrparsers-$(VERSION)-py3-none-any.whl: dist/lrparsers-$(VERSION)-py3-none-any.whl cp $< $@ diff --git a/parser/parser.py b/parser/parser.py index e0c8c3f..13f41c1 100644 --- a/parser/parser.py +++ b/parser/parser.py @@ -552,8 +552,9 @@ class ParseTable: actions: list[dict[str, ParseAction]] gotos: list[dict[str, int]] trivia: set[str] + error_names: dict[str, str] - def format(self): + def format(self) -> str: """Format a parser table so pretty.""" def format_action(actions: dict[str, ParseAction], terminal: str): @@ -642,7 +643,7 @@ class TableBuilder(object): if error is not None: raise error - return ParseTable(actions=self.actions, gotos=self.gotos, trivia=set()) + return ParseTable(actions=self.actions, gotos=self.gotos, trivia=set(), error_names={}) def new_row(self, config_set: ItemSet): """Start a new row, processing the given config set. Call this before @@ -1582,12 +1583,21 @@ class Terminal(Rule): pattern: "str | Re" meta: dict[str, typing.Any] regex: bool + error_name: str | None - def __init__(self, pattern: "str|Re", *, name: str | None = None, **kwargs): + def __init__( + self, + pattern: "str|Re", + *, + name: str | None = None, + error_name: str | None = None, + **kwargs, + ): self.name = name self.pattern = pattern self.meta = kwargs self.regex = isinstance(pattern, Re) + self.error_name = error_name def flatten( self, with_metadata: bool = False @@ -1611,12 +1621,14 @@ class NonTerminal(Rule): fn: typing.Callable[["Grammar"], Rule] name: str transparent: bool + error_name: str | None def __init__( self, fn: typing.Callable[["Grammar"], Rule], name: str | None = None, transparent: bool = False, + error_name: str | None = None, ): """Create a new NonTerminal. @@ -1624,10 +1636,16 @@ class NonTerminal(Rule): right-hand-side of this production; it will be flattened with `flatten`. `name` is the name of the production- if unspecified (or `None`) it will be replaced with the `__name__` of the provided fn. + + error_name is a human-readable name, to be shown in error messages. Use + this to fine-tune error messages. (For example, maybe you want your + nonterminal to be named "expr" but in error messages it should be + be spelled out: "expression".) """ self.fn = fn self.name = name or fn.__name__ self.transparent = transparent + self.error_name = error_name def generate_body(self, grammar) -> list[list[str | Terminal]]: """Generate the body of the non-terminal. @@ -1763,12 +1781,16 @@ def rule(f: typing.Callable, /) -> Rule: ... @typing.overload def rule( - name: str | None = None, transparent: bool | None = None + name: str | None = None, + transparent: bool | None = None, + error_name: str | None = None, ) -> typing.Callable[[typing.Callable[[typing.Any], Rule]], Rule]: ... def rule( - name: str | None | typing.Callable = None, transparent: bool | None = None + name: str | None | typing.Callable = None, + transparent: bool | None = None, + error_name: str | None = None, ) -> Rule | typing.Callable[[typing.Callable[[typing.Any], Rule]], Rule]: """The decorator that marks a method in a Grammar object as a nonterminal rule. @@ -1783,6 +1805,7 @@ def rule( def wrapper(f: typing.Callable[[typing.Any], Rule]): nonlocal name nonlocal transparent + nonlocal error_name if name is None: name = f.__name__ @@ -1791,7 +1814,7 @@ def rule( if transparent is None: transparent = name.startswith("_") - return NonTerminal(f, name, transparent) + return NonTerminal(f, name, transparent, error_name) return wrapper @@ -2969,6 +2992,17 @@ class Grammar: assert t.name is not None table.trivia.add(t.name) + for nt in self._nonterminals.values(): + if nt.error_name is not None: + table.error_names[nt.name] = nt.error_name + + for t in self._terminals.values(): + if t.name is not None: + if t.error_name is not None: + table.error_names[t.name] = t.error_name + elif isinstance(t.pattern, str): + table.error_names[t.name] = f'"{t.pattern}"' + return table def compile_lexer(self) -> LexerTable: diff --git a/parser/runtime.py b/parser/runtime.py index 4c5d8db..a74276a 100644 --- a/parser/runtime.py +++ b/parser/runtime.py @@ -404,6 +404,9 @@ class Parser: def __init__(self, table: parser.ParseTable): self.table = table + def readable(self, token_kind: str) -> str: + return self.table.error_names.get(token_kind, token_kind) + def parse(self, tokens: TokenStream) -> typing.Tuple[Tree | None, list[str]]: """Parse a token stream into a tree, returning both the root of the tree (if any could be found) and a list of errors that were encountered during @@ -527,7 +530,8 @@ class Parser: # See if we can figure out what we were working on here, # for the error message. if production_message is None and len(repair.reductions) > 0: - production_message = f"while parsing {repair.reductions[0]}" + reduction = repair.reductions[-1] + production_message = f"while parsing {self.readable(reduction)}" match repair.repair: case RepairAction.Base: @@ -553,7 +557,9 @@ class Parser: cursor += 1 if token_message is None: - token_message = f"Expected {repair.value}" + token_message = ( + f"(Did you forget {self.readable(repair.value)}?)" + ) case RepairAction.Delete: del input[cursor] @@ -567,10 +573,10 @@ class Parser: # Add the extra information about what we were looking for # here. - if token_message is not None: - error_message = f"{error_message}. {token_message}" if production_message is not None: error_message = f"{error_message} {production_message}" + if token_message is not None: + error_message = f"{error_message}. {token_message}" errors.append( ParseError( message=error_message,