]> Untitled Git - axy/ft/a-maze-ing.git/commitdiff
Improved error messages further
authorAxy <gilliardmarthey.axel@gmail.com>
Sun, 29 Mar 2026 02:48:22 +0000 (04:48 +0200)
committerAxy <gilliardmarthey.axel@gmail.com>
Sun, 29 Mar 2026 02:48:22 +0000 (04:48 +0200)
mazegen/config/config_parser.py
mazegen/config/parser_combinator.py

index 3d545589f77e53227785cab1effa10cd9a41565e..dc505e58a74eeaddc4f605b2874e288a3fa4560e 100644 (file)
@@ -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]]:
index a8b0e151a182b540829e9d8817faf0338ef6803e..a47843f97d40e90cd2db113c70a557fbd6285ca3 100644 (file)
@@ -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)
     )