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:]
|
body = fn_expr[2:]
|
||||||
loop_body = self._emit_loop_body(name, body)
|
loop_body = self._emit_loop_body(name, body)
|
||||||
return f"var {self._mangle(name)} = function() {{ while(true) {{ {loop_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};"
|
return f"var {self._mangle(name)} = {val};"
|
||||||
|
|
||||||
def _is_self_tail_recursive(self, name: str, body: list) -> bool:
|
def _is_self_tail_recursive(self, name: str, body: list) -> bool:
|
||||||
|
|||||||
@@ -1305,12 +1305,7 @@
|
|||||||
(symbol-name (nth expr 1))
|
(symbol-name (nth expr 1))
|
||||||
(str (nth expr 1))))
|
(str (nth expr 1))))
|
||||||
(val-expr (nth expr 2)))
|
(val-expr (nth expr 2)))
|
||||||
;; Match G0 bootstrap_js.py: Python `if fn_expr` treats 0/false/None/""
|
(if (nil? val-expr)
|
||||||
;; 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 "")))
|
|
||||||
(str "var " (js-mangle name) " = NIL;")
|
(str "var " (js-mangle name) " = NIL;")
|
||||||
;; Detect zero-arg self-tail-recursive functions → while loops
|
;; Detect zero-arg self-tail-recursive functions → while loops
|
||||||
(if (and (list? val-expr)
|
(if (and (list? val-expr)
|
||||||
|
|||||||
@@ -198,7 +198,8 @@
|
|||||||
(dict :label "Overview" :href "/bootstrappers/")
|
(dict :label "Overview" :href "/bootstrappers/")
|
||||||
(dict :label "JavaScript" :href "/bootstrappers/javascript")
|
(dict :label "JavaScript" :href "/bootstrappers/javascript")
|
||||||
(dict :label "Python" :href "/bootstrappers/python")
|
(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.
|
;; Spec file registry — canonical metadata for spec viewer pages.
|
||||||
;; Python only handles file I/O (read-spec-file); all metadata lives here.
|
;; 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"
|
(pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words"
|
||||||
(code (highlight g1-output "python"))))))))
|
(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
|
;; Python bootstrapper detail
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -459,6 +459,13 @@
|
|||||||
:g0-lines g0-lines
|
:g0-lines g0-lines
|
||||||
:g0-bytes g0-bytes
|
:g0-bytes g0-bytes
|
||||||
:verification-status verification-status)
|
: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"
|
"python"
|
||||||
(~bootstrapper-py-content
|
(~bootstrapper-py-content
|
||||||
:bootstrapper-source bootstrapper-source
|
:bootstrapper-source bootstrapper-source
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def _bootstrapper_data(target: str) -> dict:
|
|||||||
"""
|
"""
|
||||||
import os
|
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}
|
return {"bootstrapper-not-found": True}
|
||||||
|
|
||||||
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
|
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":
|
if target == "self-hosting":
|
||||||
return _self_hosting_data(ref_dir)
|
return _self_hosting_data(ref_dir)
|
||||||
|
if target == "self-hosting-js":
|
||||||
|
return _js_self_hosting_data(ref_dir)
|
||||||
|
|
||||||
if target == "javascript":
|
if target == "javascript":
|
||||||
# Read bootstrapper source
|
# 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:
|
def _bundle_analyzer_data() -> dict:
|
||||||
"""Compute per-page component bundle analysis for the sx-docs app."""
|
"""Compute per-page component bundle analysis for the sx-docs app."""
|
||||||
from shared.sx.jinja_bridge import get_component_env
|
from shared.sx.jinja_bridge import get_component_env
|
||||||
|
|||||||
Reference in New Issue
Block a user