From 324adff0cfa14e96c684529792b935eaa1cce621 Mon Sep 17 00:00:00 2001 From: Axy Date: Sun, 29 Mar 2026 04:48:22 +0200 Subject: [PATCH] Improved error messages further --- mazegen/config/config_parser.py | 71 +++++++++++++++++++---------- mazegen/config/parser_combinator.py | 57 ++++++++--------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/mazegen/config/config_parser.py b/mazegen/config/config_parser.py index 3d54558..dc505e5 100644 --- a/mazegen/config/config_parser.py +++ b/mazegen/config/config_parser.py @@ -34,14 +34,20 @@ from .parser_combinator import ( def parse_bool(s: str) -> ParseResult[bool]: - return alt( - value(True, tag("True")), - value(False, tag("False")), + return parser_map_err( + lambda e: ParseError("Expected boolean 'True' or 'False'", e.at), + alt( + value(True, tag("True")), + value(False, tag("False")), + ), )(s) def parse_int(s: str) -> ParseResult[int]: - return parser_map(int, recognize(many_count(ascii_digit, min_n=1)))(s) + return parser_map_err( + lambda e: ParseError("Expected integer literal", e.at), + parser_map(int, recognize(many_count(ascii_digit, min_n=1))), + )(s) def multispace0(s: str) -> ParseResult[str]: @@ -52,10 +58,6 @@ def parse_comment(s: str) -> ParseResult[str]: return recognize(seq(tag("#"), many_count(none_of("\n"))))(s) -# def parse_empty_line(s: str) -> ParseResult[None]: -# return (None, s) if s.startswith("\n") else ParseError("temp", "temp") - - def spaced[T](parser: Parser[T]) -> Parser[T]: return delimited(multispace0, parser, multispace0) @@ -87,7 +89,10 @@ def char_range(a: str, b: str) -> str: def parse_varname(s: str) -> ParseResult[str]: varstart = "_" + char_range("a", "z") + char_range("A", "Z") vartail = varstart + char_range("0", "9") - return recognize(seq(one_of(varstart), many_count(one_of(vartail))))(s) + return parser_map_err( + lambda e: ParseError("Expected color identifier", e.at), + recognize(seq(one_of(varstart), many_count(one_of(vartail)))), + )(s) type Color = tuple[int, int, int] | str @@ -98,16 +103,22 @@ type Grouped[T] = tuple[int, T] def parse_color(s: str) -> ParseResult[Color]: - return cast( - ParseResult[Color], - alt( - parser_map( - tuple, - many(parse_int, 3, 3, spaced(tag(","))), - ), - parse_varname, - )(s), + cut_comma = spaced( + cut(lookahead_parser(tag(","), seq(multispace0, parse_int))) ) + try: + return cast( + ParseResult[Color], + alt( + parser_map( + tuple, + many(parse_int, 3, 3, cut_comma), + ), + parse_varname, + )(s), + ) + except ParseError as e: + return e def parse_color_pair(s: str) -> ParseResult[ColorPair]: @@ -151,7 +162,12 @@ def parse_colored_line( return spaced( delimited( - tag('"'), many(pair(color_prefix, cut(noncolor_str))), tag('"') + tag('"'), + many(pair(color_prefix, cut(noncolor_str))), + parser_map_err( + lambda e: ParseError("Expected color prefix or '\"'", e.at), + tag('"'), + ), ) )(s) @@ -189,10 +205,7 @@ class ConfigField[T, U = T](ABC): def parse(self, s: str) -> ParseResult[T]: ... def default(self) -> U: - raise ConfigException( - f"Value {self.__name} not provided, " - + "and no default value exists" - ) + raise ConfigException(f"Value {self.__name} not provided") @abstractmethod def merge(self, vals: list[T]) -> U: ... @@ -336,7 +349,7 @@ def line_parser[T]( parser_map(lambda _: None, parse_comment), *( preceeded( - seq(tag(name), multispace0, cut(tag("=")), multispace0), + seq(tag(name), multispace0, tag("="), multispace0), parser_map( (lambda name: lambda res: (name, res))(name), cut(terminated(field.parse, multispace0)), @@ -356,7 +369,15 @@ def fields_parser( ) -> Parser[dict[str, Any]]: fields = {key: cls(key) for key, cls in fields_raw.items()} parse_line = nonempty_parser( - cut(terminated(line_parser(fields), alt(tag("\n"), eof_parser()))) + cut( + terminated( + line_parser(fields), + parser_map_err( + lambda e: ParseError("Expected newline or EOF", e.at), + alt(tag("\n"), eof_parser()), + ), + ) + ) ) def inner(s: str) -> ParseResult[dict[str, Any]]: diff --git a/mazegen/config/parser_combinator.py b/mazegen/config/parser_combinator.py index a8b0e15..a47843f 100644 --- a/mazegen/config/parser_combinator.py +++ b/mazegen/config/parser_combinator.py @@ -90,14 +90,8 @@ def parser_default[T](p: Parser[T], default: T) -> Parser[T]: def parser_complete[T]( p: Parser[T], - msg: str = "Complete parser error: leftover characters", ) -> Parser[T]: - def inner(res: tuple[T, str]) -> ParseResult[T]: - if len(res[1]) != 0: - raise ParseError(msg, res[1]) - return res - - return lambda s: error_map(inner, p(s)) + return terminated(p, eof_parser()) def recognize[T](p: Parser[T]) -> Parser[str]: @@ -117,20 +111,16 @@ def cut[T](p: Parser[T]) -> Parser[T]: return inner -def tag(tag: str, msg: str | None = None) -> Parser[str]: - if msg is None: - msg = f"Expected tag {repr(tag)}" +def tag(tag: str) -> Parser[str]: return lambda s: ( (s[: len(tag)], s[len(tag) :]) # noqa E203 if s.startswith(tag) - else ParseError(msg, s) + else ParseError(f"Expected tag {repr(tag)}", s) ) -def char(msg: str | None = None) -> Parser[str]: - if msg is None: - msg = f"Expected char {repr(tag)}" - return lambda s: (s[0], s[1:]) if len(s) > 0 else ParseError(msg, s) +def char(s: str) -> ParseResult[str]: + return (s[0], s[1:]) if len(s) > 0 else ParseError("Early EOF", s) def null_parser(s: str) -> ParseResult[str]: @@ -150,28 +140,28 @@ def lookahead_parser[T, U](p1: Parser[T], p2: Parser[U]) -> Parser[T]: return inner -def eof_parser(msg: str = "Expected end of file") -> Parser[str]: - return lambda s: ("", "") if len(s) == 0 else ParseError(msg, s) +def eof_parser() -> Parser[str]: + return lambda s: ( + ("", "") if len(s) == 0 else ParseError("Expected EOF", s) + ) -def nonempty_parser[T](p: Parser[T], msg: str = "Expected non end of file"): - return lambda s: p(s) if len(s) > 0 else ParseError(msg, s) +def nonempty_parser[T](p: Parser[T]) -> Parser[T]: + return lambda s: p(s) if len(s) > 0 else ParseError("Early EOF", s) def value[T, V](val: V, p: Parser[T]) -> Parser[V]: return parser_map(lambda _: val, p) -def alt[T]( - *choices: Parser[T], msg: str = "None of the following was met:" -) -> Parser[T]: +def alt[T](*choices: Parser[T]) -> Parser[T]: def inner(s: str) -> ParseResult[T]: acc: list[ParseError] = [] for e in map(lambda p: p(s), choices): if not isinstance(e, ParseError): return e acc.append(e) - return ParseError(msg, s, acc) + return ParseError("Expected any of the following to match:", s, acc) return inner @@ -183,7 +173,6 @@ def fold[T, R]( min_n: int = 0, max_n: int | None = None, sep: Parser[Any] = null_parser, - msg: Callable[[int], str] | None = None, ) -> Parser[R]: """ Repeatedly call the p parser, folding the results using f, with an acc @@ -191,31 +180,24 @@ def fold[T, R]( Returns error if and only if min_n iterations are not reached """ - if msg is None: - msg = ( - lambda count: f"Expected at least {min_n} elements, got {count}, after error:" - ) - # no clean way to do this with lambdas i could figure out :< def inner(s: str) -> ParseResult[R]: curr_s = s acc = acc_init() count: int = 0 curr_p: Parser[T] = p - err: list[ParseError] | None = None while max_n is None or count < max_n: nxt: ParseResult[T] = curr_p(curr_s) if isinstance(nxt, ParseError): - err = [nxt] + if count < min_n: + return nxt break if count == 0: curr_p = preceeded(sep, p) count += 1 acc = f(acc, nxt[0]) curr_s = nxt[1] - return ( - (acc, curr_s) if count >= min_n else ParseError(msg(count), s, err) - ) + return (acc, curr_s) return inner @@ -225,7 +207,6 @@ def many[T]( min_n: int = 0, max_n: int | None = None, sep: Parser[Any] = null_parser, - msg: Callable[[int], str] | None = None, ) -> Parser[list[T]]: return fold( parser_map(lambda e: [e], p), @@ -234,7 +215,6 @@ def many[T]( min_n, max_n, sep, - msg, ) @@ -243,9 +223,8 @@ def many_count[T]( min_n: int = 0, max_n: int | None = None, sep: Parser[Any] = null_parser, - msg: Callable[[int], str] | None = None, ) -> Parser[int]: - return fold(value(1, p), int.__add__, lambda: 0, min_n, max_n, sep, msg) + return fold(value(1, p), int.__add__, lambda: 0, min_n, max_n, sep) def seq[T](*parsers: Parser[T]) -> Parser[str]: @@ -292,7 +271,7 @@ def one_of(chars: str) -> Parser[str]: def none_of(chars: str) -> Parser[str]: return lambda s: ( - char()(s) + char(s) if isinstance(one_of(chars)(s), ParseError) else ParseError(f"Expected any character except {repr(chars)}", s) ) -- 2.53.0