Add js.sx bootstrapper docs page with G0 bug discovery writeup

Documents the self-hosting process for js.sx including the G0 bug
where Python's `if fn_expr` treated 0/False/"" as falsy, emitting
NIL instead of the correct value. Adds live verification page,
translation differences table, and nav entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 01:44:02 +00:00
parent e6ca1a5f44
commit cad65bcdf1
6 changed files with 182 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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