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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
110
sx/sx/specs.sx
110
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
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user