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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user