js-on-sx: harness cache — precompute HARNESS_STUB SX once per run
Root cause: every sx_server worker session used js-eval on the 3.6KB HARNESS_STUB, paying ~15s for tokenize+parse+transpile even though every session does the same thing. Over a full scoreboard with periodic worker restarts that's minutes of wasted work. Fix: transpile once per Python process. Spin up a throwaway sx_server, run (inspect (js-transpile (js-parse (js-tokenize HARNESS_STUB)))), write the resulting SX source to lib/js/.harness-cache/stub.<fingerprint>.sx and a stable-name symlink-ish copy stub.sx. Every worker session then does a single (load .harness-cache/stub.sx) instead of re-running js-eval. Fingerprint: sha256(HARNESS_STUB + lexer.sx + parser.sx + transpile.sx). Transpiler edits invalidate the cache automatically. Runs back-to-back reuse the cache — only the first run after a transpiler change pays the ~15s precompute. Transpile had to gain a $-to-_js_dollar_ name-mangler: the SX tokenizer rejects $ in identifiers, which broke round-tripping via inspect. JS $DONOTEVALUATE → SX _js_dollar_DONOTEVALUATE. Internal JS-on-SX names are unaffected (none contain $). Measured: 300-test wide (Math+Number+String @ 100/cat, --per-test-timeout 5): 593.7s → 288.0s, 2.06x speedup. Scoreboard 114→115/300 (38.3%, noise band). Math 40%, Number 44%, String 30% — same shape as prior. Baselines: 520/522 unit, 148/148 slice — unchanged.
This commit is contained in:
1
lib/js/.gitignore
vendored
1
lib/js/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
test262-upstream/
|
test262-upstream/
|
||||||
|
.harness-cache/
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ HARNESS_DIR = UPSTREAM / "harness"
|
|||||||
DEFAULT_PER_TEST_TIMEOUT_S = 5.0
|
DEFAULT_PER_TEST_TIMEOUT_S = 5.0
|
||||||
DEFAULT_BATCH_TIMEOUT_S = 120
|
DEFAULT_BATCH_TIMEOUT_S = 120
|
||||||
|
|
||||||
|
# Cache dir for precomputed SX source of harness JS (one file per Python run).
|
||||||
|
# Written once in main(), read via (load ...) by every worker session.
|
||||||
|
HARNESS_CACHE_DIR = REPO / "lib" / "js" / ".harness-cache"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Harness stub — replaces assert.js + sta.js with something our parser handles.
|
# Harness stub — replaces assert.js + sta.js with something our parser handles.
|
||||||
@@ -588,6 +592,175 @@ def load_test(path: Path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Harness cache — transpile HARNESS_STUB once, write SX to disk.
|
||||||
|
# Every worker then loads the cached .sx (a few ms) instead of re-running
|
||||||
|
# js-tokenize + js-parse + js-transpile (15+ s).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Remembered across the Python process. None until we've run the precompute.
|
||||||
|
_HARNESS_CACHE_PATH: "Path | None" = None
|
||||||
|
# Per-filename include cache: maps 'compareArray.js' -> Path of cached .sx.
|
||||||
|
_EXTRA_HARNESS_CACHE: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _harness_cache_rel_path() -> "str | None":
|
||||||
|
if _HARNESS_CACHE_PATH is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return _HARNESS_CACHE_PATH.relative_to(REPO).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
return str(_HARNESS_CACHE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def _precompute_sx(js_source: str, timeout_s: float = 120.0) -> str:
|
||||||
|
"""Run one throwaway sx_server to turn a chunk of JS into the SX text that
|
||||||
|
js-eval would have evaluated. Returns the raw SX source (no outer quotes).
|
||||||
|
"""
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[str(SX_SERVER)],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
cwd=str(REPO),
|
||||||
|
bufsize=0,
|
||||||
|
)
|
||||||
|
fd = proc.stdout.fileno()
|
||||||
|
os.set_blocking(fd, False)
|
||||||
|
|
||||||
|
buf = [b""]
|
||||||
|
|
||||||
|
def readline(timeout: float):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while True:
|
||||||
|
nl = buf[0].find(b"\n")
|
||||||
|
if nl >= 0:
|
||||||
|
line = buf[0][: nl + 1]
|
||||||
|
buf[0] = buf[0][nl + 1 :]
|
||||||
|
return line
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
raise TimeoutError("precompute readline timeout")
|
||||||
|
rlist, _, _ = select.select([fd], [], [], remaining)
|
||||||
|
if not rlist:
|
||||||
|
raise TimeoutError("precompute readline timeout")
|
||||||
|
try:
|
||||||
|
chunk = os.read(fd, 65536)
|
||||||
|
except (BlockingIOError, InterruptedError):
|
||||||
|
continue
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
buf[0] += chunk
|
||||||
|
|
||||||
|
def run(epoch: int, cmd: str, to: float = 60.0):
|
||||||
|
proc.stdin.write(f"(epoch {epoch})\n{cmd}\n".encode("utf-8"))
|
||||||
|
proc.stdin.flush()
|
||||||
|
deadline = time.monotonic() + to
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
line = readline(deadline - time.monotonic())
|
||||||
|
if line is None:
|
||||||
|
raise RuntimeError("precompute: sx_server closed stdout")
|
||||||
|
m = RX_OK_INLINE.match(line.decode("utf-8", "replace"))
|
||||||
|
if m and int(m.group(1)) == epoch:
|
||||||
|
return "ok", m.group(2)
|
||||||
|
m = RX_OK_LEN.match(line.decode("utf-8", "replace"))
|
||||||
|
if m and int(m.group(1)) == epoch:
|
||||||
|
val = readline(deadline - time.monotonic())
|
||||||
|
return "ok", (val or b"").decode("utf-8", "replace").rstrip("\n")
|
||||||
|
m = RX_ERR.match(line.decode("utf-8", "replace"))
|
||||||
|
if m and int(m.group(1)) == epoch:
|
||||||
|
return "error", m.group(2)
|
||||||
|
raise TimeoutError(f"precompute epoch {epoch}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Wait for ready
|
||||||
|
deadline = time.monotonic() + 15.0
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
line = readline(deadline - time.monotonic())
|
||||||
|
if line is None:
|
||||||
|
raise RuntimeError("precompute: sx_server closed before ready")
|
||||||
|
if b"(ready)" in line:
|
||||||
|
break
|
||||||
|
# Load JS kernel
|
||||||
|
run(1, '(load "lib/r7rs.sx")')
|
||||||
|
run(2, '(load "lib/js/lexer.sx")')
|
||||||
|
run(3, '(load "lib/js/parser.sx")')
|
||||||
|
run(4, '(load "lib/js/transpile.sx")')
|
||||||
|
# Transpile to SX source via inspect
|
||||||
|
inner = js_source.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
inner = inner.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
||||||
|
outer = inner.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
cmd = f'(eval "(inspect (js-transpile (js-parse (js-tokenize \\"{outer}\\"))))")'
|
||||||
|
kind, payload = run(5, cmd, timeout_s)
|
||||||
|
if kind != "ok":
|
||||||
|
raise RuntimeError(f"precompute error: {payload[:200]}")
|
||||||
|
# payload is an SX string-literal — peel one layer of quoting.
|
||||||
|
import json as _json
|
||||||
|
if payload.startswith('"') and payload.endswith('"'):
|
||||||
|
return _json.loads(payload)
|
||||||
|
return payload
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
proc.stdin.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait(timeout=3)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
proc.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _harness_fingerprint() -> str:
|
||||||
|
import hashlib
|
||||||
|
# Include the exact runtime/transpile source hash so a change to the
|
||||||
|
# transpiler invalidates the cache automatically.
|
||||||
|
h = hashlib.sha256()
|
||||||
|
h.update(HARNESS_STUB.encode("utf-8"))
|
||||||
|
for p in ("lib/js/lexer.sx", "lib/js/parser.sx", "lib/js/transpile.sx"):
|
||||||
|
try:
|
||||||
|
h.update((REPO / p).read_bytes())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return h.hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
def precompute_harness_cache() -> Path:
|
||||||
|
"""Populate _HARNESS_CACHE_PATH by transpiling HARNESS_STUB once and
|
||||||
|
writing it to disk. Every worker session then does (load <path>) instead.
|
||||||
|
|
||||||
|
Reuses a prior cache file from a previous `python3 test262-runner.py`
|
||||||
|
run when the fingerprint (harness text + transpiler source hash) still
|
||||||
|
matches — that covers the common case of re-running scoreboards back-to-back
|
||||||
|
without touching transpile.sx.
|
||||||
|
"""
|
||||||
|
global _HARNESS_CACHE_PATH
|
||||||
|
HARNESS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
fp = _harness_fingerprint()
|
||||||
|
dst = HARNESS_CACHE_DIR / f"stub.{fp}.sx"
|
||||||
|
stable = HARNESS_CACHE_DIR / "stub.sx"
|
||||||
|
if dst.exists() and dst.stat().st_size > 0:
|
||||||
|
# Expose both the canonical and fingerprinted names — sessions load
|
||||||
|
# the canonical one.
|
||||||
|
stable.write_bytes(dst.read_bytes())
|
||||||
|
_HARNESS_CACHE_PATH = stable
|
||||||
|
print(f"harness cache: reused {dst.name} ({dst.stat().st_size} bytes)",
|
||||||
|
file=sys.stderr)
|
||||||
|
return stable
|
||||||
|
t0 = time.monotonic()
|
||||||
|
sx = _precompute_sx(HARNESS_STUB)
|
||||||
|
dst.write_text(sx, encoding="utf-8")
|
||||||
|
stable.write_text(sx, encoding="utf-8")
|
||||||
|
_HARNESS_CACHE_PATH = stable
|
||||||
|
dt = time.monotonic() - t0
|
||||||
|
print(f"harness cache: {len(HARNESS_STUB)} JS chars → {len(sx)} SX chars "
|
||||||
|
f"at {stable.relative_to(REPO)} (fp={fp}, {dt:.2f}s)", file=sys.stderr)
|
||||||
|
return stable
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Long-lived server session
|
# Long-lived server session
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -625,13 +798,18 @@ class ServerSession:
|
|||||||
self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0)
|
self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0)
|
||||||
self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0)
|
self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0)
|
||||||
self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0)
|
self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0)
|
||||||
# Preload the stub harness as one big js-eval
|
# Preload the stub harness — use precomputed SX cache when available
|
||||||
stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB)
|
# (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx).
|
||||||
self._run_and_collect(
|
cache_rel = _harness_cache_rel_path()
|
||||||
6,
|
if cache_rel is not None:
|
||||||
f'(eval "(js-eval \\"{stub_escaped}\\")")',
|
self._run_and_collect(6, f'(load "{cache_rel}")', timeout=60.0)
|
||||||
timeout=60.0,
|
else:
|
||||||
)
|
stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB)
|
||||||
|
self._run_and_collect(
|
||||||
|
6,
|
||||||
|
f'(eval "(js-eval \\"{stub_escaped}\\")")',
|
||||||
|
timeout=60.0,
|
||||||
|
)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
@@ -964,6 +1142,14 @@ def main(argv):
|
|||||||
all_paths = all_paths[: args.limit]
|
all_paths = all_paths[: args.limit]
|
||||||
print(f"Discovered {len(all_paths)} test files.", file=sys.stderr)
|
print(f"Discovered {len(all_paths)} test files.", file=sys.stderr)
|
||||||
|
|
||||||
|
# Precompute harness cache once per run. Workers (forked) inherit module
|
||||||
|
# globals, so the cache path is visible to every session.start() call.
|
||||||
|
try:
|
||||||
|
precompute_harness_cache()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"harness cache precompute failed ({e}); falling back to js-eval per session",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
tests = []
|
tests = []
|
||||||
results = []
|
results = []
|
||||||
per_cat_count = defaultdict(int)
|
per_cat_count = defaultdict(int)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"totals": {
|
"totals": {
|
||||||
"pass": 118,
|
"pass": 115,
|
||||||
"fail": 160,
|
"fail": 174,
|
||||||
"skip": 1597,
|
"skip": 1597,
|
||||||
"timeout": 22,
|
"timeout": 11,
|
||||||
"total": 1897,
|
"total": 1897,
|
||||||
"runnable": 300,
|
"runnable": 300,
|
||||||
"pass_rate": 39.3
|
"pass_rate": 38.3
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
{
|
{
|
||||||
@@ -35,46 +35,42 @@
|
|||||||
{
|
{
|
||||||
"category": "built-ins/Number",
|
"category": "built-ins/Number",
|
||||||
"total": 340,
|
"total": 340,
|
||||||
"pass": 48,
|
"pass": 44,
|
||||||
"fail": 45,
|
"fail": 52,
|
||||||
"skip": 240,
|
"skip": 240,
|
||||||
"timeout": 7,
|
"timeout": 4,
|
||||||
"pass_rate": 48.0,
|
"pass_rate": 44.0,
|
||||||
"top_failures": [
|
"top_failures": [
|
||||||
[
|
[
|
||||||
"Test262Error (assertion failed)",
|
"Test262Error (assertion failed)",
|
||||||
42
|
52
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Timeout",
|
"Timeout",
|
||||||
7
|
4
|
||||||
],
|
|
||||||
[
|
|
||||||
"ReferenceError (undefined symbol)",
|
|
||||||
3
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "built-ins/String",
|
"category": "built-ins/String",
|
||||||
"total": 1223,
|
"total": 1223,
|
||||||
"pass": 30,
|
"pass": 31,
|
||||||
"fail": 56,
|
"fail": 63,
|
||||||
"skip": 1123,
|
"skip": 1123,
|
||||||
"timeout": 14,
|
"timeout": 6,
|
||||||
"pass_rate": 30.0,
|
"pass_rate": 31.0,
|
||||||
"top_failures": [
|
"top_failures": [
|
||||||
[
|
[
|
||||||
"Test262Error (assertion failed)",
|
"Test262Error (assertion failed)",
|
||||||
43
|
53
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Timeout",
|
"Timeout",
|
||||||
14
|
6
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"ReferenceError (undefined symbol)",
|
"ReferenceError (undefined symbol)",
|
||||||
7
|
2
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Unhandled: Not callable: \\\\\\",
|
"Unhandled: Not callable: \\\\\\",
|
||||||
@@ -100,19 +96,19 @@
|
|||||||
"top_failure_modes": [
|
"top_failure_modes": [
|
||||||
[
|
[
|
||||||
"Test262Error (assertion failed)",
|
"Test262Error (assertion failed)",
|
||||||
108
|
128
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"TypeError: not a function",
|
"TypeError: not a function",
|
||||||
36
|
37
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Timeout",
|
"Timeout",
|
||||||
22
|
11
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"ReferenceError (undefined symbol)",
|
"ReferenceError (undefined symbol)",
|
||||||
10
|
2
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Unhandled: Not callable: \\\\\\",
|
"Unhandled: Not callable: \\\\\\",
|
||||||
@@ -129,9 +125,13 @@
|
|||||||
[
|
[
|
||||||
"Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\\",
|
"Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\\",
|
||||||
1
|
1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Unhandled: js-transpile-binop: unsupported op: >>>\\",
|
||||||
|
1
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33",
|
"pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33",
|
||||||
"elapsed_seconds": 429.3,
|
"elapsed_seconds": 288.0,
|
||||||
"workers": 1
|
"workers": 1
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,37 @@
|
|||||||
# test262 scoreboard
|
# test262 scoreboard
|
||||||
|
|
||||||
Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`
|
Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`
|
||||||
Wall time: 429.3s
|
Wall time: 288.0s
|
||||||
|
|
||||||
**Total:** 118/300 runnable passed (39.3%). Raw: pass=118 fail=160 skip=1597 timeout=22 total=1897.
|
**Total:** 115/300 runnable passed (38.3%). Raw: pass=115 fail=174 skip=1597 timeout=11 total=1897.
|
||||||
|
|
||||||
## Top failure modes
|
## Top failure modes
|
||||||
|
|
||||||
- **108x** Test262Error (assertion failed)
|
- **128x** Test262Error (assertion failed)
|
||||||
- **36x** TypeError: not a function
|
- **37x** TypeError: not a function
|
||||||
- **22x** Timeout
|
- **11x** Timeout
|
||||||
- **10x** ReferenceError (undefined symbol)
|
- **2x** ReferenceError (undefined symbol)
|
||||||
- **2x** Unhandled: Not callable: \\\
|
- **2x** Unhandled: Not callable: \\\
|
||||||
- **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\
|
- **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\
|
||||||
- **1x** SyntaxError (parse/unsupported syntax)
|
- **1x** SyntaxError (parse/unsupported syntax)
|
||||||
- **1x** Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\
|
- **1x** Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\
|
||||||
|
- **1x** Unhandled: js-transpile-binop: unsupported op: >>>\
|
||||||
|
|
||||||
## Categories (worst pass-rate first, min 10 runnable)
|
## Categories (worst pass-rate first, min 10 runnable)
|
||||||
|
|
||||||
| Category | Pass | Fail | Skip | Timeout | Total | Pass % |
|
| Category | Pass | Fail | Skip | Timeout | Total | Pass % |
|
||||||
|---|---:|---:|---:|---:|---:|---:|
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
| built-ins/String | 30 | 56 | 1123 | 14 | 1223 | 30.0% |
|
| built-ins/String | 31 | 63 | 1123 | 6 | 1223 | 31.0% |
|
||||||
| built-ins/Math | 40 | 59 | 227 | 1 | 327 | 40.0% |
|
| built-ins/Math | 40 | 59 | 227 | 1 | 327 | 40.0% |
|
||||||
| built-ins/Number | 48 | 45 | 240 | 7 | 340 | 48.0% |
|
| built-ins/Number | 44 | 52 | 240 | 4 | 340 | 44.0% |
|
||||||
|
|
||||||
## Per-category top failures (min 10 runnable, worst first)
|
## Per-category top failures (min 10 runnable, worst first)
|
||||||
|
|
||||||
### built-ins/String (30/100 — 30.0%)
|
### built-ins/String (31/100 — 31.0%)
|
||||||
|
|
||||||
- **43x** Test262Error (assertion failed)
|
- **53x** Test262Error (assertion failed)
|
||||||
- **14x** Timeout
|
- **6x** Timeout
|
||||||
- **7x** ReferenceError (undefined symbol)
|
- **2x** ReferenceError (undefined symbol)
|
||||||
- **2x** Unhandled: Not callable: \\\
|
- **2x** Unhandled: Not callable: \\\
|
||||||
- **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\
|
- **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\
|
||||||
|
|
||||||
@@ -40,8 +41,7 @@ Wall time: 429.3s
|
|||||||
- **23x** Test262Error (assertion failed)
|
- **23x** Test262Error (assertion failed)
|
||||||
- **1x** Timeout
|
- **1x** Timeout
|
||||||
|
|
||||||
### built-ins/Number (48/100 — 48.0%)
|
### built-ins/Number (44/100 — 44.0%)
|
||||||
|
|
||||||
- **42x** Test262Error (assertion failed)
|
- **52x** Test262Error (assertion failed)
|
||||||
- **7x** Timeout
|
- **4x** Timeout
|
||||||
- **3x** ReferenceError (undefined symbol)
|
|
||||||
|
|||||||
@@ -23,7 +23,46 @@
|
|||||||
|
|
||||||
;; ── tiny helpers ──────────────────────────────────────────────────
|
;; ── tiny helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
(define js-sym (fn (name) (make-symbol name)))
|
(define js-has-dollar? (fn (name) (js-has-dollar-loop? name 0 (len name))))
|
||||||
|
(define
|
||||||
|
js-has-dollar-loop?
|
||||||
|
(fn
|
||||||
|
(s i n)
|
||||||
|
(cond
|
||||||
|
((>= i n) false)
|
||||||
|
((= (char-at s i) "$") true)
|
||||||
|
(else (js-has-dollar-loop? s (+ i 1) n)))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
js-mangle-ident
|
||||||
|
(fn
|
||||||
|
(name)
|
||||||
|
(if
|
||||||
|
(js-has-dollar? name)
|
||||||
|
(js-mangle-ident-loop name 0 (len name) "")
|
||||||
|
name)))
|
||||||
|
|
||||||
|
;; ── main dispatcher ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
(define
|
||||||
|
js-mangle-ident-loop
|
||||||
|
(fn
|
||||||
|
(s i n acc)
|
||||||
|
(cond
|
||||||
|
((>= i n) acc)
|
||||||
|
((= (char-at s i) "$")
|
||||||
|
(js-mangle-ident-loop s (+ i 1) n (str acc "_js_dollar_")))
|
||||||
|
(else (js-mangle-ident-loop s (+ i 1) n (str acc (char-at s i)))))))
|
||||||
|
|
||||||
|
;; ── Identifier lookup ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; `undefined` in JS is really a global binding. If the parser emits
|
||||||
|
;; (js-undef) we handle that above. A bare `undefined` ident also maps
|
||||||
|
;; to the same sentinel.
|
||||||
|
(define js-sym (fn (name) (make-symbol (js-mangle-ident name))))
|
||||||
|
|
||||||
|
;; ── Unary ops ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-tag?
|
js-tag?
|
||||||
(fn
|
(fn
|
||||||
@@ -34,9 +73,11 @@
|
|||||||
(= (type-of (first ast)) "symbol")
|
(= (type-of (first ast)) "symbol")
|
||||||
(= (symbol-name (first ast)) tag))))
|
(= (symbol-name (first ast)) tag))))
|
||||||
|
|
||||||
|
;; ── Binary ops ────────────────────────────────────────────────────
|
||||||
|
|
||||||
(define js-ast-tag (fn (ast) (symbol-name (first ast))))
|
(define js-ast-tag (fn (ast) (symbol-name (first ast))))
|
||||||
|
|
||||||
;; ── main dispatcher ───────────────────────────────────────────────
|
;; ── Member / index ────────────────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-transpile
|
js-transpile
|
||||||
@@ -146,11 +187,6 @@
|
|||||||
(else
|
(else
|
||||||
(error (str "js-transpile: unexpected value type: " (type-of ast)))))))
|
(error (str "js-transpile: unexpected value type: " (type-of ast)))))))
|
||||||
|
|
||||||
;; ── Identifier lookup ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
;; `undefined` in JS is really a global binding. If the parser emits
|
|
||||||
;; (js-undef) we handle that above. A bare `undefined` ident also maps
|
|
||||||
;; to the same sentinel.
|
|
||||||
(define
|
(define
|
||||||
js-transpile-ident
|
js-transpile-ident
|
||||||
(fn
|
(fn
|
||||||
@@ -164,8 +200,10 @@
|
|||||||
((= name "Function") (js-sym "js-function-global"))
|
((= name "Function") (js-sym "js-function-global"))
|
||||||
(else (js-sym name)))))
|
(else (js-sym name)))))
|
||||||
|
|
||||||
;; ── Unary ops ─────────────────────────────────────────────────────
|
;; ── Call ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; JS `f(a, b, c)` → `(f a b c)` after transpile. Works for both
|
||||||
|
;; identifier calls and computed callee (arrow fn, member access).
|
||||||
(define
|
(define
|
||||||
js-transpile-unop
|
js-transpile-unop
|
||||||
(fn
|
(fn
|
||||||
@@ -196,7 +234,7 @@
|
|||||||
((= op "void") (list (js-sym "quote") :js-undefined))
|
((= op "void") (list (js-sym "quote") :js-undefined))
|
||||||
(else (error (str "js-transpile-unop: unsupported op: " op)))))))))
|
(else (error (str "js-transpile-unop: unsupported op: " op)))))))))
|
||||||
|
|
||||||
;; ── Binary ops ────────────────────────────────────────────────────
|
;; ── Array literal ─────────────────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-transpile-binop
|
js-transpile-binop
|
||||||
@@ -259,22 +297,25 @@
|
|||||||
(js-sym "_a"))))
|
(js-sym "_a"))))
|
||||||
(else (error (str "js-transpile-binop: unsupported op: " op))))))
|
(else (error (str "js-transpile-binop: unsupported op: " op))))))
|
||||||
|
|
||||||
;; ── Member / index ────────────────────────────────────────────────
|
;; ── Object literal ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Build a dict by `(dict)` + `dict-set!` inside a `let` that yields
|
||||||
|
;; the dict as its final expression. This keeps keys in JS insertion
|
||||||
|
;; order and allows computed values.
|
||||||
(define
|
(define
|
||||||
js-transpile-member
|
js-transpile-member
|
||||||
(fn (obj key) (list (js-sym "js-get-prop") (js-transpile obj) key)))
|
(fn (obj key) (list (js-sym "js-get-prop") (js-transpile obj) key)))
|
||||||
|
|
||||||
|
;; ── Conditional ───────────────────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-transpile-index
|
js-transpile-index
|
||||||
(fn
|
(fn
|
||||||
(obj idx)
|
(obj idx)
|
||||||
(list (js-sym "js-get-prop") (js-transpile obj) (js-transpile idx))))
|
(list (js-sym "js-get-prop") (js-transpile obj) (js-transpile idx))))
|
||||||
|
|
||||||
;; ── Call ──────────────────────────────────────────────────────────
|
;; ── Arrow function ────────────────────────────────────────────────
|
||||||
|
|
||||||
;; JS `f(a, b, c)` → `(f a b c)` after transpile. Works for both
|
|
||||||
;; identifier calls and computed callee (arrow fn, member access).
|
|
||||||
(define
|
(define
|
||||||
js-transpile-call
|
js-transpile-call
|
||||||
(fn
|
(fn
|
||||||
@@ -320,8 +361,11 @@
|
|||||||
(js-transpile callee)
|
(js-transpile callee)
|
||||||
(js-transpile-args args))))))
|
(js-transpile-args args))))))
|
||||||
|
|
||||||
;; ── Array literal ─────────────────────────────────────────────────
|
;; ── Assignment ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; `a = b` on an ident → (set! a b).
|
||||||
|
;; `a += b` on an ident → (set! a (js-add a b)).
|
||||||
|
;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v).
|
||||||
(define
|
(define
|
||||||
js-transpile-new
|
js-transpile-new
|
||||||
(fn
|
(fn
|
||||||
@@ -331,11 +375,6 @@
|
|||||||
(js-transpile callee)
|
(js-transpile callee)
|
||||||
(cons (js-sym "list") (map js-transpile args)))))
|
(cons (js-sym "list") (map js-transpile args)))))
|
||||||
|
|
||||||
;; ── Object literal ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
;; Build a dict by `(dict)` + `dict-set!` inside a `let` that yields
|
|
||||||
;; the dict as its final expression. This keeps keys in JS insertion
|
|
||||||
;; order and allows computed values.
|
|
||||||
(define
|
(define
|
||||||
js-transpile-array
|
js-transpile-array
|
||||||
(fn
|
(fn
|
||||||
@@ -354,8 +393,6 @@
|
|||||||
elts))
|
elts))
|
||||||
(cons (js-sym "list") (map js-transpile elts)))))
|
(cons (js-sym "list") (map js-transpile elts)))))
|
||||||
|
|
||||||
;; ── Conditional ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-has-spread?
|
js-has-spread?
|
||||||
(fn
|
(fn
|
||||||
@@ -365,8 +402,9 @@
|
|||||||
((js-tag? (first lst) "js-spread") true)
|
((js-tag? (first lst) "js-spread") true)
|
||||||
(else (js-has-spread? (rest lst))))))
|
(else (js-has-spread? (rest lst))))))
|
||||||
|
|
||||||
;; ── Arrow function ────────────────────────────────────────────────
|
;; ── End-to-end entry points ───────────────────────────────────────
|
||||||
|
|
||||||
|
;; Transpile + eval a single JS expression string.
|
||||||
(define
|
(define
|
||||||
js-transpile-args
|
js-transpile-args
|
||||||
(fn
|
(fn
|
||||||
@@ -385,11 +423,8 @@
|
|||||||
args))
|
args))
|
||||||
(cons (js-sym "list") (map js-transpile args)))))
|
(cons (js-sym "list") (map js-transpile args)))))
|
||||||
|
|
||||||
;; ── Assignment ────────────────────────────────────────────────────
|
;; Transpile a JS expression string to SX source text (for inspection
|
||||||
|
;; in tests). Useful for asserting the exact emitted tree.
|
||||||
;; `a = b` on an ident → (set! a b).
|
|
||||||
;; `a += b` on an ident → (set! a (js-add a b)).
|
|
||||||
;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v).
|
|
||||||
(define
|
(define
|
||||||
js-transpile-object
|
js-transpile-object
|
||||||
(fn
|
(fn
|
||||||
@@ -451,9 +486,6 @@
|
|||||||
(append inits (list (js-transpile body))))))))
|
(append inits (list (js-transpile body))))))))
|
||||||
(list (js-sym "fn") param-syms body-tr))))
|
(list (js-sym "fn") param-syms body-tr))))
|
||||||
|
|
||||||
;; ── End-to-end entry points ───────────────────────────────────────
|
|
||||||
|
|
||||||
;; Transpile + eval a single JS expression string.
|
|
||||||
(define
|
(define
|
||||||
js-transpile-tpl
|
js-transpile-tpl
|
||||||
(fn
|
(fn
|
||||||
@@ -465,8 +497,6 @@
|
|||||||
(else
|
(else
|
||||||
(cons (js-sym "js-template-concat") (js-transpile-tpl-parts parts))))))
|
(cons (js-sym "js-template-concat") (js-transpile-tpl-parts parts))))))
|
||||||
|
|
||||||
;; Transpile a JS expression string to SX source text (for inspection
|
|
||||||
;; in tests). Useful for asserting the exact emitted tree.
|
|
||||||
(define
|
(define
|
||||||
js-transpile-tpl-parts
|
js-transpile-tpl-parts
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
Reference in New Issue
Block a user