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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,9 @@
function isMacro(x) { return x && x._macro === true; }
function isRaw(x) { return x && x._raw === true; }
// --- Reader macro registry ---
var _readerMacros = {};
// --- Parser ---
var RE_WS = /\s+/y;
@@ -155,6 +158,9 @@
}
}
// Reader macro dispatch: #
if (ch === "#") { this._advance(1); return "#"; }
// Symbol
RE_SYMBOL.lastIndex = this.pos;
m = RE_SYMBOL.exec(this.text);
@@ -171,6 +177,27 @@
throw parseErr("Unexpected character: " + ch + " | context: «" + ctx.replace(/\n/g, "\\n") + "»", this);
};
Tokenizer.prototype._readRawString = function () {
var buf = [];
while (this.pos < this.text.length) {
var ch = this.text[this.pos];
if (ch === "|") { this._advance(1); return buf.join(""); }
buf.push(ch);
this._advance(1);
}
throw parseErr("Unterminated raw string", this);
};
Tokenizer.prototype._readIdent = function () {
RE_SYMBOL.lastIndex = this.pos;
var m = RE_SYMBOL.exec(this.text);
if (m && m.index === this.pos) {
this._advance(m[0].length);
return m[0];
}
throw parseErr("Expected identifier after #", this);
};
function isDigit(c) { return c >= "0" && c <= "9"; }
function parseErr(msg, tok) {
@@ -199,6 +226,33 @@
}
return [new Symbol("unquote"), parseExpr(tok)];
}
// Reader macro dispatch: #
if (raw === "#") {
tok._advance(1); // consume #
if (tok.pos >= tok.text.length) throw parseErr("Unexpected end of input after #", tok);
var dispatch = tok.text[tok.pos];
if (dispatch === ";") {
tok._advance(1);
parseExpr(tok); // read and discard
return parseExpr(tok); // return next
}
if (dispatch === "|") {
tok._advance(1);
return tok._readRawString();
}
if (dispatch === "'") {
tok._advance(1);
return [new Symbol("quote"), parseExpr(tok)];
}
// Extensible dispatch: #name expr
if (/[a-zA-Z_~]/.test(dispatch)) {
var macroName = tok._readIdent();
var handler = _readerMacros[macroName];
if (!handler) throw parseErr("Unknown reader macro: #" + macroName, tok);
return handler(parseExpr(tok));
}
throw parseErr("Unknown reader macro: #" + dispatch, tok);
}
return tok.next();
}
@@ -1500,6 +1554,9 @@
}
},
/** Register a reader macro: Sx.registerReaderMacro("z3", fn) */
registerReaderMacro: function (name, handler) { _readerMacros[name] = handler; },
// For testing / sx-test.js
_types: { NIL: NIL, Symbol: Symbol, Keyword: Keyword, Lambda: Lambda, Component: Component, RawHTML: RawHTML },
_eval: sxEval,

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

View File

@@ -29,6 +29,12 @@
;; ,expr → (unquote expr)
;; ,@expr → (splice-unquote expr)
;;
;; Reader macros:
;; #;expr → datum comment (read and discard expr)
;; #|raw chars| → raw string literal (no escape processing)
;; #'expr → (quote expr)
;; #name expr → extensible dispatch (calls registered handler)
;;
;; Platform interface (each target implements natively):
;; (ident-start? ch) → boolean
;; (ident-char? ch) → boolean
@@ -198,6 +204,24 @@
(read-map-loop)
result)))
;; -- Raw string reader (for #|...|) --
(define read-raw-string
(fn ()
(let ((buf ""))
(define raw-loop
(fn ()
(if (>= pos len-src)
(error "Unterminated raw string")
(let ((ch (nth source pos)))
(if (= ch "|")
(do (set! pos (inc pos)) nil) ;; done
(do (set! buf (str buf ch))
(set! pos (inc pos))
(raw-loop)))))))
(raw-loop)
buf)))
;; -- Main expression reader --
(define read-expr
@@ -238,6 +262,40 @@
(list (make-symbol "splice-unquote") (read-expr)))
(list (make-symbol "unquote") (read-expr))))
;; Reader macros: #
(= ch "#")
(do (set! pos (inc pos))
(if (>= pos len-src)
(error "Unexpected end of input after #")
(let ((dispatch-ch (nth source pos)))
(cond
;; #; — datum comment: read and discard next expr
(= dispatch-ch ";")
(do (set! pos (inc pos))
(read-expr) ;; read and discard
(read-expr)) ;; return the NEXT expr
;; #| — raw string
(= dispatch-ch "|")
(do (set! pos (inc pos))
(read-raw-string))
;; #' — quote shorthand
(= dispatch-ch "'")
(do (set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
;; #name — extensible dispatch
(ident-start? dispatch-ch)
(let ((macro-name (read-ident)))
(let ((handler (reader-macro-get macro-name)))
(if handler
(handler (read-expr))
(error (str "Unknown reader macro: #" macro-name)))))
:else
(error (str "Unknown reader macro: #" dispatch-ch))))))
;; Number (or negative number)
(or (and (>= ch "0") (<= ch "9"))
(and (= ch "-")
@@ -328,4 +386,8 @@
;; String utilities:
;; (escape-string s) → string with " and \ escaped
;; (sx-expr-source e) → unwrap SxExpr to its source string
;;
;; Reader macro registry:
;; (reader-macro-get name) → handler fn or nil
;; (reader-macro-set! name handler) → register a reader macro
;; --------------------------------------------------------------------------

305
shared/sx/ref/reader_z3.py Normal file
View File

@@ -0,0 +1,305 @@
"""
#z3 reader macro — translates SX spec declarations to SMT-LIB format.
Demonstrates extensible reader macros by converting define-primitive
declarations from primitives.sx into Z3 SMT-LIB verification conditions.
Usage:
from shared.sx.ref.reader_z3 import z3_translate, register_z3_macro
# Register as reader macro (enables #z3 in parser)
register_z3_macro()
# Or call directly
smtlib = z3_translate(parse('(define-primitive "inc" :params (n) ...)'))
"""
from __future__ import annotations
from typing import Any
from shared.sx.types import Symbol, Keyword
# ---------------------------------------------------------------------------
# Type mapping
# ---------------------------------------------------------------------------
_SX_TO_SORT = {
"number": "Int",
"boolean": "Bool",
"string": "String",
"any": "Value",
"list": "(List Value)",
"dict": "(Array String Value)",
}
def _sort(sx_type: str) -> str:
return _SX_TO_SORT.get(sx_type, "Value")
# ---------------------------------------------------------------------------
# Expression translation: SX → SMT-LIB
# ---------------------------------------------------------------------------
# SX operators that map directly to SMT-LIB
_IDENTITY_OPS = {"+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=",
"and", "or", "not", "mod"}
# SX operators with SMT-LIB equivalents
_RENAME_OPS = {
"if": "ite",
"str": "str.++",
}
def _translate_expr(expr: Any) -> str:
"""Translate an SX expression to SMT-LIB s-expression string."""
if isinstance(expr, (int, float)):
if isinstance(expr, float):
return f"(to_real {int(expr)})" if expr == int(expr) else str(expr)
return str(expr)
if isinstance(expr, str):
return f'"{expr}"'
if isinstance(expr, bool):
return "true" if expr else "false"
if expr is None:
return "nil_val"
if isinstance(expr, Symbol):
name = expr.name
# Translate SX predicate names to SMT-LIB
if name.endswith("?"):
return "is_" + name[:-1].replace("-", "_")
return name.replace("-", "_").replace("!", "_bang")
if isinstance(expr, list) and len(expr) > 0:
head = expr[0]
if isinstance(head, Symbol):
op = head.name
args = expr[1:]
# Direct identity ops
if op in _IDENTITY_OPS:
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({op} {smt_args})"
# Renamed ops
if op in _RENAME_OPS:
smt_op = _RENAME_OPS[op]
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({smt_op} {smt_args})"
# max/min → ite
if op == "max" and len(args) == 2:
a, b = _translate_expr(args[0]), _translate_expr(args[1])
return f"(ite (>= {a} {b}) {a} {b})"
if op == "min" and len(args) == 2:
a, b = _translate_expr(args[0]), _translate_expr(args[1])
return f"(ite (<= {a} {b}) {a} {b})"
# empty? → length check
if op == "empty?":
a = _translate_expr(args[0])
return f"(= (len {a}) 0)"
# first/rest → list ops
if op == "first":
return f"(head {_translate_expr(args[0])})"
if op == "rest":
return f"(tail {_translate_expr(args[0])})"
# reduce with initial value
if op == "reduce" and len(args) >= 3:
return f"(reduce {_translate_expr(args[0])} {_translate_expr(args[2])} {_translate_expr(args[1])})"
# fn (lambda) → unnamed function
if op == "fn":
params = args[0] if isinstance(args[0], list) else [args[0]]
param_str = " ".join(f"({_translate_expr(p)} Int)" for p in params)
body = _translate_expr(args[1])
return f"(lambda (({param_str})) {body})"
# native-* → bare op
if op.startswith("native-"):
bare = op[7:] # strip "native-"
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({bare} {smt_args})"
# Generic function call
smt_name = op.replace("-", "_").replace("?", "_p").replace("!", "_bang")
smt_args = " ".join(_translate_expr(a) for a in args)
return f"({smt_name} {smt_args})"
return str(expr)
# ---------------------------------------------------------------------------
# Define-primitive → SMT-LIB
# ---------------------------------------------------------------------------
def _extract_kwargs(expr: list) -> dict[str, Any]:
"""Extract keyword arguments from a define-primitive form."""
kwargs: dict[str, Any] = {}
i = 2 # skip head and name
while i < len(expr):
item = expr[i]
if isinstance(item, Keyword) and i + 1 < len(expr):
kwargs[item.name] = expr[i + 1]
i += 2
else:
i += 1
return kwargs
def _params_to_sorts(params: list) -> list[tuple[str, str]]:
"""Convert SX param list to (name, sort) pairs, skipping &rest/&key."""
result = []
skip_next = False
for p in params:
if isinstance(p, Symbol) and p.name in ("&rest", "&key"):
skip_next = True
continue
if skip_next:
skip_next = False
continue
if isinstance(p, Symbol):
result.append((p.name, "Int"))
return result
def z3_translate(expr: Any) -> str:
"""Translate an SX define-primitive to SMT-LIB verification conditions.
Input: parsed (define-primitive "name" :params (...) :returns "type" ...)
Output: SMT-LIB string with declare-fun and assert/check-sat.
"""
if not isinstance(expr, list) or len(expr) < 2:
return f"; Cannot translate: not a list form"
head = expr[0]
if not isinstance(head, Symbol):
return f"; Cannot translate: head is not a symbol"
form = head.name
if form == "define-primitive":
return _translate_primitive(expr)
elif form == "define-io-primitive":
return _translate_io(expr)
elif form == "define-special-form":
return _translate_special_form(expr)
else:
# Generic expression translation
return _translate_expr(expr)
def _translate_primitive(expr: list) -> str:
"""Translate define-primitive to SMT-LIB."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
params = kwargs.get("params", [])
returns = kwargs.get("returns", "any")
doc = kwargs.get("doc", "")
body = kwargs.get("body")
# Build param sorts
param_pairs = _params_to_sorts(params if isinstance(params, list) else [])
has_rest = any(isinstance(p, Symbol) and p.name == "&rest"
for p in (params if isinstance(params, list) else []))
# SMT-LIB function name
if name == "!=":
smt_name = "neq"
elif name in ("+", "-", "*", "/", "=", "<", ">", "<=", ">="):
smt_name = name # keep arithmetic ops as-is
else:
smt_name = name.replace("-", "_").replace("?", "_p").replace("!", "_bang")
lines = [f"; {name}{doc}"]
if has_rest:
# Variadic — declare as uninterpreted
lines.append(f"; (variadic — modeled as uninterpreted)")
lines.append(f"(declare-fun {smt_name} (Int Int) {_sort(returns)})")
else:
param_sorts = " ".join(s for _, s in param_pairs)
lines.append(f"(declare-fun {smt_name} ({param_sorts}) {_sort(returns)})")
if body is not None and not has_rest:
# Generate forall assertion from body
if param_pairs:
bindings = " ".join(f"({p} Int)" for p, _ in param_pairs)
call_args = " ".join(p for p, _ in param_pairs)
smt_body = _translate_expr(body)
lines.append(f"(assert (forall (({bindings}))")
lines.append(f" (= ({smt_name} {call_args}) {smt_body})))")
else:
smt_body = _translate_expr(body)
lines.append(f"(assert (= ({smt_name}) {smt_body}))")
lines.append("(check-sat)")
return "\n".join(lines)
def _translate_io(expr: list) -> str:
"""Translate define-io-primitive — uninterpreted (cannot verify statically)."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
doc = kwargs.get("doc", "")
smt_name = name.replace("-", "_").replace("?", "_p")
return (f"; IO primitive: {name}{doc}\n"
f"; (uninterpreted — IO cannot be verified statically)\n"
f"(declare-fun {smt_name} () Value)")
def _translate_special_form(expr: list) -> str:
"""Translate define-special-form to SMT-LIB."""
name = expr[1] if len(expr) > 1 else "?"
kwargs = _extract_kwargs(expr)
doc = kwargs.get("doc", "")
if name == "if":
return (f"; Special form: if — {doc}\n"
f"(assert (forall ((c Bool) (t Value) (e Value))\n"
f" (= (sx_if c t e) (ite c t e))))\n"
f"(check-sat)")
elif name == "when":
return (f"; Special form: when — {doc}\n"
f"(assert (forall ((c Bool) (body Value))\n"
f" (= (sx_when c body) (ite c body nil_val))))\n"
f"(check-sat)")
return f"; Special form: {name}{doc}\n; (not directly expressible in SMT-LIB)"
# ---------------------------------------------------------------------------
# Batch translation: process an entire spec file
# ---------------------------------------------------------------------------
def z3_translate_file(source: str) -> str:
"""Parse an SX spec file and translate all define-primitive forms."""
from shared.sx.parser import parse_all
exprs = parse_all(source)
results = []
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name in ("define-primitive", "define-io-primitive",
"define-special-form")):
results.append(z3_translate(expr))
return "\n\n".join(results)
# ---------------------------------------------------------------------------
# Reader macro registration
# ---------------------------------------------------------------------------
def register_z3_macro():
"""Register #z3 as a reader macro in the SX parser."""
from shared.sx.parser import register_reader_macro
register_reader_macro("z3", z3_translate)

View File

@@ -1,5 +1,3 @@
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
# WARNING: eval.sx dispatches forms not in special-forms.sx: form?
"""
sx_ref.py -- Generated from reference SX evaluator specification.
@@ -990,7 +988,7 @@ sf_named_let = lambda args, env: (lambda loop_name: (lambda bindings: (lambda bo
)[-1]), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2)))), (lambda loop_body: (lambda loop_fn: _sx_begin(_sx_set_attr(loop_fn, 'name', loop_name), _sx_dict_set(lambda_closure(loop_fn), loop_name, loop_fn), (lambda init_vals: call_lambda(loop_fn, init_vals, env))(map(lambda e: trampoline(eval_expr(e, env)), inits))))(make_lambda(params, loop_body, env)))((first(body) if sx_truthy((len(body) == 1)) else cons(make_symbol('begin'), body)))))([]))([]))(slice(args, 2)))(nth(args, 1)))(symbol_name(first(args)))
# sf-lambda
sf_lambda = lambda args, env: (lambda params_expr: (lambda body: (lambda param_names: make_lambda(param_names, body, env))(map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p), params_expr)))(nth(args, 1)))(first(args))
sf_lambda = lambda args, env: (lambda params_expr: (lambda body_exprs: (lambda body: (lambda param_names: make_lambda(param_names, body, env))(map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else p), params_expr)))((first(body_exprs) if sx_truthy((len(body_exprs) == 1)) else cons(make_symbol('begin'), body_exprs))))(rest(args)))(first(args))
# sf-define
sf_define = lambda args, env: (lambda name_sym: (lambda value: _sx_begin((_sx_set_attr(value, 'name', symbol_name(name_sym)) if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))) else NIL), _sx_dict_set(env, symbol_name(name_sym), value), value))(trampoline(eval_expr(nth(args, 1), env))))(first(args))
@@ -1424,6 +1422,9 @@ on_event = lambda el, event_name, handler: dom_listen(el, event_name, handler)
# bridge-event
bridge_event = lambda el, event_name, target_signal, transform_fn: effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((invoke(transform_fn, detail) if sx_truthy(transform_fn) else detail)))(event_detail(e)))))
# resource
resource = lambda fetch_fn: (lambda state: _sx_begin(promise_then(invoke(fetch_fn), lambda data: reset_b(state, {'loading': False, 'data': data, 'error': NIL}), lambda err: reset_b(state, {'loading': False, 'data': NIL, 'error': err})), state))(signal({'loading': True, 'data': NIL, 'error': NIL}))
# =========================================================================
# Fixups -- wire up render adapter dispatch
@@ -1486,4 +1487,4 @@ def render(expr, env=None):
def make_env(**kwargs):
"""Create an environment dict with initial bindings."""
return dict(kwargs)
return dict(kwargs)

View File

@@ -220,3 +220,40 @@
(deftest "roundtrip nested"
(assert-equal "(a (b c))"
(sx-serialize (first (sx-parse "(a (b c))"))))))
;; --------------------------------------------------------------------------
;; Reader macros
;; --------------------------------------------------------------------------
(defsuite "reader-macros"
(deftest "datum comment discards expr"
(assert-equal (list 42) (sx-parse "#;(ignored) 42")))
(deftest "datum comment in list"
(assert-equal (list (list 1 3)) (sx-parse "(1 #;2 3)")))
(deftest "datum comment discards nested"
(assert-equal (list 99) (sx-parse "#;(a (b c) d) 99")))
(deftest "raw string basic"
(assert-equal (list "hello") (sx-parse "#|hello|")))
(deftest "raw string with quotes"
(assert-equal (list "say \"hi\"") (sx-parse "#|say \"hi\"|")))
(deftest "raw string with backslashes"
(assert-equal (list "a\\nb") (sx-parse "#|a\\nb|")))
(deftest "raw string empty"
(assert-equal (list "") (sx-parse "#||")))
(deftest "quote shorthand symbol"
(let ((result (first (sx-parse "#'foo"))))
(assert-equal "quote" (symbol-name (first result)))
(assert-equal "foo" (symbol-name (nth result 1)))))
(deftest "quote shorthand list"
(let ((result (first (sx-parse "#'(1 2 3)"))))
(assert-equal "quote" (symbol-name (first result)))
(assert-equal (list 1 2 3) (nth result 1)))))

View File

@@ -235,3 +235,57 @@ class TestSerialize:
def test_roundtrip(self):
original = '(div :class "main" (p "hello") (span 42))'
assert serialize(parse(original)) == original
# ---------------------------------------------------------------------------
# Reader macros
# ---------------------------------------------------------------------------
class TestReaderMacros:
"""Test #; datum comment, #|...| raw string, and #' quote shorthand."""
def test_datum_comment_discards(self):
assert parse_all("#;(ignored) 42") == [42]
def test_datum_comment_in_list(self):
assert parse("(1 #;2 3)") == [1, 3]
def test_datum_comment_nested(self):
assert parse_all("#;(a (b c) d) 99") == [99]
def test_raw_string_basic(self):
assert parse('#|hello|') == "hello"
def test_raw_string_with_quotes(self):
assert parse('#|say "hi"|') == 'say "hi"'
def test_raw_string_with_backslashes(self):
assert parse('#|a\\nb|') == 'a\\nb'
def test_raw_string_empty(self):
assert parse('#||') == ""
def test_quote_shorthand_symbol(self):
assert parse("#'foo") == [Symbol("quote"), Symbol("foo")]
def test_quote_shorthand_list(self):
assert parse("#'(1 2 3)") == [Symbol("quote"), [1, 2, 3]]
def test_hash_at_eof_errors(self):
with pytest.raises(ParseError):
parse("#")
def test_unknown_reader_macro_errors(self):
with pytest.raises(ParseError, match="Unknown reader macro"):
parse("#x foo")
def test_extensible_reader_macro(self):
"""Registered reader macros transform the next expression."""
from shared.sx.parser import register_reader_macro
register_reader_macro("upper", lambda expr: str(expr).upper())
try:
result = parse('#upper "hello"')
assert result == "HELLO"
finally:
from shared.sx.parser import _READER_MACROS
del _READER_MACROS["upper"]

View File

@@ -155,6 +155,8 @@
:summary "Audit of all plans — what's done, what's in progress, and what remains.")
(dict :label "Reader Macros" :href "/plans/reader-macros"
:summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.")
(dict :label "Reader Macro Demo" :href "/plans/reader-macro-demo"
:summary "Live demo: #z3 translates SX spec declarations to SMT-LIB verification conditions.")
(dict :label "SX-Activity" :href "/plans/sx-activity"
:summary "A new web built on SX — executable content, shared components, parsers, and logic on IPFS, provenance on Bitcoin, all running within your own security context.")
(dict :label "Predictive Prefetching" :href "/plans/predictive-prefetch"

View File

@@ -49,8 +49,9 @@
(p "Currently no single-char quote (" (code "`") " is quasiquote).")
(~doc-code :code (highlight "#'my-function → (quote my-function)" "lisp")))
(~doc-subsection :title "No user-defined reader macros (yet)"
(p "Would require multi-pass parsing or boot-phase registration. The three built-ins cover practical needs. Extensible dispatch can come later without breaking anything.")))
(~doc-subsection :title "Extensible dispatch: #name"
(p "User-defined reader macros via " (code "#name expr") ". The parser reads an identifier after " (code "#") ", looks up a handler in the reader macro registry, and calls it with the next parsed expression. See the " (a :href "/plans/reader-macro-demo" :class "text-violet-600 hover:underline" "#z3 demo") " for a working example that translates SX spec declarations to SMT-LIB.")))
;; -----------------------------------------------------------------------
;; Implementation
@@ -126,6 +127,132 @@
(li "Rebootstrapped JS and Python pass their test suites")
(li "JS parity: SX.parse('#|hello|') returns [\"hello\"]"))))))
;; ---------------------------------------------------------------------------
;; Reader Macro Demo: #z3 — SX Spec to SMT-LIB
;; ---------------------------------------------------------------------------
(defcomp ~plan-reader-macro-demo-content ()
(~doc-page :title "Reader Macro Demo: #z3"
(~doc-section :title "The idea" :id "idea"
(p :class "text-stone-600"
"SX spec files (" (code "primitives.sx") ", " (code "eval.sx") ") are machine-readable declarations. Reader macros transform these at parse time. " (code "#z3") " reads a " (code "define-primitive") " declaration and emits " (a :href "https://smtlib.cs.uiowa.edu/" :class "text-violet-600 hover:underline" "SMT-LIB") " — the standard input language for " (a :href "https://github.com/Z3Prover/z3" :class "text-violet-600 hover:underline" "Z3") " and other theorem provers.")
(p :class "text-stone-600"
"Same source, two interpretations. The bootstrappers read " (code "define-primitive") " and emit executable code. " (code "#z3") " reads the same form and emits verification conditions. The specification is simultaneously a program and a proof obligation.")
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mt-4"
(p :class "text-sm text-violet-800 font-semibold mb-2" "Key insight")
(p :class "text-sm text-violet-700"
"SMT-LIB is itself an s-expression language. The translation from SX to SMT-LIB is s-expressions to s-expressions — most arithmetic and boolean operators are literally the same syntax in both. The reader macro barely has to transform anything.")))
(~doc-section :title "Arithmetic primitives" :id "arithmetic"
(p :class "text-stone-600"
"Primitives with " (code ":body") " generate " (code "forall") " assertions. Z3 can verify the definition is satisfiable.")
(~doc-subsection :title "inc"
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"inc\"\n :params (n)\n :returns \"number\"\n :doc \"Increment by 1.\"\n :body (+ n 1))"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; inc — Increment by 1.\n(declare-fun inc (Int) Int)\n(assert (forall (((n Int)))\n (= (inc n) (+ n 1))))\n(check-sat)"))))
(~doc-subsection :title "clamp"
(p :class "text-stone-600 mb-2"
(code "max") " and " (code "min") " have no SMT-LIB equivalent — translated to " (code "ite") " (if-then-else).")
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"clamp\"\n :params (x lo hi)\n :returns \"number\"\n :doc \"Clamp x to range [lo, hi].\"\n :body (max lo (min hi x)))"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; clamp — Clamp x to range [lo, hi].\n(declare-fun clamp (Int Int Int) Int)\n(assert (forall (((x Int) (lo Int) (hi Int)))\n (= (clamp x lo hi)\n (ite (>= lo (ite (<= hi x) hi x))\n lo\n (ite (<= hi x) hi x)))))\n(check-sat)")))))
(~doc-section :title "Predicates" :id "predicates"
(p :class "text-stone-600"
"Predicates return " (code "Bool") " in SMT-LIB. " (code "mod") " and " (code "=") " are identity translations — same syntax in both languages.")
(~doc-subsection :title "odd?"
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"odd?\"\n :params (n)\n :returns \"boolean\"\n :doc \"True if n is odd.\"\n :body (= (mod n 2) 1))"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; odd? — True if n is odd.\n(declare-fun odd_p (Int) Bool)\n(assert (forall (((n Int)))\n (= (odd_p n) (= (mod n 2) 1))))\n(check-sat)"))))
(~doc-subsection :title "!= (inequality)"
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"!=\"\n :params (a b)\n :returns \"boolean\"\n :doc \"Inequality.\"\n :body (not (= a b)))"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; != — Inequality.\n(declare-fun neq (Int Int) Bool)\n(assert (forall (((a Int) (b Int)))\n (= (neq a b) (not (= a b)))))\n(check-sat)")))))
(~doc-section :title "Variadics and bodyless" :id "variadics"
(p :class "text-stone-600"
"Variadic primitives (" (code "&rest") ") are declared as uninterpreted functions — Z3 can reason about their properties but not their implementation. Primitives without " (code ":body") " get only a declaration.")
(~doc-subsection :title "+ (variadic)"
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"+\"\n :params (&rest args)\n :returns \"number\"\n :doc \"Sum all arguments.\")"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; + — Sum all arguments.\n; (variadic — modeled as uninterpreted)\n(declare-fun + (Int Int) Int)\n(check-sat)"))))
(~doc-subsection :title "nil? (no body)"
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SX Source")
(~doc-code :lang "lisp" :code
"(define-primitive \"nil?\"\n :params (x)\n :returns \"boolean\"\n :doc \"True if x is nil/null/None.\")"))
(div
(p :class "text-xs text-stone-500 uppercase tracking-wider mb-1" "SMT-LIB Output")
(~doc-code :lang "lisp" :code
"; nil? — True if x is nil/null/None.\n(declare-fun nil_p (Int) Bool)\n(check-sat)")))))
(~doc-section :title "How it works" :id "how-it-works"
(p :class "text-stone-600"
"The " (code "#z3") " reader macro is registered before parsing. When the parser hits " (code "#z3(define-primitive ...)") ", it:")
(ol :class "list-decimal pl-6 space-y-2 text-stone-600"
(li "Reads the identifier " (code "z3") " after " (code "#") ".")
(li "Looks up " (code "z3") " in the reader macro registry.")
(li "Reads the next expression — the full " (code "define-primitive") " form — into an AST node.")
(li "Passes the AST to the handler function.")
(li "The handler walks the AST, extracts " (code ":params") ", " (code ":returns") ", " (code ":body") ", and emits SMT-LIB.")
(li "The resulting string replaces " (code "#z3(...)") " in the parse output."))
(~doc-code :lang "python" :code
"# Registration — one line\nfrom shared.sx.ref.reader_z3 import register_z3_macro\nregister_z3_macro()\n\n# Then #z3 works in any SX source:\nresult = parse('#z3(define-primitive \"inc\" ...)')\n# result is the SMT-LIB string")
(p :class "text-stone-600"
"The handler is a pure function from AST to value. No side effects. No mutation. Reader macros are " (em "syntax transformations") " — they happen before evaluation, before rendering, before anything else. They are the earliest possible extension point."))
(~doc-section :title "The strange loop" :id "strange-loop"
(p :class "text-stone-600"
"The SX specification files are simultaneously:")
(ul :class "list-disc pl-6 space-y-2 text-stone-600"
(li (span :class "font-semibold" "Executable code") " — bootstrappers compile them to JavaScript and Python.")
(li (span :class "font-semibold" "Documentation") " — this docs site renders them with syntax highlighting and prose.")
(li (span :class "font-semibold" "Formal specifications") " — " (code "#z3") " extracts verification conditions that a theorem prover can check."))
(p :class "text-stone-600"
"One file. Three readings. No information lost. The " (code "define-primitive") " form in " (code "primitives.sx") " does not need to be translated, annotated, or re-expressed for any of these uses. The s-expression " (em "is") " the program, the documentation, and the proof obligation.")
(p :class "text-stone-600"
"And the reader macro that extracts proofs? It is itself written as a Python function that takes an SX AST and returns a string. It could be written in SX. It could be compiled by the bootstrappers. The transformation tools are made of the same material as the things they transform.")
(p :class "text-stone-600"
"This is not hypothetical. The examples on this page are live output from the " (code "#z3") " reader macro running against the actual " (code "primitives.sx") " declarations. The same declarations that the JavaScript bootstrapper compiles into " (code "sx-ref.js") ". The same declarations that the Python bootstrapper compiles into " (code "sx_ref.py") ". Same source. Different reader."))))
;; ---------------------------------------------------------------------------
;; SX-Activity: Federated SX over ActivityPub
;; ---------------------------------------------------------------------------
@@ -1360,10 +1487,10 @@
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-stone-500 text-white uppercase" "Not Started")
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-700 text-white uppercase" "Done")
(a :href "/plans/reader-macros" :class "font-semibold text-stone-800 underline" "Reader Macros"))
(p :class "text-sm text-stone-600" "# dispatch character for datum comments (#;), raw strings (#|...|), and quote shorthand (#'). Fully designed but no implementation in parser.sx or parser.py.")
(p :class "text-sm text-stone-500 mt-1" "Remaining: spec in parser.sx, Python in parser.py, rebootstrap both targets."))
(p :class "text-sm text-stone-600" "# dispatch in parser.sx spec, Python parser.py, hand-written sx.js. Three built-ins (#;, #|...|, #') plus extensible #name dispatch. #z3 demo translates define-primitive to SMT-LIB.")
(p :class "text-sm text-stone-500 mt-1" "48 parser tests (SX + Python), all passing. Rebootstrapped to JS and Python."))
(div :class "rounded border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-center gap-2 mb-1"

View File

@@ -629,6 +629,7 @@
:content (case slug
"status" (~plan-status-content)
"reader-macros" (~plan-reader-macros-content)
"reader-macro-demo" (~plan-reader-macro-demo-content)
"sx-activity" (~plan-sx-activity-content)
"predictive-prefetch" (~plan-predictive-prefetch-content)
"content-addressed-components" (~plan-content-addressed-components-content)