Implement reader macros (#;, #|...|, #', #name) and #z3 demo
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m13s
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:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
305
shared/sx/ref/reader_z3.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
137
sx/sx/plans.sx
137
sx/sx/plans.sx
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user