Implement reader macros (#;, #|...|, #', #name) and #z3 demo
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m13s

Reader macros in parser.sx spec, Python parser.py, and hand-written sx.js:
- #; datum comment: read and discard next expression
- #|...|  raw string: no escape processing
- #' quote shorthand: (quote expr)
- #name extensible dispatch: registered handler transforms next expression

#z3 reader macro demo (reader_z3.py): translates define-primitive
declarations from primitives.sx into SMT-LIB verification conditions.
Same source, two interpretations — bootstrappers compile to executable
code, #z3 extracts proof obligations.

48 parser tests (SX spec + Python), all passing. Rebootstrapped JS+Python.
Demo page at /plans/reader-macro-demo with side-by-side examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 20:21:40 +00:00
parent 56589a81b2
commit 03ba8e58e5
12 changed files with 7625 additions and 26 deletions

View File

@@ -20,6 +20,17 @@ from typing import Any
from .types import Keyword, Symbol, NIL
# ---------------------------------------------------------------------------
# Reader macro registry
# ---------------------------------------------------------------------------
_READER_MACROS: dict[str, Any] = {}
def register_reader_macro(name: str, handler: Any) -> None:
"""Register a reader macro handler: #name expr → handler(expr)."""
_READER_MACROS[name] = handler
# ---------------------------------------------------------------------------
# SxExpr — pre-built sx source marker
@@ -203,8 +214,33 @@ class Tokenizer:
return NIL
return Symbol(name)
# Reader macro dispatch: #
if char == "#":
return "#"
raise ParseError(f"Unexpected character: {char!r}", self.pos, self.line, self.col)
def _read_raw_string(self) -> str:
"""Read raw string literal until closing |."""
buf: list[str] = []
while self.pos < len(self.text):
ch = self.text[self.pos]
if ch == "|":
self._advance(1)
return "".join(buf)
buf.append(ch)
self._advance(1)
raise ParseError("Unterminated raw string", self.pos, self.line, self.col)
def _read_ident(self) -> str:
"""Read an identifier (for reader macro names)."""
import re
m = self.SYMBOL.match(self.text, self.pos)
if m:
self._advance(m.end() - self.pos)
return m.group()
raise ParseError("Expected identifier after #", self.pos, self.line, self.col)
# ---------------------------------------------------------------------------
# Parsing
@@ -264,6 +300,33 @@ def _parse_expr(tok: Tokenizer) -> Any:
return [Symbol("splice-unquote"), inner]
inner = _parse_expr(tok)
return [Symbol("unquote"), inner]
# Reader macro dispatch: #
if raw == "#":
tok._advance(1) # consume the #
if tok.pos >= len(tok.text):
raise ParseError("Unexpected end of input after #",
tok.pos, tok.line, tok.col)
dispatch = tok.text[tok.pos]
if dispatch == ";":
tok._advance(1)
_parse_expr(tok) # read and discard
return _parse_expr(tok) # return next
if dispatch == "|":
tok._advance(1)
return tok._read_raw_string()
if dispatch == "'":
tok._advance(1)
return [Symbol("quote"), _parse_expr(tok)]
# Extensible dispatch: #name expr
if dispatch.isalpha() or dispatch in "_~":
macro_name = tok._read_ident()
handler = _READER_MACROS.get(macro_name)
if handler is None:
raise ParseError(f"Unknown reader macro: #{macro_name}",
tok.pos, tok.line, tok.col)
return handler(_parse_expr(tok))
raise ParseError(f"Unknown reader macro: #{dispatch}",
tok.pos, tok.line, tok.col)
# Everything else: strings, keywords, symbols, numbers
token = tok.next_token()
return token