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

@@ -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,