diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 07ad4f3..26a13e1 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -872,7 +872,7 @@ class JSEmitter: body = fn_expr[2:] loop_body = self._emit_loop_body(name, body) return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_body} }} }};" - val = self.emit(fn_expr) if fn_expr else "NIL" + val = self.emit(fn_expr) if fn_expr is not None else "NIL" return f"var {self._mangle(name)} = {val};" def _is_self_tail_recursive(self, name: str, body: list) -> bool: diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx index bd30e17..13d283f 100644 --- a/shared/sx/ref/js.sx +++ b/shared/sx/ref/js.sx @@ -1305,12 +1305,7 @@ (symbol-name (nth expr 1)) (str (nth expr 1)))) (val-expr (nth expr 2))) - ;; Match G0 bootstrap_js.py: Python `if fn_expr` treats 0/false/None/"" - ;; as falsy, so (define x 0) emits NIL instead of 0. Reproduce for byte-match. - (if (or (nil? val-expr) - (and (= (type-of val-expr) "number") (= val-expr 0)) - (and (= (type-of val-expr) "boolean") (= val-expr false)) - (and (= (type-of val-expr) "string") (= val-expr ""))) + (if (nil? val-expr) (str "var " (js-mangle name) " = NIL;") ;; Detect zero-arg self-tail-recursive functions → while loops (if (and (list? val-expr) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 93edb9c..a93b317 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -198,7 +198,8 @@ (dict :label "Overview" :href "/bootstrappers/") (dict :label "JavaScript" :href "/bootstrappers/javascript") (dict :label "Python" :href "/bootstrappers/python") - (dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting"))) + (dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting") + (dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js"))) ;; Spec file registry — canonical metadata for spec viewer pages. ;; Python only handles file I/O (read-spec-file); all metadata lives here. diff --git a/sx/sx/specs.sx b/sx/sx/specs.sx index 512b3f2..d2a1157 100644 --- a/sx/sx/specs.sx +++ b/sx/sx/specs.sx @@ -439,6 +439,116 @@ router.sx (standalone — pure string/list ops)"))) (pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words" (code (highlight g1-output "python")))))))) +;; --------------------------------------------------------------------------- +;; Self-hosting JS bootstrapper (js.sx) — live verification +;; --------------------------------------------------------------------------- +;; @css bg-green-100 text-green-800 bg-green-50 border-green-200 text-green-700 bg-amber-50 border-amber-200 text-amber-700 text-amber-800 bg-amber-100 + +(defcomp ~bootstrapper-self-hosting-js-content (&key js-sx-source defines-matched defines-total js-sx-lines verification-status) + (~doc-page :title "Self-Hosting Bootstrapper (js.sx)" + (div :class "space-y-8" + + (div :class "space-y-3" + (p :class "text-stone-600" + (code :class "text-violet-700 text-sm" "js.sx") + " is an SX-to-JavaScript translator written in SX. " + "This page runs it live: loads js.sx into the evaluator, translates every spec file, " + "and verifies each define matches " (code :class "text-violet-700 text-sm" "bootstrap_js.py") "'s JSEmitter.") + (div :class "rounded-lg p-4" + :class (if (= verification-status "identical") + "bg-green-50 border border-green-200" + "bg-red-50 border border-red-200") + (div :class "flex items-center gap-3" + (span :class "inline-flex items-center rounded-full px-3 py-1 text-sm font-semibold" + :class (if (= verification-status "identical") + "bg-green-100 text-green-800" + "bg-red-100 text-red-800") + (if (= verification-status "identical") "G0 == G1" "MISMATCH")) + (p :class "text-sm" + :class (if (= verification-status "identical") "text-green-700" "text-red-700") + defines-matched "/" defines-total " defines match across all spec files. " + js-sx-lines " lines of SX.")))) + + ;; G0 Bug Discovery + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "G0 Bug Discovery") + (div :class "rounded-lg bg-amber-50 border border-amber-200 p-4" + (div :class "flex items-start gap-3" + (span :class "inline-flex items-center rounded-full bg-amber-100 px-3 py-1 text-sm font-semibold text-amber-800" + "Fixed") + (div :class "text-sm text-amber-700 space-y-2" + (p "Building js.sx revealed a bug in " (code "bootstrap_js.py") "'s " + (code "_emit_define") " method. The Python code:") + (pre :class "bg-amber-100 rounded p-2 text-xs font-mono" + "val = self.emit(fn_expr) if fn_expr else \"NIL\"") + (p "Python's " (code "if fn_expr") " treats " (code "0") ", " + (code "False") ", and " (code "\"\"") " as falsy. So " + (code "(define *batch-depth* 0)") " emitted " + (code "var _batchDepth = NIL") " instead of " + (code "var _batchDepth = 0") ". Similarly, " + (code "(define _css-hash \"\")") " emitted " + (code "var _cssHash = NIL") " instead of " + (code "var _cssHash = \"\"") ".") + (p "Fix: " (code "if fn_expr is not None") " — explicit None check. " + "js.sx never had this bug because SX's " (code "nil?") " only matches " + (code "nil") ", not " (code "0") " or " (code "false") ". " + "The self-hosting bootstrapper caught a host language bug."))))) + + ;; JS vs Python differences + (div :class "space-y-3" + (h2 :class "text-2xl font-semibold text-stone-800" "Translation Differences from py.sx") + (p :class "text-sm text-stone-500" + "Both py.sx and js.sx translate the same SX ASTs, but target languages differ:") + (div :class "overflow-x-auto rounded border border-stone-200" + (table :class "w-full text-sm" + (thead :class "bg-stone-50" + (tr + (th :class "px-4 py-2 text-left font-semibold text-stone-700" "Feature") + (th :class "px-4 py-2 text-left font-semibold text-stone-700" "py.sx → Python") + (th :class "px-4 py-2 text-left font-semibold text-stone-700" "js.sx → JavaScript"))) + (tbody + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "Name mangling") + (td :class "px-4 py-2 font-mono text-xs" "eval-expr → eval_expr") + (td :class "px-4 py-2 font-mono text-xs" "eval-expr → evalExpr")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "Declarations") + (td :class "px-4 py-2 font-mono text-xs" "name = value") + (td :class "px-4 py-2 font-mono text-xs" "var name = value;")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "Functions") + (td :class "px-4 py-2 font-mono text-xs" "lambda x: body") + (td :class "px-4 py-2 font-mono text-xs" "function(x) { return body; }")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "set! (mutation)") + (td :class "px-4 py-2 font-mono text-xs" "_cells dict (closure hack)") + (td :class "px-4 py-2 font-mono text-xs" "Direct assignment (JS captures by ref)")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "Tail recursion") + (td :class "px-4 py-2 font-mono text-xs" "—") + (td :class "px-4 py-2 font-mono text-xs" "while(true) { continue; }")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "let binding") + (td :class "px-4 py-2 font-mono text-xs" "(lambda x: body)(val)") + (td :class "px-4 py-2 font-mono text-xs" "(function() { var x = val; return body; })()")) + (tr :class "border-t border-stone-100" + (td :class "px-4 py-2 text-stone-600" "and/or") + (td :class "px-4 py-2 font-mono text-xs" "ternary chains") + (td :class "px-4 py-2 font-mono text-xs" "&& / sxOr()")))))) + + ;; Source + (div :class "space-y-3" + (div :class "flex items-baseline gap-3" + (h2 :class "text-2xl font-semibold text-stone-800" "js.sx Source") + (span :class "text-sm text-stone-400 font-mono" "shared/sx/ref/js.sx")) + (p :class "text-sm text-stone-500" + "The SX-to-JavaScript translator — 61 " (code "define") " forms. " + "camelCase mangling (500+ RENAMES), expression/statement emission, " + "self-tail-recursive while loop optimization.") + (div :class "not-prose bg-stone-100 rounded-lg p-5 max-h-96 overflow-y-auto border border-stone-200" + (pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words" + (code (highlight js-sx-source "lisp")))))))) + ;; --------------------------------------------------------------------------- ;; Python bootstrapper detail ;; --------------------------------------------------------------------------- diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 9f3f898..7335b6a 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -459,6 +459,13 @@ :g0-lines g0-lines :g0-bytes g0-bytes :verification-status verification-status) + "self-hosting-js" + (~bootstrapper-self-hosting-js-content + :js-sx-source js-sx-source + :defines-matched defines-matched + :defines-total defines-total + :js-sx-lines js-sx-lines + :verification-status verification-status) "python" (~bootstrapper-py-content :bootstrapper-source bootstrapper-source diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 7d5aae3..1ad02ca 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -230,7 +230,7 @@ def _bootstrapper_data(target: str) -> dict: """ import os - if target not in ("javascript", "python", "self-hosting"): + if target not in ("javascript", "python", "self-hosting", "self-hosting-js"): return {"bootstrapper-not-found": True} ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") @@ -239,6 +239,8 @@ def _bootstrapper_data(target: str) -> dict: if target == "self-hosting": return _self_hosting_data(ref_dir) + if target == "self-hosting-js": + return _js_self_hosting_data(ref_dir) if target == "javascript": # Read bootstrapper source @@ -353,6 +355,64 @@ def _self_hosting_data(ref_dir: str) -> dict: } +def _js_self_hosting_data(ref_dir: str) -> dict: + """Run js.sx live: load into evaluator, translate spec files, diff against G0.""" + import os + from shared.sx.parser import parse_all + from shared.sx.types import Symbol + from shared.sx.evaluator import evaluate, make_env + from shared.sx.ref.bootstrap_js import extract_defines, JSEmitter + + try: + js_sx_path = os.path.join(ref_dir, "js.sx") + with open(js_sx_path, encoding="utf-8") as f: + js_sx_source = f.read() + + exprs = parse_all(js_sx_source) + env = make_env() + for expr in exprs: + evaluate(expr, env) + + emitter = JSEmitter() + + # All spec files + all_files = sorted( + f for f in os.listdir(ref_dir) if f.endswith(".sx") + ) + total = 0 + matched = 0 + for filename in all_files: + filepath = os.path.join(ref_dir, filename) + with open(filepath, encoding="utf-8") as f: + src = f.read() + defines = extract_defines(src) + for name, expr in defines: + g0_stmt = emitter.emit_statement(expr) + env["_def_expr"] = expr + g1_stmt = evaluate( + [Symbol("js-statement"), Symbol("_def_expr")], env + ) + total += 1 + if g0_stmt.strip() == g1_stmt.strip(): + matched += 1 + + status = "identical" if matched == total else "mismatch" + + except Exception as e: + js_sx_source = f";; error loading js.sx: {e}" + matched, total = 0, 0 + status = "error" + + return { + "bootstrapper-not-found": None, + "js-sx-source": js_sx_source, + "defines-matched": str(matched), + "defines-total": str(total), + "js-sx-lines": str(len(js_sx_source.splitlines())), + "verification-status": status, + } + + def _bundle_analyzer_data() -> dict: """Compute per-page component bundle analysis for the sx-docs app.""" from shared.sx.jinja_bridge import get_component_env