From 179631130cbbdf2e409c057c9315fc37bcc3d40b Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 7 Mar 2026 22:17:38 +0000 Subject: [PATCH] Fix parser escape ordering and prim_get for non-dict objects Parser: chained .replace() calls processed \n before \\, causing \\n to become a real newline. Replaced with character-by-character _unescape_string. Fixes 2 parser spec test failures. Primitives: prim_get only handled dict and list. Objects with .get() methods (like PageDef) returned None. Added hasattr fallback. Fixes 9 defpage spec test failures. All 259 spec tests now pass (was 244/259). Co-Authored-By: Claude Opus 4.6 --- shared/sx/parser.py | 24 +++++++++++++++++++----- shared/sx/primitives.py | 2 ++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/shared/sx/parser.py b/shared/sx/parser.py index 2e81c58..4017ec4 100644 --- a/shared/sx/parser.py +++ b/shared/sx/parser.py @@ -62,6 +62,24 @@ class SxExpr(str): # Errors # --------------------------------------------------------------------------- +_ESCAPE_MAP = {"n": "\n", "t": "\t", '"': '"', "\\": "\\", "/": "/"} + + +def _unescape_string(s: str) -> str: + """Process escape sequences in a parsed string, character by character.""" + out: list[str] = [] + i = 0 + while i < len(s): + if s[i] == "\\" and i + 1 < len(s): + nxt = s[i + 1] + out.append(_ESCAPE_MAP.get(nxt, nxt)) + i += 2 + else: + out.append(s[i]) + i += 1 + return "".join(out) + + class ParseError(Exception): """Error during s-expression parsing.""" @@ -141,11 +159,7 @@ class Tokenizer: raise ParseError("Unterminated string", self.pos, self.line, self.col) self._advance(m.end() - self.pos) content = m.group()[1:-1] - content = content.replace("\\n", "\n") - content = content.replace("\\t", "\t") - content = content.replace('\\"', '"') - content = content.replace("\\/", "/") - content = content.replace("\\\\", "\\") + content = _unescape_string(content) return content # Keyword diff --git a/shared/sx/primitives.py b/shared/sx/primitives.py index f3ff5a1..5f74c42 100644 --- a/shared/sx/primitives.py +++ b/shared/sx/primitives.py @@ -374,6 +374,8 @@ def prim_get(coll: Any, key: Any, default: Any = None) -> Any: return default if isinstance(coll, list): return coll[key] if 0 <= key < len(coll) else default + if hasattr(coll, "get"): + return coll.get(key, default) return default @register_primitive("len")