Compare commits

...

3 commits

Author SHA1 Message Date
55c4675fe5 Use precedence and not a lot of different rules 2024-05-31 08:32:05 -07:00
fee1c68dea Wrap errors
Maybe people want to see them
2024-05-31 08:31:47 -07:00
275d8afe26 Raise special errors on ambiguities
Maybe people want to see them
2024-05-31 08:31:30 -07:00
3 changed files with 89 additions and 69 deletions

View file

@ -75,6 +75,11 @@ class FineGrammar(Grammar):
# statement or an expression, prefer the statement. # statement or an expression, prefer the statement.
# #
(Assoc.NONE, [self.if_statement]), (Assoc.NONE, [self.if_statement]),
#
# If there's confusion about whether to make an IS expression
# or something else, prefer IS.
#
(Assoc.NONE, [self.is_expression]),
], ],
) )
@ -224,59 +229,32 @@ class FineGrammar(Grammar):
return seq(self.expression, SEMICOLON) return seq(self.expression, SEMICOLON)
# Expressions # Expressions
@rule @rule(transparent=True)
def expression(self) -> Rule: def expression(self) -> Rule:
return self.assignment_expression return self.binary_expression | self.is_expression | self.primary_expression
@rule @rule("BinaryExpression")
def assignment_expression(self) -> Rule: def binary_expression(self) -> Rule:
return seq(self.or_expression, EQUAL, self.assignment_expression) | self.or_expression return (
seq(self.expression, EQUAL, self.expression)
| seq(self.expression, OR, self.expression)
| seq(self.expression, IS, self.pattern)
| seq(self.expression, AND, self.expression)
| seq(self.expression, EQUALEQUAL, self.expression)
| seq(self.expression, BANGEQUAL, self.expression)
| seq(self.expression, LESS, self.expression)
| seq(self.expression, LESSEQUAL, self.expression)
| seq(self.expression, GREATER, self.expression)
| seq(self.expression, GREATEREQUAL, self.expression)
| seq(self.expression, PLUS, self.expression)
| seq(self.expression, MINUS, self.expression)
| seq(self.expression, STAR, self.expression)
| seq(self.expression, SLASH, self.expression)
)
@rule @rule("IsExpression")
def or_expression(self) -> Rule:
return seq(self.or_expression, OR, self.is_expression) | self.is_expression
@rule
def is_expression(self) -> Rule: def is_expression(self) -> Rule:
return seq(self.is_expression, IS, self.pattern) | self.and_expression return seq(self.expression, IS, self.pattern)
@rule
def and_expression(self) -> Rule:
return seq(self.and_expression, AND, self.equality_expression) | self.equality_expression
@rule
def equality_expression(self) -> Rule:
return (
seq(self.equality_expression, EQUALEQUAL, self.relation_expression)
| seq(self.equality_expression, BANGEQUAL, self.relation_expression)
| self.relation_expression
)
@rule
def relation_expression(self) -> Rule:
return (
seq(self.relation_expression, LESS, self.additive_expression)
| seq(self.relation_expression, LESSEQUAL, self.additive_expression)
| seq(self.relation_expression, GREATER, self.additive_expression)
| seq(self.relation_expression, GREATEREQUAL, self.additive_expression)
| self.additive_expression
)
@rule
def additive_expression(self) -> Rule:
return (
seq(self.additive_expression, PLUS, self.multiplication_expression)
| seq(self.additive_expression, MINUS, self.multiplication_expression)
| self.multiplication_expression
)
@rule
def multiplication_expression(self) -> Rule:
return (
seq(self.multiplication_expression, STAR, self.primary_expression)
| seq(self.multiplication_expression, SLASH, self.primary_expression)
| self.primary_expression
)
@rule @rule
def primary_expression(self) -> Rule: def primary_expression(self) -> Rule:
@ -343,12 +321,16 @@ class FineGrammar(Grammar):
@rule("Pattern") @rule("Pattern")
def pattern(self) -> Rule: def pattern(self) -> Rule:
return ( return (
seq(self.variable_binding, self._pattern_core, AND, self.and_expression) seq(self.variable_binding, self._pattern_core, self.pattern_predicate)
| seq(self.variable_binding, self._pattern_core) | seq(self.variable_binding, self._pattern_core)
| seq(self._pattern_core, AND, self.and_expression) | seq(self._pattern_core, self.pattern_predicate)
| self._pattern_core | self._pattern_core
) )
@rule(transparent=True)
def pattern_predicate(self) -> Rule:
return seq(AND, self.expression)
@rule @rule
def _pattern_core(self) -> Rule: def _pattern_core(self) -> Rule:
return self.type_expression | self.wildcard_pattern return self.type_expression | self.wildcard_pattern

View file

@ -7,6 +7,7 @@ import os
import select import select
import sys import sys
import termios import termios
import textwrap
import time import time
import traceback import traceback
import tty import tty
@ -390,8 +391,10 @@ class Harness:
for line in lines[: rows - 3]: for line in lines[: rows - 3]:
print(line[:cols] + "\r") print(line[:cols] + "\r")
else: else:
for error in self.errors[: rows - 3]: wrapper = textwrap.TextWrapper(width=cols)
print(error[:cols] + "\r") lines = [line for error in self.errors for line in wrapper.wrap(error)]
for line in lines[: rows - 3]:
print(line + "\r")
sys.stdout.flush() sys.stdout.flush()
sys.stdout.buffer.flush() sys.stdout.buffer.flush()

View file

@ -420,6 +420,43 @@ class Error(Action):
pass pass
@dataclasses.dataclass
class PossibleAction:
name: str
rule: str
action_str: str
def __str__(self):
return f"We are in the rule `{self.name}: {self.rule}` and we should {self.action_str}"
@dataclasses.dataclass
class Ambiguity:
path: str
symbol: str
actions: typing.Tuple[PossibleAction]
def __str__(self):
lines = []
lines.append(
f"When we have parsed '{self.path}' and see '{self.symbol}' we don't know whether:"
)
lines.extend(f"- {action}" for action in self.actions)
return "\n".join(lines)
class AmbiguityError(Exception):
ambiguities: list[Ambiguity]
def __init__(self, ambiguities):
self.ambiguities = ambiguities
def __str__(self):
return "The grammar is ambiguous:\n\n" + "\n\n".join(
str(ambiguity) for ambiguity in self.ambiguities
)
class ErrorCollection: class ErrorCollection:
"""A collection of errors. The errors are grouped by config set and alphabet """A collection of errors. The errors are grouped by config set and alphabet
symbol, so that we can group the error strings appropriately when we format symbol, so that we can group the error strings appropriately when we format
@ -463,11 +500,11 @@ class ErrorCollection:
symbol_errors[config] = action symbol_errors[config] = action
def format( def gen_exception(
self, self,
alphabet: list[str], alphabet: list[str],
all_sets: ConfigurationSetInfo, all_sets: ConfigurationSetInfo,
) -> str | None: ) -> AmbiguityError | None:
"""Format all the errors into a string, or return None if there are no """Format all the errors into a string, or return None if there are no
errors. errors.
@ -475,7 +512,7 @@ class ErrorCollection:
readable, and all the sets to trace a path to where the errors were readable, and all the sets to trace a path to where the errors were
encountered. encountered.
""" """
if len(self.errors) is None: if len(self.errors) == 0:
return None return None
errors = [] errors = []
@ -484,10 +521,7 @@ class ErrorCollection:
path_str = " ".join(alphabet[s] for s in path) path_str = " ".join(alphabet[s] for s in path)
for symbol, symbol_errors in set_errors.items(): for symbol, symbol_errors in set_errors.items():
lines = [] actions = []
lines.append(
f"When we have parsed '{path_str}' and see '{alphabet[symbol]}' we don't know whether:"
)
for config, action in symbol_errors.items(): for config, action in symbol_errors.items():
name = alphabet[config.name] name = alphabet[config.name]
rule = " ".join( rule = " ".join(
@ -499,7 +533,7 @@ class ErrorCollection:
match action: match action:
case Reduce(name=name, count=count, transparent=transparent): case Reduce(name=name, count=count, transparent=transparent):
name_str = name if not transparent else "transparent node" name_str = name if not transparent else f"transparent node ({name})"
action_str = f"pop {count} values off the stack and make a {name_str}" action_str = f"pop {count} values off the stack and make a {name_str}"
case Shift(): case Shift():
action_str = "consume the token and keep going" action_str = "consume the token and keep going"
@ -508,13 +542,13 @@ class ErrorCollection:
case _: case _:
raise Exception(f"unknown action type {action}") raise Exception(f"unknown action type {action}")
lines.append( actions.append(PossibleAction(name, rule, action_str))
f" - We are in the rule `{name}: {rule}` and we should {action_str}"
errors.append(
Ambiguity(path=path_str, symbol=alphabet[symbol], actions=tuple(actions))
) )
errors.append("\n".join(lines)) return AmbiguityError(errors)
return "\n\n".join(errors)
@dataclasses.dataclass @dataclasses.dataclass
@ -607,9 +641,10 @@ class TableBuilder(object):
Raises ValueError if there were any conflicts during construction. Raises ValueError if there were any conflicts during construction.
""" """
self._flush_row() self._flush_row()
if self.errors.any(): error = self.errors.gen_exception(self.alphabet, all_sets)
errors = self.errors.format(self.alphabet, all_sets) if error is not None:
raise ValueError(f"Errors building the table:\n\n{errors}") raise error
return ParseTable(actions=self.actions, gotos=self.gotos) return ParseTable(actions=self.actions, gotos=self.gotos)
def new_row(self, config_set: ConfigSet): def new_row(self, config_set: ConfigSet):