Make continuations an optional extension, add special-forms.sx, ellipsis parsing
- Both bootstrappers (JS + Python) now gate shift/reset behind --extensions continuations flag. Without it, using reset/shift errors at runtime. - JS bootstrapper: extracted Continuation/ShiftSignal types, sfReset/sfShift, continuation? primitive, and typeOf handling into CONTINUATIONS_JS constant. Extension wraps evalList, aserSpecial, and typeOf post-transpilation. - Python bootstrapper: added special-forms.sx validation cross-check against eval.sx dispatch, warns on mismatches. - Added shared/sx/ref/special-forms.sx: 36 declarative form specs with syntax, docs, tail-position, and examples. Used by bootstrappers for validation. - Added ellipsis (...) support to both parser.py and parser.sx spec. - Updated continuations essay to reflect optional extension architecture. - Updated specs page and nav with special-forms.sx entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,11 @@ class Tokenizer:
|
||||
return float(num_str)
|
||||
return int(num_str)
|
||||
|
||||
# Ellipsis (... as a symbol, used in spec declarations)
|
||||
if char == "." and self.text[self.pos:self.pos + 3] == "...":
|
||||
self._advance(3)
|
||||
return Symbol("...")
|
||||
|
||||
# Symbol
|
||||
m = self.SYMBOL.match(self.text, self.pos)
|
||||
if m:
|
||||
|
||||
@@ -180,8 +180,6 @@ class JSEmitter:
|
||||
"sf-quasiquote": "sfQuasiquote",
|
||||
"sf-thread-first": "sfThreadFirst",
|
||||
"sf-set!": "sfSetBang",
|
||||
"sf-reset": "sfReset",
|
||||
"sf-shift": "sfShift",
|
||||
"qq-expand": "qqExpand",
|
||||
"ho-map": "hoMap",
|
||||
"ho-map-indexed": "hoMapIndexed",
|
||||
@@ -1004,13 +1002,102 @@ ADAPTER_DEPS = {
|
||||
}
|
||||
|
||||
|
||||
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
CONTINUATIONS_JS = '''
|
||||
// =========================================================================
|
||||
// Extension: Delimited continuations (shift/reset)
|
||||
// =========================================================================
|
||||
|
||||
function Continuation(fn) { this.fn = fn; }
|
||||
Continuation.prototype._continuation = true;
|
||||
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
||||
|
||||
function ShiftSignal(kName, body, env) {
|
||||
this.kName = kName;
|
||||
this.body = body;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||
|
||||
var _resetResume = [];
|
||||
|
||||
function sfReset(args, env) {
|
||||
var body = args[0];
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} catch (e) {
|
||||
if (e instanceof ShiftSignal) {
|
||||
var sig = e;
|
||||
var cont = new Continuation(function(value) {
|
||||
if (value === undefined) value = NIL;
|
||||
_resetResume.push(value);
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} finally {
|
||||
_resetResume.pop();
|
||||
}
|
||||
});
|
||||
var sigEnv = merge(sig.env);
|
||||
sigEnv[sig.kName] = cont;
|
||||
return trampoline(evalExpr(sig.body, sigEnv));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sfShift(args, env) {
|
||||
if (_resetResume.length > 0) {
|
||||
return _resetResume[_resetResume.length - 1];
|
||||
}
|
||||
var kName = symbolName(args[0]);
|
||||
var body = args[1];
|
||||
throw new ShiftSignal(kName, body, env);
|
||||
}
|
||||
|
||||
// Wrap evalList to intercept reset/shift
|
||||
var _baseEvalList = evalList;
|
||||
evalList = function(expr, env) {
|
||||
var head = expr[0];
|
||||
if (isSym(head)) {
|
||||
var name = head.name;
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
}
|
||||
return _baseEvalList(expr, env);
|
||||
};
|
||||
|
||||
// Wrap aserSpecial to handle reset/shift in SX wire mode
|
||||
if (typeof aserSpecial === "function") {
|
||||
var _baseAserSpecial = aserSpecial;
|
||||
aserSpecial = function(name, expr, env) {
|
||||
if (name === "reset") return sfReset(expr.slice(1), env);
|
||||
if (name === "shift") return sfShift(expr.slice(1), env);
|
||||
return _baseAserSpecial(name, expr, env);
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap typeOf to recognize continuations
|
||||
var _baseTypeOf = typeOf;
|
||||
typeOf = function(x) {
|
||||
if (x != null && x._continuation) return "continuation";
|
||||
return _baseTypeOf(x);
|
||||
};
|
||||
'''
|
||||
|
||||
|
||||
def compile_ref_to_js(adapters: list[str] | None = None,
|
||||
extensions: list[str] | None = None) -> str:
|
||||
"""Read reference .sx files and emit JavaScript.
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx, dom, engine.
|
||||
None = include all adapters.
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
"""
|
||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
emitter = JSEmitter()
|
||||
@@ -1057,6 +1144,15 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
defines = extract_defines(src)
|
||||
all_sections.append((label, defines))
|
||||
|
||||
# Resolve extensions
|
||||
ext_set = set()
|
||||
if extensions:
|
||||
for e in extensions:
|
||||
if e not in EXTENSION_NAMES:
|
||||
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
|
||||
ext_set.add(e)
|
||||
has_continuations = "continuations" in ext_set
|
||||
|
||||
# Build output
|
||||
has_html = "html" in adapter_set
|
||||
has_sx = "sx" in adapter_set
|
||||
@@ -1091,6 +1187,8 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
|
||||
parts.append(adapter_platform[name])
|
||||
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label))
|
||||
parts.append(EPILOGUE)
|
||||
return "\n".join(parts)
|
||||
@@ -1170,16 +1268,6 @@ PREAMBLE = '''\
|
||||
}
|
||||
StyleValue.prototype._styleValue = true;
|
||||
|
||||
function Continuation(fn) { this.fn = fn; }
|
||||
Continuation.prototype._continuation = true;
|
||||
Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); };
|
||||
|
||||
function ShiftSignal(kName, body, env) {
|
||||
this.kName = kName;
|
||||
this.body = body;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -1217,7 +1305,6 @@ PLATFORM_JS = '''
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (x._continuation) return "continuation";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
@@ -1389,7 +1476,6 @@ PLATFORM_JS = '''
|
||||
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
|
||||
PRIMITIVES["list?"] = Array.isArray;
|
||||
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
|
||||
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
|
||||
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
|
||||
PRIMITIVES["contains?"] = function(c, k) {
|
||||
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
|
||||
@@ -1579,7 +1665,7 @@ PLATFORM_JS = '''
|
||||
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
|
||||
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
|
||||
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1,"reset":1,"shift":1
|
||||
"quote":1,"quasiquote":1,"->":1,"set!":1
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
"map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1
|
||||
@@ -2659,44 +2745,6 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return _rawCallLambda(f, args, callerEnv);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Delimited continuations (shift/reset)
|
||||
// =========================================================================
|
||||
var _resetResume = []; // stack of resume values
|
||||
|
||||
function sfReset(args, env) {
|
||||
var body = args[0];
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} catch (e) {
|
||||
if (e instanceof ShiftSignal) {
|
||||
var sig = e;
|
||||
var cont = new Continuation(function(value) {
|
||||
if (value === undefined) value = NIL;
|
||||
_resetResume.push(value);
|
||||
try {
|
||||
return trampoline(evalExpr(body, env));
|
||||
} finally {
|
||||
_resetResume.pop();
|
||||
}
|
||||
});
|
||||
var sigEnv = merge(sig.env);
|
||||
sigEnv[sig.kName] = cont;
|
||||
return trampoline(evalExpr(sig.body, sigEnv));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sfShift(args, env) {
|
||||
if (_resetResume.length > 0) {
|
||||
return _resetResume[_resetResume.length - 1];
|
||||
}
|
||||
var kName = symbolName(args[0]);
|
||||
var body = args[1];
|
||||
throw new ShiftSignal(kName, body, env);
|
||||
}
|
||||
|
||||
// Expose render functions as primitives so SX code can call them''']
|
||||
if has_html:
|
||||
lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;')
|
||||
@@ -2883,18 +2931,22 @@ if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
|
||||
p.add_argument("--adapters", "-a",
|
||||
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
|
||||
p.add_argument("--extensions",
|
||||
help="Comma-separated extensions (continuations). Default: none.")
|
||||
p.add_argument("--output", "-o",
|
||||
help="Output file (default: stdout)")
|
||||
args = p.parse_args()
|
||||
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
js = compile_ref_to_js(adapters)
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
js = compile_ref_to_js(adapters, extensions)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w") as f:
|
||||
f.write(js)
|
||||
included = ", ".join(adapters) if adapters else "all"
|
||||
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included})",
|
||||
ext_label = ", ".join(extensions) if extensions else "none"
|
||||
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, extensions: {ext_label})",
|
||||
file=sys.stderr)
|
||||
else:
|
||||
print(js)
|
||||
|
||||
@@ -803,13 +803,110 @@ ADAPTER_FILES = {
|
||||
}
|
||||
|
||||
|
||||
def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
||||
EXTENSION_NAMES = {"continuations"}
|
||||
|
||||
# Extension-provided special forms (not in eval.sx core)
|
||||
EXTENSION_FORMS = {
|
||||
"continuations": {"reset", "shift"},
|
||||
}
|
||||
|
||||
|
||||
def _parse_special_forms_spec(ref_dir: str) -> set[str]:
|
||||
"""Parse special-forms.sx to extract declared form names."""
|
||||
filepath = os.path.join(ref_dir, "special-forms.sx")
|
||||
if not os.path.exists(filepath):
|
||||
return set()
|
||||
with open(filepath) as f:
|
||||
src = f.read()
|
||||
names = set()
|
||||
for expr in parse_all(src):
|
||||
if (isinstance(expr, list) and len(expr) >= 2
|
||||
and isinstance(expr[0], Symbol)
|
||||
and expr[0].name == "define-special-form"
|
||||
and isinstance(expr[1], str)):
|
||||
names.add(expr[1])
|
||||
return names
|
||||
|
||||
|
||||
def _extract_eval_dispatch_names(all_sections: list) -> set[str]:
|
||||
"""Extract special form names dispatched in eval-list from transpiled sections."""
|
||||
names = set()
|
||||
for _label, defines in all_sections:
|
||||
for name, _expr in defines:
|
||||
# sf-* functions correspond to dispatched special forms
|
||||
if name.startswith("sf-"):
|
||||
# sf-if → if, sf-set! → set!, sf-named-let → named-let
|
||||
form = name[3:]
|
||||
# Map back: sf_cond_scheme etc. are internal, skip
|
||||
if form in ("cond-scheme", "cond-clojure", "case-loop"):
|
||||
continue
|
||||
names.add(form)
|
||||
if name.startswith("ho-"):
|
||||
form = name[3:]
|
||||
names.add(form)
|
||||
return names
|
||||
|
||||
|
||||
def _validate_special_forms(ref_dir: str, all_sections: list,
|
||||
has_continuations: bool) -> None:
|
||||
"""Cross-check special-forms.sx against eval.sx dispatch. Warn on mismatches."""
|
||||
spec_names = _parse_special_forms_spec(ref_dir)
|
||||
if not spec_names:
|
||||
return # no spec file, skip validation
|
||||
|
||||
# Collect what eval.sx dispatches
|
||||
dispatch_names = _extract_eval_dispatch_names(all_sections)
|
||||
|
||||
# Add extension forms if enabled
|
||||
if has_continuations:
|
||||
dispatch_names |= EXTENSION_FORMS["continuations"]
|
||||
|
||||
# Normalize: eval.sx sf-* names don't always match form names directly
|
||||
# sf-thread-first → ->, sf-named-let is internal, ho-every → every?
|
||||
name_aliases = {
|
||||
"thread-first": "->",
|
||||
"every": "every?",
|
||||
"set-bang": "set!",
|
||||
}
|
||||
normalized_dispatch = set()
|
||||
for n in dispatch_names:
|
||||
normalized_dispatch.add(name_aliases.get(n, n))
|
||||
|
||||
# Internal helpers that aren't user-facing forms
|
||||
internal = {"named-let"}
|
||||
normalized_dispatch -= internal
|
||||
|
||||
# Forms in spec but not dispatched
|
||||
undispatched = spec_names - normalized_dispatch
|
||||
# Ignore aliases and domain forms that are handled differently
|
||||
ignore = {"fn", "let*", "do", "defrelation"}
|
||||
undispatched -= ignore
|
||||
|
||||
# Forms dispatched but not in spec
|
||||
unspecced = normalized_dispatch - spec_names
|
||||
unspecced -= ignore
|
||||
|
||||
if undispatched:
|
||||
import sys
|
||||
print(f"# WARNING: special-forms.sx declares forms not in eval.sx: "
|
||||
f"{', '.join(sorted(undispatched))}", file=sys.stderr)
|
||||
if unspecced:
|
||||
import sys
|
||||
print(f"# WARNING: eval.sx dispatches forms not in special-forms.sx: "
|
||||
f"{', '.join(sorted(unspecced))}", file=sys.stderr)
|
||||
|
||||
|
||||
def compile_ref_to_py(adapters: list[str] | None = None,
|
||||
extensions: list[str] | None = None) -> str:
|
||||
"""Read reference .sx files and emit Python.
|
||||
|
||||
Args:
|
||||
adapters: List of adapter names to include.
|
||||
Valid names: html, sx.
|
||||
None = include all server-side adapters.
|
||||
extensions: List of optional extensions to include.
|
||||
Valid names: continuations.
|
||||
None = no extensions.
|
||||
"""
|
||||
ref_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
emitter = PyEmitter()
|
||||
@@ -844,6 +941,18 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
||||
defines = extract_defines(src)
|
||||
all_sections.append((label, defines))
|
||||
|
||||
# Resolve extensions
|
||||
ext_set = set()
|
||||
if extensions:
|
||||
for e in extensions:
|
||||
if e not in EXTENSION_NAMES:
|
||||
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
|
||||
ext_set.add(e)
|
||||
has_continuations = "continuations" in ext_set
|
||||
|
||||
# Validate special forms
|
||||
_validate_special_forms(ref_dir, all_sections, has_continuations)
|
||||
|
||||
# Build output
|
||||
has_html = "html" in adapter_set
|
||||
has_sx = "sx" in adapter_set
|
||||
@@ -861,6 +970,8 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
|
||||
parts.append("")
|
||||
|
||||
parts.append(FIXUPS_PY)
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_PY)
|
||||
parts.append(public_api_py(has_html, has_sx))
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -1342,7 +1453,7 @@ _SPECIAL_FORM_NAMES = frozenset([
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!", "reset", "shift",
|
||||
"->", "set!",
|
||||
])
|
||||
|
||||
_HO_FORM_NAMES = frozenset([
|
||||
@@ -1505,11 +1616,6 @@ def aser_special(name, expr, env):
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
# reset/shift — evaluate normally in aser mode (they're control flow)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
||||
result = eval_expr(expr, env)
|
||||
return trampoline(result)
|
||||
@@ -1735,38 +1841,6 @@ concat = PRIMITIVES["concat"]
|
||||
'''
|
||||
|
||||
FIXUPS_PY = '''
|
||||
# =========================================================================
|
||||
# Delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
# =========================================================================
|
||||
@@ -1799,6 +1873,65 @@ def _wrap_aser_outputs():
|
||||
aser_fragment = _aser_fragment_wrapped
|
||||
'''
|
||||
|
||||
CONTINUATIONS_PY = '''
|
||||
# =========================================================================
|
||||
# Extension: delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
# Wrap eval_list to inject shift/reset dispatch
|
||||
_base_eval_list = eval_list
|
||||
def _eval_list_with_continuations(expr, env):
|
||||
head = first(expr)
|
||||
if type_of(head) == "symbol":
|
||||
name = symbol_name(head)
|
||||
args = rest(expr)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
return _base_eval_list(expr, env)
|
||||
eval_list = _eval_list_with_continuations
|
||||
|
||||
# Inject into aser_special
|
||||
_base_aser_special = aser_special
|
||||
def _aser_special_with_continuations(name, expr, env):
|
||||
if name == "reset":
|
||||
return sf_reset(expr[1:], env)
|
||||
if name == "shift":
|
||||
return sf_shift(expr[1:], env)
|
||||
return _base_aser_special(name, expr, env)
|
||||
aser_special = _aser_special_with_continuations
|
||||
'''
|
||||
|
||||
|
||||
def public_api_py(has_html: bool, has_sx: bool) -> str:
|
||||
lines = [
|
||||
@@ -1853,9 +1986,15 @@ def main():
|
||||
default=None,
|
||||
help="Comma-separated adapter names (html,sx). Default: all server-side.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extensions",
|
||||
default=None,
|
||||
help="Comma-separated extensions (continuations). Default: none.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
adapters = args.adapters.split(",") if args.adapters else None
|
||||
print(compile_ref_to_py(adapters))
|
||||
extensions = args.extensions.split(",") if args.extensions else None
|
||||
print(compile_ref_to_py(adapters, extensions))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -246,6 +246,14 @@
|
||||
(and (>= next-ch "0") (<= next-ch "9")))))
|
||||
(read-number)
|
||||
|
||||
;; Ellipsis (... as a symbol)
|
||||
(and (= ch ".")
|
||||
(< (+ pos 2) len-src)
|
||||
(= (nth source (+ pos 1)) ".")
|
||||
(= (nth source (+ pos 2)) "."))
|
||||
(do (set! pos (+ pos 3))
|
||||
(make-symbol "..."))
|
||||
|
||||
;; Symbol (must be ident-start char)
|
||||
(ident-start? ch)
|
||||
(read-symbol)
|
||||
|
||||
412
shared/sx/ref/special-forms.sx
Normal file
412
shared/sx/ref/special-forms.sx
Normal file
@@ -0,0 +1,412 @@
|
||||
;; ==========================================================================
|
||||
;; special-forms.sx — Specification of all SX special forms
|
||||
;;
|
||||
;; Special forms are syntactic constructs whose arguments are NOT evaluated
|
||||
;; before dispatch. Each form has its own evaluation rules — unlike primitives,
|
||||
;; which receive pre-evaluated values.
|
||||
;;
|
||||
;; This file is a SPECIFICATION, not executable code. Bootstrap compilers
|
||||
;; consume these declarations but implement special forms natively.
|
||||
;;
|
||||
;; Format:
|
||||
;; (define-special-form "name"
|
||||
;; :syntax (name arg1 arg2 ...)
|
||||
;; :doc "description"
|
||||
;; :tail-position "which subexpressions are in tail position"
|
||||
;; :example "(name ...)")
|
||||
;;
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Control flow
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "if"
|
||||
:syntax (if condition then-expr else-expr)
|
||||
:doc "If condition is truthy, evaluate then-expr; otherwise evaluate else-expr.
|
||||
Both branches are in tail position. The else branch is optional and
|
||||
defaults to nil."
|
||||
:tail-position "then-expr, else-expr"
|
||||
:example "(if (> x 10) \"big\" \"small\")")
|
||||
|
||||
(define-special-form "when"
|
||||
:syntax (when condition body ...)
|
||||
:doc "If condition is truthy, evaluate all body expressions sequentially.
|
||||
Returns the value of the last body expression, or nil if condition
|
||||
is falsy. Only the last body expression is in tail position."
|
||||
:tail-position "last body expression"
|
||||
:example "(when (logged-in? user)
|
||||
(render-dashboard user))")
|
||||
|
||||
(define-special-form "cond"
|
||||
:syntax (cond test1 result1 test2 result2 ... :else default)
|
||||
:doc "Multi-way conditional. Tests are evaluated in order; the result
|
||||
paired with the first truthy test is returned. The :else keyword
|
||||
(or the symbol else) matches unconditionally. Supports both
|
||||
Clojure-style flat pairs and Scheme-style nested pairs:
|
||||
Clojure: (cond test1 result1 test2 result2 :else default)
|
||||
Scheme: (cond (test1 result1) (test2 result2) (else default))"
|
||||
:tail-position "all result expressions"
|
||||
:example "(cond
|
||||
(= status \"active\") (render-active item)
|
||||
(= status \"draft\") (render-draft item)
|
||||
:else (render-unknown item))")
|
||||
|
||||
(define-special-form "case"
|
||||
:syntax (case expr val1 result1 val2 result2 ... :else default)
|
||||
:doc "Match expr against values using equality. Like cond but tests
|
||||
a single expression against multiple values. The :else keyword
|
||||
matches if no values match."
|
||||
:tail-position "all result expressions"
|
||||
:example "(case (get request \"method\")
|
||||
\"GET\" (handle-get request)
|
||||
\"POST\" (handle-post request)
|
||||
:else (method-not-allowed))")
|
||||
|
||||
(define-special-form "and"
|
||||
:syntax (and expr ...)
|
||||
:doc "Short-circuit logical AND. Evaluates expressions left to right.
|
||||
Returns the first falsy value, or the last value if all are truthy.
|
||||
Returns true if given no arguments."
|
||||
:tail-position "last expression"
|
||||
:example "(and (valid? input) (authorized? user) (process input))")
|
||||
|
||||
(define-special-form "or"
|
||||
:syntax (or expr ...)
|
||||
:doc "Short-circuit logical OR. Evaluates expressions left to right.
|
||||
Returns the first truthy value, or the last value if all are falsy.
|
||||
Returns false if given no arguments."
|
||||
:tail-position "last expression"
|
||||
:example "(or (get cache key) (fetch-from-db key) \"default\")")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Binding
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "let"
|
||||
:syntax (let bindings body ...)
|
||||
:doc "Create local bindings and evaluate body in the extended environment.
|
||||
Bindings can be Scheme-style ((name val) ...) or Clojure-style
|
||||
(name val name val ...). Each binding can see previous bindings.
|
||||
Only the last body expression is in tail position.
|
||||
|
||||
Named let: (let name ((x init) ...) body) creates a loop. The name
|
||||
is bound to a function that takes the same params and recurses with
|
||||
tail-call optimization."
|
||||
:tail-position "last body expression; recursive call in named let"
|
||||
:example ";; Basic let
|
||||
(let ((x 10) (y 20))
|
||||
(+ x y))
|
||||
|
||||
;; Clojure-style
|
||||
(let (x 10 y 20)
|
||||
(+ x y))
|
||||
|
||||
;; Named let (loop)
|
||||
(let loop ((i 0) (acc 0))
|
||||
(if (= i 100)
|
||||
acc
|
||||
(loop (+ i 1) (+ acc i))))")
|
||||
|
||||
(define-special-form "let*"
|
||||
:syntax (let* bindings body ...)
|
||||
:doc "Alias for let. In SX, let is already sequential (each binding
|
||||
sees previous ones), so let* is identical to let."
|
||||
:tail-position "last body expression"
|
||||
:example "(let* ((x 10) (y (* x 2)))
|
||||
(+ x y)) ;; → 30")
|
||||
|
||||
(define-special-form "letrec"
|
||||
:syntax (letrec bindings body ...)
|
||||
:doc "Mutually recursive local bindings. All names are bound to nil first,
|
||||
then all values are evaluated (so they can reference each other),
|
||||
then lambda closures are patched to include the final bindings.
|
||||
Used for defining mutually recursive local functions."
|
||||
:tail-position "last body expression"
|
||||
:example "(letrec ((even? (fn (n) (if (= n 0) true (odd? (- n 1)))))
|
||||
(odd? (fn (n) (if (= n 0) false (even? (- n 1))))))
|
||||
(even? 10)) ;; → true")
|
||||
|
||||
(define-special-form "define"
|
||||
:syntax (define name value)
|
||||
:doc "Bind name to value in the current environment. If value is a lambda
|
||||
and has no name, the lambda's name is set to the symbol name.
|
||||
Returns the value."
|
||||
:tail-position "none (value is eagerly evaluated)"
|
||||
:example "(define greeting \"hello\")
|
||||
(define double (fn (x) (* x 2)))")
|
||||
|
||||
(define-special-form "set!"
|
||||
:syntax (set! name value)
|
||||
:doc "Mutate an existing binding. The name must already be bound in the
|
||||
current environment. Returns the new value."
|
||||
:tail-position "none (value is eagerly evaluated)"
|
||||
:example "(let (count 0)
|
||||
(set! count (+ count 1)))")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Functions and components
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "lambda"
|
||||
:syntax (lambda params body)
|
||||
:doc "Create a function. Params is a list of parameter names. Body is
|
||||
a single expression (the return value). The lambda captures the
|
||||
current environment as its closure."
|
||||
:tail-position "body"
|
||||
:example "(lambda (x y) (+ x y))")
|
||||
|
||||
(define-special-form "fn"
|
||||
:syntax (fn params body)
|
||||
:doc "Alias for lambda."
|
||||
:tail-position "body"
|
||||
:example "(fn (x) (* x x))")
|
||||
|
||||
(define-special-form "defcomp"
|
||||
:syntax (defcomp ~name (&key param1 param2 &rest children) body)
|
||||
:doc "Define a component. Components are called with keyword arguments
|
||||
and optional positional children. The &key marker introduces
|
||||
keyword parameters. The &rest (or &children) marker captures
|
||||
remaining positional arguments as a list.
|
||||
|
||||
Component names conventionally start with ~ to distinguish them
|
||||
from HTML elements. Components are evaluated with a merged
|
||||
environment: closure + caller-env + bound-params."
|
||||
:tail-position "body"
|
||||
:example "(defcomp ~card (&key title subtitle &rest children)
|
||||
(div :class \"card\"
|
||||
(h2 title)
|
||||
(when subtitle (p subtitle))
|
||||
children))")
|
||||
|
||||
(define-special-form "defmacro"
|
||||
:syntax (defmacro name (params ...) body)
|
||||
:doc "Define a macro. Macros receive their arguments unevaluated (as raw
|
||||
AST) and return a new expression that is then evaluated. The
|
||||
returned expression replaces the macro call. Use quasiquote for
|
||||
template construction."
|
||||
:tail-position "none (expansion is evaluated separately)"
|
||||
:example "(defmacro unless (condition &rest body)
|
||||
`(when (not ~condition) ~@body))")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Sequencing and threading
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "begin"
|
||||
:syntax (begin expr ...)
|
||||
:doc "Evaluate expressions sequentially. Returns the value of the last
|
||||
expression. Used when multiple side-effecting expressions need
|
||||
to be grouped."
|
||||
:tail-position "last expression"
|
||||
:example "(begin
|
||||
(log \"starting\")
|
||||
(process data)
|
||||
(log \"done\"))")
|
||||
|
||||
(define-special-form "do"
|
||||
:syntax (do expr ...)
|
||||
:doc "Alias for begin."
|
||||
:tail-position "last expression"
|
||||
:example "(do (set! x 1) (set! y 2) (+ x y))")
|
||||
|
||||
(define-special-form "->"
|
||||
:syntax (-> value form1 form2 ...)
|
||||
:doc "Thread-first macro. Threads value through a series of function calls,
|
||||
inserting it as the first argument of each form. Nested lists are
|
||||
treated as function calls; bare symbols become unary calls."
|
||||
:tail-position "last form"
|
||||
:example "(-> user
|
||||
(get \"name\")
|
||||
upper
|
||||
(str \" says hello\"))
|
||||
;; Expands to: (str (upper (get user \"name\")) \" says hello\")")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Quoting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "quote"
|
||||
:syntax (quote expr)
|
||||
:doc "Return expr as data, without evaluating it. Symbols remain symbols,
|
||||
lists remain lists. The reader shorthand is the ' prefix."
|
||||
:tail-position "none (not evaluated)"
|
||||
:example "'(+ 1 2) ;; → the list (+ 1 2), not the number 3")
|
||||
|
||||
(define-special-form "quasiquote"
|
||||
:syntax (quasiquote expr)
|
||||
:doc "Template construction. Like quote, but allows unquoting with ~ and
|
||||
splicing with ~@. The reader shorthand is the ` prefix.
|
||||
`(a ~b ~@c)
|
||||
Quotes everything except: ~expr evaluates expr and inserts the
|
||||
result; ~@expr evaluates to a list and splices its elements."
|
||||
:tail-position "none (template is constructed, not evaluated)"
|
||||
:example "`(div :class \"card\" ~title ~@children)")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Continuations
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "reset"
|
||||
:syntax (reset body)
|
||||
:doc "Establish a continuation delimiter. Evaluates body normally unless
|
||||
a shift is encountered, in which case the continuation (the rest
|
||||
of the computation up to this reset) is captured and passed to
|
||||
the shift's body. Without shift, reset is a no-op wrapper."
|
||||
:tail-position "body"
|
||||
:example "(reset (+ 1 (shift k (k 10)))) ;; → 11")
|
||||
|
||||
(define-special-form "shift"
|
||||
:syntax (shift k body)
|
||||
:doc "Capture the continuation to the nearest reset as k, then evaluate
|
||||
body with k bound. If k is never called, the value of body is
|
||||
returned from the reset (abort). If k is called with a value,
|
||||
the reset body is re-evaluated with shift returning that value.
|
||||
k can be called multiple times."
|
||||
:tail-position "body"
|
||||
:example ";; Abort: shift body becomes the reset result
|
||||
(reset (+ 1 (shift k 42))) ;; → 42
|
||||
|
||||
;; Resume: k re-enters the computation
|
||||
(reset (+ 1 (shift k (k 10)))) ;; → 11
|
||||
|
||||
;; Multiple invocations
|
||||
(reset (* 2 (shift k (+ (k 1) (k 10))))) ;; → 24")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Guards
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "dynamic-wind"
|
||||
:syntax (dynamic-wind before-thunk body-thunk after-thunk)
|
||||
:doc "Entry/exit guards. All three arguments are zero-argument functions
|
||||
(thunks). before-thunk is called on entry, body-thunk is called
|
||||
for the result, and after-thunk is always called on exit (even on
|
||||
error). The wind stack is maintained so that when continuations
|
||||
jump across dynamic-wind boundaries, the correct before/after
|
||||
thunks fire."
|
||||
:tail-position "none (all thunks are eagerly called)"
|
||||
:example "(dynamic-wind
|
||||
(fn () (log \"entering\"))
|
||||
(fn () (do-work))
|
||||
(fn () (log \"exiting\")))")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Higher-order forms
|
||||
;;
|
||||
;; These are syntactic forms (not primitives) because the evaluator
|
||||
;; handles them directly for performance — avoiding the overhead of
|
||||
;; constructing argument lists and doing generic dispatch. They could
|
||||
;; be implemented as primitives but are special-cased in eval-list.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "map"
|
||||
:syntax (map fn coll)
|
||||
:doc "Apply fn to each element of coll, returning a list of results."
|
||||
:tail-position "none"
|
||||
:example "(map (fn (x) (* x x)) (list 1 2 3 4)) ;; → (1 4 9 16)")
|
||||
|
||||
(define-special-form "map-indexed"
|
||||
:syntax (map-indexed fn coll)
|
||||
:doc "Like map, but fn receives two arguments: (index element)."
|
||||
:tail-position "none"
|
||||
:example "(map-indexed (fn (i x) (str i \": \" x)) (list \"a\" \"b\" \"c\"))")
|
||||
|
||||
(define-special-form "filter"
|
||||
:syntax (filter fn coll)
|
||||
:doc "Return elements of coll for which fn returns truthy."
|
||||
:tail-position "none"
|
||||
:example "(filter (fn (x) (> x 3)) (list 1 5 2 8 3)) ;; → (5 8)")
|
||||
|
||||
(define-special-form "reduce"
|
||||
:syntax (reduce fn init coll)
|
||||
:doc "Reduce coll to a single value. fn receives (accumulator element)
|
||||
and returns the new accumulator. init is the initial value."
|
||||
:tail-position "none"
|
||||
:example "(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4)) ;; → 10")
|
||||
|
||||
(define-special-form "some"
|
||||
:syntax (some fn coll)
|
||||
:doc "Return the first truthy result of applying fn to elements of coll,
|
||||
or nil if none match. Short-circuits on first truthy result."
|
||||
:tail-position "none"
|
||||
:example "(some (fn (x) (> x 3)) (list 1 2 5 3)) ;; → true")
|
||||
|
||||
(define-special-form "every?"
|
||||
:syntax (every? fn coll)
|
||||
:doc "Return true if fn returns truthy for every element of coll.
|
||||
Short-circuits on first falsy result."
|
||||
:tail-position "none"
|
||||
:example "(every? (fn (x) (> x 0)) (list 1 2 3)) ;; → true")
|
||||
|
||||
(define-special-form "for-each"
|
||||
:syntax (for-each fn coll)
|
||||
:doc "Apply fn to each element of coll for side effects. Returns nil."
|
||||
:tail-position "none"
|
||||
:example "(for-each (fn (x) (log x)) (list 1 2 3))")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Definition forms (domain-specific)
|
||||
;;
|
||||
;; These define named entities in the environment. They are special forms
|
||||
;; because their arguments have domain-specific structure that the
|
||||
;; evaluator parses directly.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define-special-form "defstyle"
|
||||
:syntax (defstyle name atoms ...)
|
||||
:doc "Define a named style. Evaluates atoms to a StyleValue and binds
|
||||
it to name in the environment."
|
||||
:tail-position "none"
|
||||
:example "(defstyle card-style :rounded-lg :shadow-md :p-4 :bg-white)")
|
||||
|
||||
(define-special-form "defkeyframes"
|
||||
:syntax (defkeyframes name steps ...)
|
||||
:doc "Define a CSS @keyframes animation. Steps are (percentage properties ...)
|
||||
pairs. Produces a StyleValue with the animation name and keyframe rules."
|
||||
:tail-position "none"
|
||||
:example "(defkeyframes fade-in
|
||||
(0 :opacity-0)
|
||||
(100 :opacity-100))")
|
||||
|
||||
(define-special-form "defhandler"
|
||||
:syntax (defhandler name (&key params ...) body)
|
||||
:doc "Define an event handler function. Used by the SxEngine for
|
||||
client-side event handling."
|
||||
:tail-position "body"
|
||||
:example "(defhandler toggle-menu (&key target)
|
||||
(toggle-class target \"hidden\"))")
|
||||
|
||||
(define-special-form "defpage"
|
||||
:syntax (defpage name &key route method content ...)
|
||||
:doc "Define a page route. Declares the URL pattern, HTTP method, and
|
||||
content component for server-side page routing."
|
||||
:tail-position "none"
|
||||
:example "(defpage dashboard-page
|
||||
:route \"/dashboard\"
|
||||
:content (~dashboard-content))")
|
||||
|
||||
(define-special-form "defquery"
|
||||
:syntax (defquery name (&key params ...) body)
|
||||
:doc "Define a named query for data fetching. Used by the resolver
|
||||
system to declare data dependencies."
|
||||
:tail-position "body"
|
||||
:example "(defquery user-profile (&key user-id)
|
||||
(fetch-user user-id))")
|
||||
|
||||
(define-special-form "defaction"
|
||||
:syntax (defaction name (&key params ...) body)
|
||||
:doc "Define a named action for mutations. Like defquery but for
|
||||
write operations."
|
||||
:tail-position "body"
|
||||
:example "(defaction update-profile (&key user-id name email)
|
||||
(save-user user-id name email))")
|
||||
@@ -469,7 +469,7 @@ _SPECIAL_FORM_NAMES = frozenset([
|
||||
"define", "defcomp", "defmacro", "defstyle", "defkeyframes",
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation",
|
||||
"begin", "do", "quote", "quasiquote",
|
||||
"->", "set!", "reset", "shift",
|
||||
"->", "set!",
|
||||
])
|
||||
|
||||
_HO_FORM_NAMES = frozenset([
|
||||
@@ -632,11 +632,6 @@ def aser_special(name, expr, env):
|
||||
"defhandler", "defpage", "defquery", "defaction", "defrelation"):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
# reset/shift — evaluate normally in aser mode (they're control flow)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
# Lambda/fn, quote, quasiquote, set!, -> : evaluate normally
|
||||
result = eval_expr(expr, env)
|
||||
return trampoline(result)
|
||||
@@ -869,7 +864,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result
|
||||
eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)])
|
||||
|
||||
# eval-list
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defkeyframes(args, env) if sx_truthy((name == 'defkeyframes')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr))
|
||||
|
||||
# eval-call
|
||||
eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else error(sx_str('Not callable: ', inspect(f)))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))
|
||||
@@ -911,7 +906,13 @@ sf_and = lambda args, env: (True if sx_truthy(empty_p(args)) else (lambda val: (
|
||||
sf_or = lambda args, env: (False if sx_truthy(empty_p(args)) else (lambda val: (val if sx_truthy(val) else sf_or(rest(args), env)))(trampoline(eval_expr(first(args), env))))
|
||||
|
||||
# sf-let
|
||||
sf_let = lambda args, env: (lambda bindings: (lambda body: (lambda local: _sx_begin((for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))(env_extend(env)))(rest(args)))(first(args))
|
||||
sf_let = lambda args, env: (sf_named_let(args, env) if sx_truthy((type_of(first(args)) == 'symbol')) else (lambda bindings: (lambda body: (lambda local: _sx_begin((for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))(env_extend(env)))(rest(args)))(first(args)))
|
||||
|
||||
# sf-named-let
|
||||
sf_named_let = lambda args, env: (lambda loop_name: (lambda bindings: (lambda body: (lambda params: (lambda inits: _sx_begin((for_each(_sx_fn(lambda binding: (
|
||||
_sx_append(params, (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))),
|
||||
_sx_append(inits, nth(binding, 1))
|
||||
)[-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))
|
||||
@@ -980,6 +981,12 @@ sf_thread_first = lambda args, env: (lambda val: reduce(lambda result, form: ((l
|
||||
# sf-set!
|
||||
sf_set_bang = lambda args, env: (lambda name: (lambda value: _sx_begin(_sx_dict_set(env, name, value), value))(trampoline(eval_expr(nth(args, 1), env))))(symbol_name(first(args)))
|
||||
|
||||
# sf-letrec
|
||||
sf_letrec = lambda args, env: (lambda bindings: (lambda body: (lambda local: (lambda names: (lambda val_exprs: _sx_begin((for_each(lambda binding: (lambda vname: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, nth(binding, 1)), _sx_dict_set(local, vname, NIL)))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), 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: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2)))), (lambda values: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), nth(pair, 1)), zip(names, values)), for_each(lambda val: (for_each(lambda n: _sx_dict_set(lambda_closure(val), n, env_get(local, n)), names) if sx_truthy(is_lambda(val)) else NIL), values)))(map(lambda e: trampoline(eval_expr(e, local)), val_exprs)), for_each(lambda e: trampoline(eval_expr(e, local)), slice(body, 0, (len(body) - 1))), make_thunk(last(body), local)))([]))([]))(env_extend(env)))(rest(args)))(first(args))
|
||||
|
||||
# sf-dynamic-wind
|
||||
sf_dynamic_wind = lambda args, env: (lambda before: (lambda body: (lambda after: _sx_begin(call_thunk(before, env), push_wind_b(before, after), (lambda result: _sx_begin(pop_wind_b(), call_thunk(after, env), result))(call_thunk(body, env))))(trampoline(eval_expr(nth(args, 2), env))))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env)))
|
||||
|
||||
# expand-macro
|
||||
expand_macro = lambda mac, raw_args, env: (lambda local: _sx_begin(for_each(lambda pair: _sx_dict_set(local, first(pair), (nth(raw_args, nth(pair, 1)) if sx_truthy((nth(pair, 1) < len(raw_args))) else NIL)), map_indexed(lambda i, p: [p, i], macro_params(mac))), (_sx_dict_set(local, macro_rest_param(mac), slice(raw_args, len(macro_params(mac)))) if sx_truthy(macro_rest_param(mac)) else NIL), trampoline(eval_expr(macro_body(mac), local))))(env_merge(macro_closure(mac), env))
|
||||
|
||||
@@ -1108,38 +1115,6 @@ aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(pa
|
||||
aser_call = lambda name, args, env: (lambda parts: _sx_begin(reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args), sx_str('(', join(' ', parts), ')')))([name])
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Fixups -- wire up render adapter dispatch
|
||||
# =========================================================================
|
||||
@@ -1172,6 +1147,64 @@ def _wrap_aser_outputs():
|
||||
aser_fragment = _aser_fragment_wrapped
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Extension: delimited continuations (shift/reset)
|
||||
# =========================================================================
|
||||
|
||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
||||
|
||||
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
|
||||
|
||||
def sf_reset(args, env):
|
||||
"""(reset body) -- establish a continuation delimiter."""
|
||||
body = first(args)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
except _ShiftSignal as sig:
|
||||
def cont_fn(value=NIL):
|
||||
_RESET_RESUME.append(value)
|
||||
try:
|
||||
return trampoline(eval_expr(body, env))
|
||||
finally:
|
||||
_RESET_RESUME.pop()
|
||||
k = Continuation(cont_fn)
|
||||
sig_env = dict(sig.env)
|
||||
sig_env[sig.k_name] = k
|
||||
return trampoline(eval_expr(sig.body, sig_env))
|
||||
|
||||
def sf_shift(args, env):
|
||||
"""(shift k body) -- capture continuation to nearest reset."""
|
||||
if _RESET_RESUME:
|
||||
return _RESET_RESUME[-1]
|
||||
k_name = symbol_name(first(args))
|
||||
body = nth(args, 1)
|
||||
raise _ShiftSignal(k_name, body, env)
|
||||
|
||||
# Wrap eval_list to inject shift/reset dispatch
|
||||
_base_eval_list = eval_list
|
||||
def _eval_list_with_continuations(expr, env):
|
||||
head = first(expr)
|
||||
if type_of(head) == "symbol":
|
||||
name = symbol_name(head)
|
||||
args = rest(expr)
|
||||
if name == "reset":
|
||||
return sf_reset(args, env)
|
||||
if name == "shift":
|
||||
return sf_shift(args, env)
|
||||
return _base_eval_list(expr, env)
|
||||
eval_list = _eval_list_with_continuations
|
||||
|
||||
# Inject into aser_special
|
||||
_base_aser_special = aser_special
|
||||
def _aser_special_with_continuations(name, expr, env):
|
||||
if name == "reset":
|
||||
return sf_reset(expr[1:], env)
|
||||
if name == "shift":
|
||||
return sf_shift(expr[1:], env)
|
||||
return _base_aser_special(name, expr, env)
|
||||
aser_special = _aser_special_with_continuations
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Public API
|
||||
# =========================================================================
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -86,6 +86,7 @@
|
||||
(dict :label "Parser" :href "/specs/parser")
|
||||
(dict :label "Evaluator" :href "/specs/evaluator")
|
||||
(dict :label "Primitives" :href "/specs/primitives")
|
||||
(dict :label "Special Forms" :href "/specs/special-forms")
|
||||
(dict :label "Renderer" :href "/specs/renderer")
|
||||
(dict :label "Adapters" :href "/specs/adapters")
|
||||
(dict :label "DOM Adapter" :href "/specs/adapter-dom")
|
||||
@@ -119,6 +120,9 @@
|
||||
(dict :slug "primitives" :filename "primitives.sx" :title "Primitives"
|
||||
:desc "All built-in pure functions and their signatures."
|
||||
:prose "Primitives are the built-in functions available in every SX environment. Each entry declares a name, parameter signature, and semantics. Bootstrap compilers implement these natively per target (JavaScript, Python, etc.). The registry covers arithmetic, comparison, string manipulation, list operations, dict operations, type predicates, and control flow helpers. All primitives are pure — they take values and return values with no side effects. Platform-specific operations (DOM access, HTTP, file I/O) are provided separately via platform bridge functions, not primitives.")
|
||||
(dict :slug "special-forms" :filename "special-forms.sx" :title "Special Forms"
|
||||
:desc "All special forms — syntactic constructs with custom evaluation rules."
|
||||
:prose "Special forms are the syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface. The registry covers control flow (if, when, cond, case, and, or), binding (let, letrec, define, set!), functions (lambda, defcomp, defmacro), sequencing (begin, do, thread-first), quoting (quote, quasiquote), continuations (reset, shift), guards (dynamic-wind), higher-order forms (map, filter, reduce), and domain-specific definitions (defstyle, defhandler, defpage, defquery, defaction).")
|
||||
(dict :slug "renderer" :filename "render.sx" :title "Renderer"
|
||||
:desc "Shared rendering registries and utilities used by all adapters."
|
||||
:prose "The renderer defines what is renderable and how arguments are parsed, but not the output format. It maintains registries of known HTML tags, SVG tags, void elements, and boolean attributes. It specifies how keyword arguments on elements become HTML attributes, how children are collected, and how special attributes (class, style, data-*) are handled. All three adapters (DOM, HTML, SX wire) share these definitions so they agree on what constitutes valid markup. The renderer also defines the StyleValue type used by the CSSX on-demand CSS system.")))
|
||||
|
||||
@@ -47,6 +47,13 @@
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
"primitives.sx"))
|
||||
(td :class "px-3 py-2 text-stone-700" "All built-in pure functions and their signatures"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
|
||||
(a :href "/specs/special-forms" :class "hover:underline"
|
||||
:sx-get "/specs/special-forms" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
"special-forms.sx"))
|
||||
(td :class "px-3 py-2 text-stone-700" "All special forms — syntactic constructs with custom evaluation rules"))
|
||||
(tr :class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 font-mono text-sm text-violet-700"
|
||||
(a :href "/specs/renderer" :class "hover:underline"
|
||||
@@ -157,7 +164,8 @@
|
||||
(pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words font-mono text-stone-700"
|
||||
"parser.sx (standalone — no dependencies)
|
||||
primitives.sx (standalone — declarative registry)
|
||||
eval.sx depends on: parser, primitives
|
||||
special-forms.sx (standalone — declarative registry)
|
||||
eval.sx depends on: parser, primitives, special-forms
|
||||
render.sx (standalone — shared registries)
|
||||
|
||||
adapter-dom.sx depends on: render, eval
|
||||
|
||||
Reference in New Issue
Block a user