js-on-sx: baseline commit (278/280 unit, 148/148 slice, runner stub)
Initial commit of the lib/js/ tree and plans/ directory. A previous session left template-string work in progress — 278/280 unit tests pass (2 failing: tpl part-count off-by-one, escaped-backtick ident lookup). test262-runner.py and scoreboard are placeholders (0/8 with 7 timeouts); fixing the runner is the next queue item.
This commit is contained in:
1
lib/js/.gitignore
vendored
Normal file
1
lib/js/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test262-upstream/
|
||||||
130
lib/js/conformance.sh
Executable file
130
lib/js/conformance.sh
Executable file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Cherry-picked test262 conformance runner for JS-on-SX.
|
||||||
|
# Walks lib/js/test262-slice/**/*.js, evaluates each via js-eval,
|
||||||
|
# and compares against the sibling .expected file (substring match).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash lib/js/conformance.sh # summary only
|
||||||
|
# bash lib/js/conformance.sh -v # per-test pass/fail
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||||
|
if [ ! -x "$SX_SERVER" ]; then
|
||||||
|
echo "ERROR: $SX_SERVER not found. Run: cd hosts/ocaml && dune build"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERBOSE="${1:-}"
|
||||||
|
SLICE_DIR="lib/js/test262-slice"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
ERRORS=""
|
||||||
|
|
||||||
|
# Find all .js fixtures (sorted for stable output).
|
||||||
|
# Skip README.md and similar.
|
||||||
|
mapfile -t FIXTURES < <(find "$SLICE_DIR" -type f -name '*.js' | sort)
|
||||||
|
|
||||||
|
if [ ${#FIXTURES[@]} -eq 0 ]; then
|
||||||
|
echo "No fixtures found in $SLICE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build one big batch script: load everything once, then one epoch per
|
||||||
|
# fixture. Avoids the ~200ms boot cost of starting the server for each
|
||||||
|
# test.
|
||||||
|
|
||||||
|
TMPFILE=$(mktemp)
|
||||||
|
trap "rm -f $TMPFILE" EXIT
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '(epoch 1)'
|
||||||
|
echo '(load "lib/r7rs.sx")'
|
||||||
|
echo '(epoch 2)'
|
||||||
|
echo '(load "lib/js/lexer.sx")'
|
||||||
|
echo '(epoch 3)'
|
||||||
|
echo '(load "lib/js/parser.sx")'
|
||||||
|
echo '(epoch 4)'
|
||||||
|
echo '(load "lib/js/transpile.sx")'
|
||||||
|
echo '(epoch 5)'
|
||||||
|
echo '(load "lib/js/runtime.sx")'
|
||||||
|
|
||||||
|
epoch=100
|
||||||
|
for f in "${FIXTURES[@]}"; do
|
||||||
|
# Read source, strip trailing newline, then escape for *two*
|
||||||
|
# nested SX string literals: the outer epoch `(eval "…")` and
|
||||||
|
# the inner `(js-eval "…")` that it wraps.
|
||||||
|
#
|
||||||
|
# Source char → final stream char
|
||||||
|
# \ → \\\\ (outer: becomes \\ ; inner: becomes \)
|
||||||
|
# " → \\\" (outer: becomes \" ; inner: becomes ")
|
||||||
|
# nl → \\n (SX newline escape, survives both levels)
|
||||||
|
src=$(python3 -c '
|
||||||
|
import sys
|
||||||
|
s = open(sys.argv[1], "r", encoding="utf-8").read().rstrip("\n")
|
||||||
|
# Two nested SX string literals: outer eval wraps inner js-eval.
|
||||||
|
# Escape once for inner (JS source → SX inner string literal):
|
||||||
|
inner = s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
|
||||||
|
# Escape the result again for the outer SX string literal:
|
||||||
|
outer = inner.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||||
|
sys.stdout.write(outer)
|
||||||
|
' "$f")
|
||||||
|
echo "(epoch $epoch)"
|
||||||
|
echo "(eval \"(js-eval \\\"$src\\\")\")"
|
||||||
|
epoch=$((epoch + 1))
|
||||||
|
done
|
||||||
|
} > "$TMPFILE"
|
||||||
|
|
||||||
|
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||||
|
|
||||||
|
# Iterate fixtures with the same epoch sequence and check each.
|
||||||
|
epoch=100
|
||||||
|
for f in "${FIXTURES[@]}"; do
|
||||||
|
expected=$(cat "${f%.js}.expected" | sed -e 's/[[:space:]]*$//' | head -n 1)
|
||||||
|
name="${f#${SLICE_DIR}/}"
|
||||||
|
name="${name%.js}"
|
||||||
|
|
||||||
|
# Actual output lives on the line after "(ok-len $epoch N)" or on
|
||||||
|
# "(ok $epoch VAL)" for short values. Errors surface as "(error …)".
|
||||||
|
actual=$(echo "$OUTPUT" | awk -v e="$epoch" '
|
||||||
|
$0 ~ ("^\\(ok-len "e" ") { getline; print; exit }
|
||||||
|
$0 ~ ("^\\(ok "e" ") { sub("^\\(ok "e" ", ""); sub(")$", ""); print; exit }
|
||||||
|
$0 ~ ("^\\(error "e" ") { print; exit }
|
||||||
|
')
|
||||||
|
[ -z "$actual" ] && actual="<no output>"
|
||||||
|
|
||||||
|
if echo "$actual" | grep -qF -- "$expected"; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
[ "$VERBOSE" = "-v" ] && echo " ✓ $name"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
ERRORS+=" ✗ $name
|
||||||
|
expected: $expected
|
||||||
|
actual: $actual
|
||||||
|
"
|
||||||
|
[ "$VERBOSE" = "-v" ] && echo " ✗ $name (expected: $expected, actual: $actual)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
epoch=$((epoch + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
TOTAL=$((PASS + FAIL))
|
||||||
|
PCT=$(awk "BEGIN{printf \"%.1f\", ($PASS/$TOTAL)*100}")
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ $FAIL -eq 0 ]; then
|
||||||
|
echo "✓ $PASS/$TOTAL test262-slice tests passed ($PCT%)"
|
||||||
|
else
|
||||||
|
echo "✗ $PASS/$TOTAL passed, $FAIL failed ($PCT%):"
|
||||||
|
[ "$VERBOSE" != "-v" ] && echo "$ERRORS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 5 target: ≥50% pass.
|
||||||
|
TARGET=50
|
||||||
|
if (( $(echo "$PCT >= $TARGET" | bc -l 2>/dev/null || python3 -c "print($PCT >= $TARGET)") )); then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "(below target of ${TARGET}%)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
519
lib/js/lexer.sx
Normal file
519
lib/js/lexer.sx
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
;; lib/js/lexer.sx — JavaScript source → token stream
|
||||||
|
;;
|
||||||
|
;; Tokens: {:type T :value V :pos P}
|
||||||
|
;; Types:
|
||||||
|
;; "number" — numeric literals (decoded into value as number)
|
||||||
|
;; "string" — string literals (decoded, escape sequences processed)
|
||||||
|
;; "template"— template literal body (no interpolation split yet — deferred)
|
||||||
|
;; "ident" — identifier (not a reserved word)
|
||||||
|
;; "keyword" — reserved word
|
||||||
|
;; "punct" — ( ) [ ] { } , ; : . ...
|
||||||
|
;; "op" — all operator tokens (incl. = == === !== < > etc.)
|
||||||
|
;; "eof" — end of input
|
||||||
|
;;
|
||||||
|
;; NOTE: `cond` clauses take exactly ONE body expression — multi-body
|
||||||
|
;; clauses must wrap their body in `(do ...)`.
|
||||||
|
|
||||||
|
;; ── Token constructor ─────────────────────────────────────────────
|
||||||
|
(define js-make-token (fn (type value pos) {:pos pos :value value :type type}))
|
||||||
|
|
||||||
|
;; ── Character predicates ──────────────────────────────────────────
|
||||||
|
(define js-digit? (fn (c) (and (>= c "0") (<= c "9"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
js-hex-digit?
|
||||||
|
(fn
|
||||||
|
(c)
|
||||||
|
(or
|
||||||
|
(js-digit? c)
|
||||||
|
(and (>= c "a") (<= c "f"))
|
||||||
|
(and (>= c "A") (<= c "F")))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
js-letter?
|
||||||
|
(fn (c) (or (and (>= c "a") (<= c "z")) (and (>= c "A") (<= c "Z")))))
|
||||||
|
|
||||||
|
(define js-ident-start? (fn (c) (or (js-letter? c) (= c "_") (= c "$"))))
|
||||||
|
|
||||||
|
(define js-ident-char? (fn (c) (or (js-ident-start? c) (js-digit? c))))
|
||||||
|
|
||||||
|
(define js-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
|
||||||
|
|
||||||
|
;; ── Reserved words ────────────────────────────────────────────────
|
||||||
|
(define
|
||||||
|
js-keywords
|
||||||
|
(list
|
||||||
|
"break"
|
||||||
|
"case"
|
||||||
|
"catch"
|
||||||
|
"class"
|
||||||
|
"const"
|
||||||
|
"continue"
|
||||||
|
"debugger"
|
||||||
|
"default"
|
||||||
|
"delete"
|
||||||
|
"do"
|
||||||
|
"else"
|
||||||
|
"export"
|
||||||
|
"extends"
|
||||||
|
"false"
|
||||||
|
"finally"
|
||||||
|
"for"
|
||||||
|
"function"
|
||||||
|
"if"
|
||||||
|
"import"
|
||||||
|
"in"
|
||||||
|
"instanceof"
|
||||||
|
"new"
|
||||||
|
"null"
|
||||||
|
"return"
|
||||||
|
"super"
|
||||||
|
"switch"
|
||||||
|
"this"
|
||||||
|
"throw"
|
||||||
|
"true"
|
||||||
|
"try"
|
||||||
|
"typeof"
|
||||||
|
"undefined"
|
||||||
|
"var"
|
||||||
|
"void"
|
||||||
|
"while"
|
||||||
|
"with"
|
||||||
|
"yield"
|
||||||
|
"let"
|
||||||
|
"static"
|
||||||
|
"async"
|
||||||
|
"await"
|
||||||
|
"of"))
|
||||||
|
|
||||||
|
(define js-keyword? (fn (word) (contains? js-keywords word)))
|
||||||
|
|
||||||
|
;; ── Main tokenizer ────────────────────────────────────────────────
|
||||||
|
(define
|
||||||
|
js-tokenize
|
||||||
|
(fn
|
||||||
|
(src)
|
||||||
|
(let
|
||||||
|
((tokens (list)) (pos 0) (src-len (len src)))
|
||||||
|
(define
|
||||||
|
js-peek
|
||||||
|
(fn
|
||||||
|
(offset)
|
||||||
|
(if (< (+ pos offset) src-len) (nth src (+ pos offset)) nil)))
|
||||||
|
(define cur (fn () (js-peek 0)))
|
||||||
|
(define advance! (fn (n) (set! pos (+ pos n))))
|
||||||
|
(define
|
||||||
|
at?
|
||||||
|
(fn
|
||||||
|
(s)
|
||||||
|
(let
|
||||||
|
((sl (len s)))
|
||||||
|
(and (<= (+ pos sl) src-len) (= (slice src pos (+ pos sl)) s)))))
|
||||||
|
(define
|
||||||
|
js-emit!
|
||||||
|
(fn
|
||||||
|
(type value start)
|
||||||
|
(append! tokens (js-make-token type value start))))
|
||||||
|
(define
|
||||||
|
skip-line-comment!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (not (= (cur) "\n")))
|
||||||
|
(do (advance! 1) (skip-line-comment!)))))
|
||||||
|
(define
|
||||||
|
skip-block-comment!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((and (= (cur) "*") (< (+ pos 1) src-len) (= (js-peek 1) "/"))
|
||||||
|
(advance! 2))
|
||||||
|
(else (do (advance! 1) (skip-block-comment!))))))
|
||||||
|
(define
|
||||||
|
skip-ws!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((js-ws? (cur)) (do (advance! 1) (skip-ws!)))
|
||||||
|
((and (= (cur) "/") (< (+ pos 1) src-len) (= (js-peek 1) "/"))
|
||||||
|
(do (advance! 2) (skip-line-comment!) (skip-ws!)))
|
||||||
|
((and (= (cur) "/") (< (+ pos 1) src-len) (= (js-peek 1) "*"))
|
||||||
|
(do (advance! 2) (skip-block-comment!) (skip-ws!)))
|
||||||
|
(else nil))))
|
||||||
|
(define
|
||||||
|
read-ident
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(do
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (js-ident-char? (cur)))
|
||||||
|
(do (advance! 1) (read-ident start)))
|
||||||
|
(slice src start pos))))
|
||||||
|
(define
|
||||||
|
read-decimal-digits!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (js-digit? (cur)))
|
||||||
|
(do (advance! 1) (read-decimal-digits!)))))
|
||||||
|
(define
|
||||||
|
read-hex-digits!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (js-hex-digit? (cur)))
|
||||||
|
(do (advance! 1) (read-hex-digits!)))))
|
||||||
|
(define
|
||||||
|
read-exp-part!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(and (< pos src-len) (or (= (cur) "e") (= (cur) "E")))
|
||||||
|
(let
|
||||||
|
((p1 (js-peek 1)))
|
||||||
|
(when
|
||||||
|
(or
|
||||||
|
(and (not (= p1 nil)) (js-digit? p1))
|
||||||
|
(and
|
||||||
|
(or (= p1 "+") (= p1 "-"))
|
||||||
|
(< (+ pos 2) src-len)
|
||||||
|
(js-digit? (js-peek 2))))
|
||||||
|
(do
|
||||||
|
(advance! 1)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(or (= (cur) "+") (= (cur) "-")))
|
||||||
|
(advance! 1))
|
||||||
|
(read-decimal-digits!)))))))
|
||||||
|
(define
|
||||||
|
read-number
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(cond
|
||||||
|
((and (= (cur) "0") (< (+ pos 1) src-len) (or (= (js-peek 1) "x") (= (js-peek 1) "X")))
|
||||||
|
(do
|
||||||
|
(advance! 2)
|
||||||
|
(read-hex-digits!)
|
||||||
|
(let
|
||||||
|
((raw (slice src (+ start 2) pos)))
|
||||||
|
(parse-number (str "0x" raw)))))
|
||||||
|
(else
|
||||||
|
(do
|
||||||
|
(read-decimal-digits!)
|
||||||
|
(when
|
||||||
|
(and
|
||||||
|
(< pos src-len)
|
||||||
|
(= (cur) ".")
|
||||||
|
(< (+ pos 1) src-len)
|
||||||
|
(js-digit? (js-peek 1)))
|
||||||
|
(do (advance! 1) (read-decimal-digits!)))
|
||||||
|
(read-exp-part!)
|
||||||
|
(parse-number (slice src start pos)))))))
|
||||||
|
(define
|
||||||
|
read-dot-number
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(do
|
||||||
|
(advance! 1)
|
||||||
|
(read-decimal-digits!)
|
||||||
|
(read-exp-part!)
|
||||||
|
(parse-number (slice src start pos)))))
|
||||||
|
(define
|
||||||
|
read-string
|
||||||
|
(fn
|
||||||
|
(quote-char)
|
||||||
|
(let
|
||||||
|
((chars (list)))
|
||||||
|
(advance! 1)
|
||||||
|
(define
|
||||||
|
loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((= (cur) "\\")
|
||||||
|
(do
|
||||||
|
(advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur)))
|
||||||
|
(do
|
||||||
|
(cond
|
||||||
|
((= ch "n") (append! chars "\n"))
|
||||||
|
((= ch "t") (append! chars "\t"))
|
||||||
|
((= ch "r") (append! chars "\r"))
|
||||||
|
((= ch "\\") (append! chars "\\"))
|
||||||
|
((= ch "'") (append! chars "'"))
|
||||||
|
((= ch "\"") (append! chars "\""))
|
||||||
|
((= ch "`") (append! chars "`"))
|
||||||
|
((= ch "0") (append! chars "\\0"))
|
||||||
|
((= ch "b") (append! chars "\\b"))
|
||||||
|
((= ch "f") (append! chars "\\f"))
|
||||||
|
((= ch "v") (append! chars "\\v"))
|
||||||
|
(else (append! chars ch)))
|
||||||
|
(advance! 1))))
|
||||||
|
(loop)))
|
||||||
|
((= (cur) quote-char) (advance! 1))
|
||||||
|
(else (do (append! chars (cur)) (advance! 1) (loop))))))
|
||||||
|
(loop)
|
||||||
|
(join "" chars))))
|
||||||
|
(define
|
||||||
|
read-template
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((parts (list)) (chars (list)))
|
||||||
|
(advance! 1)
|
||||||
|
(define
|
||||||
|
flush-chars!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(when
|
||||||
|
(> (len chars) 0)
|
||||||
|
(do
|
||||||
|
(append! parts (list "str" (join "" chars)))
|
||||||
|
(set! chars (list))))))
|
||||||
|
(define
|
||||||
|
read-expr-source!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((buf (list)) (depth 1))
|
||||||
|
(define
|
||||||
|
expr-loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((and (= (cur) "}") (= depth 1)) (advance! 1))
|
||||||
|
((= (cur) "}")
|
||||||
|
(do
|
||||||
|
(append! buf (cur))
|
||||||
|
(set! depth (- depth 1))
|
||||||
|
(advance! 1)
|
||||||
|
(expr-loop)))
|
||||||
|
((= (cur) "{")
|
||||||
|
(do
|
||||||
|
(append! buf (cur))
|
||||||
|
(set! depth (+ depth 1))
|
||||||
|
(advance! 1)
|
||||||
|
(expr-loop)))
|
||||||
|
((or (= (cur) "\"") (= (cur) "'"))
|
||||||
|
(let
|
||||||
|
((q (cur)))
|
||||||
|
(do
|
||||||
|
(append! buf q)
|
||||||
|
(advance! 1)
|
||||||
|
(define
|
||||||
|
sloop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((= (cur) "\\")
|
||||||
|
(do
|
||||||
|
(append! buf (cur))
|
||||||
|
(advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(do
|
||||||
|
(append! buf (cur))
|
||||||
|
(advance! 1)))
|
||||||
|
(sloop)))
|
||||||
|
((= (cur) q)
|
||||||
|
(do (append! buf (cur)) (advance! 1)))
|
||||||
|
(else
|
||||||
|
(do
|
||||||
|
(append! buf (cur))
|
||||||
|
(advance! 1)
|
||||||
|
(sloop))))))
|
||||||
|
(sloop)
|
||||||
|
(expr-loop))))
|
||||||
|
(else
|
||||||
|
(do (append! buf (cur)) (advance! 1) (expr-loop))))))
|
||||||
|
(expr-loop)
|
||||||
|
(join "" buf))))
|
||||||
|
(define
|
||||||
|
loop
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(cond
|
||||||
|
((>= pos src-len) nil)
|
||||||
|
((= (cur) "`") (advance! 1))
|
||||||
|
((and (= (cur) "$") (< (+ pos 1) src-len) (= (js-peek 1) "{"))
|
||||||
|
(do
|
||||||
|
(flush-chars!)
|
||||||
|
(advance! 2)
|
||||||
|
(let
|
||||||
|
((src (read-expr-source!)))
|
||||||
|
(append! parts (list "expr" src)))
|
||||||
|
(loop)))
|
||||||
|
((= (cur) "\\")
|
||||||
|
(do
|
||||||
|
(advance! 1)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur)))
|
||||||
|
(do
|
||||||
|
(cond
|
||||||
|
((= ch "n") (append! chars "\n"))
|
||||||
|
((= ch "t") (append! chars "\t"))
|
||||||
|
((= ch "r") (append! chars "\r"))
|
||||||
|
((= ch "\\") (append! chars "\\"))
|
||||||
|
((= ch "'") (append! chars "'"))
|
||||||
|
((= ch "\"") (append! chars "\""))
|
||||||
|
((= ch "`") (append! chars "`"))
|
||||||
|
((= ch "$") (append! chars "$"))
|
||||||
|
((= ch "0") (append! chars "0"))
|
||||||
|
((= ch "b") (append! chars "b"))
|
||||||
|
((= ch "f") (append! chars "f"))
|
||||||
|
((= ch "v") (append! chars "v"))
|
||||||
|
(else (append! chars ch)))
|
||||||
|
(advance! 1))))
|
||||||
|
(loop)))
|
||||||
|
(else (do (append! chars (cur)) (advance! 1) (loop))))))
|
||||||
|
(loop)
|
||||||
|
(flush-chars!)
|
||||||
|
(if
|
||||||
|
(= (len parts) 0)
|
||||||
|
""
|
||||||
|
(if
|
||||||
|
(and (= (len parts) 1) (= (nth (nth parts 0) 0) "str"))
|
||||||
|
(nth (nth parts 0) 1)
|
||||||
|
parts)))))
|
||||||
|
(define
|
||||||
|
try-op-4!
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(cond
|
||||||
|
((at? ">>>=")
|
||||||
|
(do (js-emit! "op" ">>>=" start) (advance! 4) true))
|
||||||
|
(else false))))
|
||||||
|
(define
|
||||||
|
try-op-3!
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(cond
|
||||||
|
((at? "===")
|
||||||
|
(do (js-emit! "op" "===" start) (advance! 3) true))
|
||||||
|
((at? "!==")
|
||||||
|
(do (js-emit! "op" "!==" start) (advance! 3) true))
|
||||||
|
((at? "**=")
|
||||||
|
(do (js-emit! "op" "**=" start) (advance! 3) true))
|
||||||
|
((at? "<<=")
|
||||||
|
(do (js-emit! "op" "<<=" start) (advance! 3) true))
|
||||||
|
((at? ">>=")
|
||||||
|
(do (js-emit! "op" ">>=" start) (advance! 3) true))
|
||||||
|
((at? ">>>")
|
||||||
|
(do (js-emit! "op" ">>>" start) (advance! 3) true))
|
||||||
|
((at? "&&=")
|
||||||
|
(do (js-emit! "op" "&&=" start) (advance! 3) true))
|
||||||
|
((at? "||=")
|
||||||
|
(do (js-emit! "op" "||=" start) (advance! 3) true))
|
||||||
|
((at? "??=")
|
||||||
|
(do (js-emit! "op" "??=" start) (advance! 3) true))
|
||||||
|
((at? "...")
|
||||||
|
(do (js-emit! "punct" "..." start) (advance! 3) true))
|
||||||
|
(else false))))
|
||||||
|
(define
|
||||||
|
try-op-2!
|
||||||
|
(fn
|
||||||
|
(start)
|
||||||
|
(cond
|
||||||
|
((at? "==") (do (js-emit! "op" "==" start) (advance! 2) true))
|
||||||
|
((at? "!=") (do (js-emit! "op" "!=" start) (advance! 2) true))
|
||||||
|
((at? "<=") (do (js-emit! "op" "<=" start) (advance! 2) true))
|
||||||
|
((at? ">=") (do (js-emit! "op" ">=" start) (advance! 2) true))
|
||||||
|
((at? "&&") (do (js-emit! "op" "&&" start) (advance! 2) true))
|
||||||
|
((at? "||") (do (js-emit! "op" "||" start) (advance! 2) true))
|
||||||
|
((at? "??") (do (js-emit! "op" "??" start) (advance! 2) true))
|
||||||
|
((at? "=>") (do (js-emit! "op" "=>" start) (advance! 2) true))
|
||||||
|
((at? "**") (do (js-emit! "op" "**" start) (advance! 2) true))
|
||||||
|
((at? "<<") (do (js-emit! "op" "<<" start) (advance! 2) true))
|
||||||
|
((at? ">>") (do (js-emit! "op" ">>" start) (advance! 2) true))
|
||||||
|
((at? "++") (do (js-emit! "op" "++" start) (advance! 2) true))
|
||||||
|
((at? "--") (do (js-emit! "op" "--" start) (advance! 2) true))
|
||||||
|
((at? "+=") (do (js-emit! "op" "+=" start) (advance! 2) true))
|
||||||
|
((at? "-=") (do (js-emit! "op" "-=" start) (advance! 2) true))
|
||||||
|
((at? "*=") (do (js-emit! "op" "*=" start) (advance! 2) true))
|
||||||
|
((at? "/=") (do (js-emit! "op" "/=" start) (advance! 2) true))
|
||||||
|
((at? "%=") (do (js-emit! "op" "%=" start) (advance! 2) true))
|
||||||
|
((at? "&=") (do (js-emit! "op" "&=" start) (advance! 2) true))
|
||||||
|
((at? "|=") (do (js-emit! "op" "|=" start) (advance! 2) true))
|
||||||
|
((at? "^=") (do (js-emit! "op" "^=" start) (advance! 2) true))
|
||||||
|
((at? "?.") (do (js-emit! "op" "?." start) (advance! 2) true))
|
||||||
|
(else false))))
|
||||||
|
(define
|
||||||
|
emit-one-op!
|
||||||
|
(fn
|
||||||
|
(ch start)
|
||||||
|
(cond
|
||||||
|
((= ch "(") (do (js-emit! "punct" "(" start) (advance! 1)))
|
||||||
|
((= ch ")") (do (js-emit! "punct" ")" start) (advance! 1)))
|
||||||
|
((= ch "[") (do (js-emit! "punct" "[" start) (advance! 1)))
|
||||||
|
((= ch "]") (do (js-emit! "punct" "]" start) (advance! 1)))
|
||||||
|
((= ch "{") (do (js-emit! "punct" "{" start) (advance! 1)))
|
||||||
|
((= ch "}") (do (js-emit! "punct" "}" start) (advance! 1)))
|
||||||
|
((= ch ",") (do (js-emit! "punct" "," start) (advance! 1)))
|
||||||
|
((= ch ";") (do (js-emit! "punct" ";" start) (advance! 1)))
|
||||||
|
((= ch ":") (do (js-emit! "punct" ":" start) (advance! 1)))
|
||||||
|
((= ch ".") (do (js-emit! "punct" "." start) (advance! 1)))
|
||||||
|
((= ch "?") (do (js-emit! "op" "?" start) (advance! 1)))
|
||||||
|
((= ch "+") (do (js-emit! "op" "+" start) (advance! 1)))
|
||||||
|
((= ch "-") (do (js-emit! "op" "-" start) (advance! 1)))
|
||||||
|
((= ch "*") (do (js-emit! "op" "*" start) (advance! 1)))
|
||||||
|
((= ch "/") (do (js-emit! "op" "/" start) (advance! 1)))
|
||||||
|
((= ch "%") (do (js-emit! "op" "%" start) (advance! 1)))
|
||||||
|
((= ch "=") (do (js-emit! "op" "=" start) (advance! 1)))
|
||||||
|
((= ch "<") (do (js-emit! "op" "<" start) (advance! 1)))
|
||||||
|
((= ch ">") (do (js-emit! "op" ">" start) (advance! 1)))
|
||||||
|
((= ch "!") (do (js-emit! "op" "!" start) (advance! 1)))
|
||||||
|
((= ch "&") (do (js-emit! "op" "&" start) (advance! 1)))
|
||||||
|
((= ch "|") (do (js-emit! "op" "|" start) (advance! 1)))
|
||||||
|
((= ch "^") (do (js-emit! "op" "^" start) (advance! 1)))
|
||||||
|
((= ch "~") (do (js-emit! "op" "~" start) (advance! 1)))
|
||||||
|
(else (advance! 1)))))
|
||||||
|
(define
|
||||||
|
scan!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(do
|
||||||
|
(skip-ws!)
|
||||||
|
(when
|
||||||
|
(< pos src-len)
|
||||||
|
(let
|
||||||
|
((ch (cur)) (start pos))
|
||||||
|
(cond
|
||||||
|
((or (= ch "\"") (= ch "'"))
|
||||||
|
(do (js-emit! "string" (read-string ch) start) (scan!)))
|
||||||
|
((= ch "`")
|
||||||
|
(do (js-emit! "template" (read-template) start) (scan!)))
|
||||||
|
((js-digit? ch)
|
||||||
|
(do
|
||||||
|
(js-emit! "number" (read-number start) start)
|
||||||
|
(scan!)))
|
||||||
|
((and (= ch ".") (< (+ pos 1) src-len) (js-digit? (js-peek 1)))
|
||||||
|
(do
|
||||||
|
(js-emit! "number" (read-dot-number start) start)
|
||||||
|
(scan!)))
|
||||||
|
((js-ident-start? ch)
|
||||||
|
(do
|
||||||
|
(let
|
||||||
|
((word (read-ident start)))
|
||||||
|
(js-emit!
|
||||||
|
(if (js-keyword? word) "keyword" "ident")
|
||||||
|
word
|
||||||
|
start))
|
||||||
|
(scan!)))
|
||||||
|
((try-op-4! start) (scan!))
|
||||||
|
((try-op-3! start) (scan!))
|
||||||
|
((try-op-2! start) (scan!))
|
||||||
|
(else (do (emit-one-op! ch start) (scan!)))))))))
|
||||||
|
(scan!)
|
||||||
|
(js-emit! "eof" nil pos)
|
||||||
|
tokens)))
|
||||||
1097
lib/js/parser.sx
Normal file
1097
lib/js/parser.sx
Normal file
File diff suppressed because it is too large
Load Diff
1596
lib/js/runtime.sx
Normal file
1596
lib/js/runtime.sx
Normal file
File diff suppressed because it is too large
Load Diff
1156
lib/js/test.sh
Executable file
1156
lib/js/test.sh
Executable file
File diff suppressed because it is too large
Load Diff
711
lib/js/test262-runner.py
Normal file
711
lib/js/test262-runner.py
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
test262-runner — run the official TC39 test262 suite against our JS-on-SX runtime.
|
||||||
|
|
||||||
|
Walks lib/js/test262-upstream/test/**/*.js, parses YAML-ish frontmatter, batches
|
||||||
|
tests through sx_server.exe, and emits a JSON + Markdown scoreboard.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 lib/js/test262-runner.py # full run
|
||||||
|
python3 lib/js/test262-runner.py --limit 2000 # first 2000 tests only
|
||||||
|
python3 lib/js/test262-runner.py --filter built-ins/Math
|
||||||
|
python3 lib/js/test262-runner.py --batch-size 200 # tests per sx_server boot
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
lib/js/test262-scoreboard.json — per-category stats + top failure modes
|
||||||
|
lib/js/test262-scoreboard.md — human-readable summary (worst first)
|
||||||
|
|
||||||
|
Pinned to commit (see test262-upstream/.git/HEAD after clone). Update:
|
||||||
|
rm -rf lib/js/test262-upstream
|
||||||
|
git -C lib/js clone --depth 1 https://github.com/tc39/test262.git test262-upstream
|
||||||
|
|
||||||
|
Timeouts:
|
||||||
|
per-test wallclock: 5s
|
||||||
|
per-batch wallclock: 120s
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[2]
|
||||||
|
SX_SERVER = REPO / "hosts" / "ocaml" / "_build" / "default" / "bin" / "sx_server.exe"
|
||||||
|
UPSTREAM = REPO / "lib" / "js" / "test262-upstream"
|
||||||
|
TEST_ROOT = UPSTREAM / "test"
|
||||||
|
HARNESS_DIR = UPSTREAM / "harness"
|
||||||
|
|
||||||
|
# Default harness files every test implicitly gets (per INTERPRETING.md).
|
||||||
|
DEFAULT_HARNESS = ["assert.js", "sta.js"]
|
||||||
|
|
||||||
|
# Per-batch timeout (seconds). Each batch runs N tests; if sx_server hangs on
|
||||||
|
# one, we kill the whole batch and mark remaining as timeout.
|
||||||
|
BATCH_TIMEOUT_S = 120
|
||||||
|
|
||||||
|
# Per-test wallclock is enforced by slicing batches: if a batch of N tests
|
||||||
|
# takes > PER_TEST_S * N + slack, it's killed. We also record elapsed time
|
||||||
|
# per test by parsing the output stream.
|
||||||
|
PER_TEST_S = 5
|
||||||
|
|
||||||
|
# Target batch size — tune to balance sx_server startup cost (~500ms) against
|
||||||
|
# memory / risk of one bad test killing many.
|
||||||
|
DEFAULT_BATCH_SIZE = 200
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Frontmatter parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FRONTMATTER_RE = re.compile(r"/\*---(.*?)---\*/", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Frontmatter:
|
||||||
|
description: str = ""
|
||||||
|
flags: list[str] = dataclasses.field(default_factory=list)
|
||||||
|
includes: list[str] = dataclasses.field(default_factory=list)
|
||||||
|
features: list[str] = dataclasses.field(default_factory=list)
|
||||||
|
negative_phase: str | None = None
|
||||||
|
negative_type: str | None = None
|
||||||
|
esid: str | None = None
|
||||||
|
es5id: str | None = None
|
||||||
|
es6id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_yaml_list(s: str) -> list[str]:
|
||||||
|
"""Parse a `[a, b, c]` style list. Loose — test262 YAML uses this form almost exclusively."""
|
||||||
|
s = s.strip()
|
||||||
|
if s.startswith("[") and s.endswith("]"):
|
||||||
|
s = s[1:-1]
|
||||||
|
return [item.strip().strip('"').strip("'") for item in s.split(",") if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frontmatter(src: str) -> Frontmatter:
|
||||||
|
"""Parse test262 YAML-ish frontmatter. Lenient — handles the subset actually in use."""
|
||||||
|
fm = Frontmatter()
|
||||||
|
m = FRONTMATTER_RE.search(src)
|
||||||
|
if not m:
|
||||||
|
return fm
|
||||||
|
body = m.group(1)
|
||||||
|
|
||||||
|
# Walk lines, tracking indent for nested negative: {phase, type}.
|
||||||
|
lines = body.split("\n")
|
||||||
|
i = 0
|
||||||
|
current_key = None
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
# Top-level key: value
|
||||||
|
m2 = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$", line)
|
||||||
|
if m2 and not line.startswith(" ") and not line.startswith("\t"):
|
||||||
|
key, value = m2.group(1), m2.group(2).strip()
|
||||||
|
if key == "description":
|
||||||
|
# Multi-line description supported via `>` or `|`
|
||||||
|
if value in (">", "|"):
|
||||||
|
desc_lines: list[str] = []
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines):
|
||||||
|
nxt = lines[j]
|
||||||
|
if nxt.startswith(" ") or nxt.startswith("\t") or not nxt.strip():
|
||||||
|
desc_lines.append(nxt.strip())
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
fm.description = " ".join(d for d in desc_lines if d)
|
||||||
|
i = j
|
||||||
|
continue
|
||||||
|
fm.description = value
|
||||||
|
elif key == "flags":
|
||||||
|
fm.flags = _parse_yaml_list(value)
|
||||||
|
elif key == "includes":
|
||||||
|
fm.includes = _parse_yaml_list(value)
|
||||||
|
elif key == "features":
|
||||||
|
fm.features = _parse_yaml_list(value)
|
||||||
|
elif key == "negative":
|
||||||
|
# Either `negative: {phase: parse, type: SyntaxError}` (inline)
|
||||||
|
# or spans two indented lines.
|
||||||
|
if value.startswith("{"):
|
||||||
|
# Inline dict
|
||||||
|
inner = value.strip("{}")
|
||||||
|
for part in inner.split(","):
|
||||||
|
if ":" in part:
|
||||||
|
pk, pv = part.split(":", 1)
|
||||||
|
pk = pk.strip()
|
||||||
|
pv = pv.strip().strip('"').strip("'")
|
||||||
|
if pk == "phase":
|
||||||
|
fm.negative_phase = pv
|
||||||
|
elif pk == "type":
|
||||||
|
fm.negative_type = pv
|
||||||
|
else:
|
||||||
|
current_key = "negative"
|
||||||
|
elif key == "esid":
|
||||||
|
fm.esid = value
|
||||||
|
elif key == "es5id":
|
||||||
|
fm.es5id = value
|
||||||
|
elif key == "es6id":
|
||||||
|
fm.es6id = value
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
# Indented continuation — e.g., negative: {phase:..., type:...}
|
||||||
|
if current_key == "negative":
|
||||||
|
m3 = re.match(r"^\s+([a-zA-Z_]+)\s*:\s*(.*)$", line)
|
||||||
|
if m3:
|
||||||
|
pk, pv = m3.group(1), m3.group(2).strip().strip('"').strip("'")
|
||||||
|
if pk == "phase":
|
||||||
|
fm.negative_phase = pv
|
||||||
|
elif pk == "type":
|
||||||
|
fm.negative_type = pv
|
||||||
|
else:
|
||||||
|
current_key = None
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return fm
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Harness loading
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_HARNESS_CACHE: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_harness(name: str) -> str:
|
||||||
|
if name not in _HARNESS_CACHE:
|
||||||
|
p = HARNESS_DIR / name
|
||||||
|
if p.exists():
|
||||||
|
_HARNESS_CACHE[name] = p.read_text(encoding="utf-8")
|
||||||
|
else:
|
||||||
|
_HARNESS_CACHE[name] = ""
|
||||||
|
return _HARNESS_CACHE[name]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Categories
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_category(test_path: Path) -> str:
|
||||||
|
"""Derive a category like 'built-ins/Math' from the test path."""
|
||||||
|
rel = test_path.relative_to(TEST_ROOT).as_posix()
|
||||||
|
parts = rel.split("/")
|
||||||
|
# Use at most 2 levels; e.g. built-ins/Math/abs/foo.js → built-ins/Math
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return "/".join(parts[:2])
|
||||||
|
return parts[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SX escaping
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def sx_escape_double(s: str) -> str:
|
||||||
|
"""Escape for a single SX string literal. Turn bytes that break SX parsing into escapes."""
|
||||||
|
return (
|
||||||
|
s.replace("\\", "\\\\")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sx_double_escape(s: str) -> str:
|
||||||
|
"""Escape a JS source string for the nested `(eval "(js-eval \"...\")")` form.
|
||||||
|
|
||||||
|
Two levels of SX string-literal escaping. Matches conformance.sh.
|
||||||
|
"""
|
||||||
|
inner = sx_escape_double(s)
|
||||||
|
# The inner string gets consumed by the outer `(eval "...")`, so we need
|
||||||
|
# to escape backslashes and quotes again.
|
||||||
|
outer = inner.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
return outer
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test assembly
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# A tiny helper we prepend so assert.X = function syntax has a hope. The real
|
||||||
|
# test262 assert.js does `assert.sameValue = function(...){}` which requires
|
||||||
|
# function-property support. Our runtime doesn't have that yet, so many tests
|
||||||
|
# will fail — that's the point of the scoreboard.
|
||||||
|
#
|
||||||
|
# We don't patch. We run the real harness as-is so the numbers reflect reality.
|
||||||
|
|
||||||
|
|
||||||
|
def assemble_source(test_src: str, includes: list[str]) -> str:
|
||||||
|
"""Assemble the full JS source for a test: harness preludes + test."""
|
||||||
|
chunks: list[str] = []
|
||||||
|
for h in DEFAULT_HARNESS:
|
||||||
|
chunks.append(load_harness(h))
|
||||||
|
for inc in includes:
|
||||||
|
chunks.append(load_harness(inc))
|
||||||
|
chunks.append(test_src)
|
||||||
|
return "\n".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output parsing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Output from sx_server looks like:
|
||||||
|
# (ready)
|
||||||
|
# (ok 1 2) -- short value: (ok EPOCH VALUE)
|
||||||
|
# (ok-len 100 42) -- long value: next line has the value
|
||||||
|
# NEXT_LINE_WITH_VALUE
|
||||||
|
# (error 101 "msg") -- epoch errored
|
||||||
|
#
|
||||||
|
# For our purposes, each test has an epoch. We look up the ok/error result
|
||||||
|
# and classify as pass/fail.
|
||||||
|
|
||||||
|
|
||||||
|
def parse_output(output: str) -> dict[int, tuple[str, str]]:
|
||||||
|
"""Return {epoch: (kind, payload)} where kind is 'ok' | 'error' | 'missing'."""
|
||||||
|
results: dict[int, tuple[str, str]] = {}
|
||||||
|
lines = output.split("\n")
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
m_ok = re.match(r"^\(ok (\d+) (.*)\)$", line)
|
||||||
|
m_oklen = re.match(r"^\(ok-len (\d+) \d+\)$", line)
|
||||||
|
m_err = re.match(r"^\(error (\d+) (.*)\)$", line)
|
||||||
|
if m_ok:
|
||||||
|
epoch = int(m_ok.group(1))
|
||||||
|
results[epoch] = ("ok", m_ok.group(2))
|
||||||
|
elif m_oklen:
|
||||||
|
epoch = int(m_oklen.group(1))
|
||||||
|
val = lines[i + 1] if i + 1 < len(lines) else ""
|
||||||
|
results[epoch] = ("ok", val)
|
||||||
|
i += 1
|
||||||
|
elif m_err:
|
||||||
|
epoch = int(m_err.group(1))
|
||||||
|
results[epoch] = ("error", m_err.group(2))
|
||||||
|
i += 1
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Classification
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def classify_error(msg: str) -> str:
|
||||||
|
"""Bucket an error message into a failure mode."""
|
||||||
|
m = msg.lower()
|
||||||
|
if "syntaxerror" in m or "parse" in m or "expected" in m and "got" in m:
|
||||||
|
return "SyntaxError (parse/unsupported syntax)"
|
||||||
|
if "referenceerror" in m or "undefined symbol" in m or "unbound" in m:
|
||||||
|
return "ReferenceError (undefined symbol)"
|
||||||
|
if "typeerror" in m and "not a function" in m:
|
||||||
|
return "TypeError: not a function"
|
||||||
|
if "typeerror" in m:
|
||||||
|
return "TypeError (other)"
|
||||||
|
if "rangeerror" in m:
|
||||||
|
return "RangeError"
|
||||||
|
if "test262error" in m:
|
||||||
|
return "Test262Error (assertion failed)"
|
||||||
|
if "timeout" in m:
|
||||||
|
return "Timeout"
|
||||||
|
if "killed" in m or "crash" in m:
|
||||||
|
return "Crash"
|
||||||
|
if "unhandled exception" in m:
|
||||||
|
# Could be almost anything — extract the inner message.
|
||||||
|
inner = re.search(r"Unhandled exception:\s*\\?\"([^\"]{0,80})", msg)
|
||||||
|
if inner:
|
||||||
|
return f"Unhandled: {inner.group(1)[:60]}"
|
||||||
|
return "Unhandled exception"
|
||||||
|
return f"Other: {msg[:80]}"
|
||||||
|
|
||||||
|
|
||||||
|
def classify_negative_result(
|
||||||
|
fm: Frontmatter, kind: str, payload: str
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""For negative tests: pass if the right error was thrown."""
|
||||||
|
expected_type = fm.negative_type or ""
|
||||||
|
if kind == "error":
|
||||||
|
# We throw; check if it matches. Our error messages look like:
|
||||||
|
# Unhandled exception: "...TypeError..."
|
||||||
|
if expected_type and expected_type.lower() in payload.lower():
|
||||||
|
return True, f"negative: threw {expected_type} as expected"
|
||||||
|
# Also consider "Test262Error" a match for anything (assertion failed
|
||||||
|
# instead of throw) — some negative tests assert more than just the throw.
|
||||||
|
return False, f"negative: expected {expected_type}, got: {payload[:100]}"
|
||||||
|
# ok → the test ran without throwing; that's a fail for negative tests
|
||||||
|
return False, f"negative: expected {expected_type}, but test completed normally"
|
||||||
|
|
||||||
|
|
||||||
|
def classify_positive_result(kind: str, payload: str) -> tuple[bool, str]:
|
||||||
|
"""For positive tests: pass if no error thrown."""
|
||||||
|
if kind == "ok":
|
||||||
|
return True, "passed"
|
||||||
|
return False, classify_error(payload)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Batch execution
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class TestCase:
|
||||||
|
path: Path
|
||||||
|
rel: str
|
||||||
|
category: str
|
||||||
|
fm: Frontmatter
|
||||||
|
src: str # Test source (pre-harness); full source assembled at run time.
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class TestResult:
|
||||||
|
rel: str
|
||||||
|
category: str
|
||||||
|
status: str # pass | fail | skip | timeout
|
||||||
|
reason: str
|
||||||
|
elapsed_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_batch_script(tests: list[TestCase], start_epoch: int) -> tuple[str, list[int]]:
|
||||||
|
"""Build one big SX script that loads the kernel once, then runs each test
|
||||||
|
in its own epoch. Returns (script, [epoch_per_test])."""
|
||||||
|
lines = []
|
||||||
|
lines.append("(epoch 1)")
|
||||||
|
lines.append('(load "lib/r7rs.sx")')
|
||||||
|
lines.append("(epoch 2)")
|
||||||
|
lines.append('(load "lib/js/lexer.sx")')
|
||||||
|
lines.append("(epoch 3)")
|
||||||
|
lines.append('(load "lib/js/parser.sx")')
|
||||||
|
lines.append("(epoch 4)")
|
||||||
|
lines.append('(load "lib/js/transpile.sx")')
|
||||||
|
lines.append("(epoch 5)")
|
||||||
|
lines.append('(load "lib/js/runtime.sx")')
|
||||||
|
|
||||||
|
epochs: list[int] = []
|
||||||
|
epoch = start_epoch
|
||||||
|
for t in tests:
|
||||||
|
full_src = assemble_source(t.src, t.fm.includes)
|
||||||
|
escaped = sx_double_escape(full_src)
|
||||||
|
lines.append(f"(epoch {epoch})")
|
||||||
|
lines.append(f'(eval "(js-eval \\"{escaped}\\")")')
|
||||||
|
epochs.append(epoch)
|
||||||
|
epoch += 1
|
||||||
|
return "\n".join(lines) + "\n", epochs
|
||||||
|
|
||||||
|
|
||||||
|
def run_batch(
|
||||||
|
tests: list[TestCase], start_epoch: int, timeout_s: int
|
||||||
|
) -> tuple[dict[int, tuple[str, str]], bool, float]:
|
||||||
|
"""Run a batch; return (results, timed_out, elapsed_s)."""
|
||||||
|
script, epochs = build_batch_script(tests, start_epoch)
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[str(SX_SERVER)],
|
||||||
|
input=script,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout_s,
|
||||||
|
cwd=str(REPO),
|
||||||
|
)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
return parse_output(proc.stdout), False, elapsed
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
# Partial output may still be parseable
|
||||||
|
stdout = (e.stdout or b"").decode("utf-8", errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or "")
|
||||||
|
return parse_output(stdout), True, elapsed
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main loop
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def discover_tests(filter_prefix: str | None) -> list[Path]:
|
||||||
|
"""Walk test262/test/**/*.js, skipping _FIXTURE files and _FIXTURE dirs."""
|
||||||
|
tests: list[Path] = []
|
||||||
|
for p in TEST_ROOT.rglob("*.js"):
|
||||||
|
if p.name.endswith("_FIXTURE.js"):
|
||||||
|
continue
|
||||||
|
if "_FIXTURE" in p.parts:
|
||||||
|
continue
|
||||||
|
if filter_prefix:
|
||||||
|
rel = p.relative_to(TEST_ROOT).as_posix()
|
||||||
|
if not rel.startswith(filter_prefix):
|
||||||
|
continue
|
||||||
|
tests.append(p)
|
||||||
|
tests.sort()
|
||||||
|
return tests
|
||||||
|
|
||||||
|
|
||||||
|
def load_test(path: Path) -> TestCase | None:
|
||||||
|
"""Load + parse frontmatter. Returns None on read error."""
|
||||||
|
try:
|
||||||
|
src = path.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
fm = parse_frontmatter(src)
|
||||||
|
return TestCase(
|
||||||
|
path=path,
|
||||||
|
rel=path.relative_to(TEST_ROOT).as_posix(),
|
||||||
|
category=test_category(path),
|
||||||
|
fm=fm,
|
||||||
|
src=src,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip(t: TestCase) -> tuple[bool, str]:
|
||||||
|
"""Skip tests we know we can't run or are explicitly excluded."""
|
||||||
|
# Strict-mode tests — we don't support strict mode, so these are noise.
|
||||||
|
if "onlyStrict" in t.fm.flags:
|
||||||
|
return True, "strict-mode only (not supported)"
|
||||||
|
# module flag — ESM tests not supported
|
||||||
|
if "module" in t.fm.flags:
|
||||||
|
return True, "ESM module (not supported)"
|
||||||
|
# async tests time out easily without a proper event loop
|
||||||
|
if "async" in t.fm.flags:
|
||||||
|
# Let them run; the executor handles timeouts per-batch.
|
||||||
|
pass
|
||||||
|
# raw tests — they don't load the harness; we can't use assert.* at all.
|
||||||
|
# Still run them — some raw tests just check syntax via parse.
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate(results: list[TestResult]) -> dict:
|
||||||
|
"""Build the scoreboard dict."""
|
||||||
|
by_cat: dict[str, dict] = defaultdict(
|
||||||
|
lambda: {"pass": 0, "fail": 0, "skip": 0, "timeout": 0, "total": 0, "failures": Counter()}
|
||||||
|
)
|
||||||
|
totals = {"pass": 0, "fail": 0, "skip": 0, "timeout": 0, "total": 0}
|
||||||
|
failure_modes: Counter[str] = Counter()
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
cat = by_cat[r.category]
|
||||||
|
cat[r.status] += 1
|
||||||
|
cat["total"] += 1
|
||||||
|
totals[r.status] += 1
|
||||||
|
totals["total"] += 1
|
||||||
|
if r.status == "fail":
|
||||||
|
cat["failures"][r.reason] += 1
|
||||||
|
failure_modes[r.reason] += 1
|
||||||
|
|
||||||
|
# Build the scoreboard
|
||||||
|
categories = []
|
||||||
|
for name, stats in sorted(by_cat.items()):
|
||||||
|
total = stats["total"]
|
||||||
|
passed = stats["pass"]
|
||||||
|
runnable = total - stats["skip"]
|
||||||
|
pass_rate = (passed / runnable * 100.0) if runnable else 0.0
|
||||||
|
categories.append(
|
||||||
|
{
|
||||||
|
"category": name,
|
||||||
|
"total": total,
|
||||||
|
"pass": passed,
|
||||||
|
"fail": stats["fail"],
|
||||||
|
"skip": stats["skip"],
|
||||||
|
"timeout": stats["timeout"],
|
||||||
|
"pass_rate": round(pass_rate, 1),
|
||||||
|
"top_failures": stats["failures"].most_common(5),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pass_rate = (totals["pass"] / (totals["total"] - totals["skip"]) * 100.0) if totals["total"] - totals["skip"] else 0.0
|
||||||
|
return {
|
||||||
|
"totals": {**totals, "pass_rate": round(pass_rate, 1)},
|
||||||
|
"categories": categories,
|
||||||
|
"top_failure_modes": failure_modes.most_common(20),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def write_markdown(scoreboard: dict, path: Path, pinned_commit: str) -> None:
|
||||||
|
t = scoreboard["totals"]
|
||||||
|
lines = [
|
||||||
|
"# test262 scoreboard",
|
||||||
|
"",
|
||||||
|
f"Pinned commit: `{pinned_commit}`",
|
||||||
|
"",
|
||||||
|
f"**Total:** {t['pass']}/{t['total']} passed ({t['pass_rate']}%), "
|
||||||
|
f"{t['fail']} failed, {t['skip']} skipped, {t['timeout']} timeouts.",
|
||||||
|
"",
|
||||||
|
"## Top failure modes",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for mode, count in scoreboard["top_failure_modes"]:
|
||||||
|
lines.append(f"- **{count}x** {mode}")
|
||||||
|
lines.extend(["", "## Categories (worst pass-rate first)", ""])
|
||||||
|
lines.append("| Category | Pass | Fail | Skip | Timeout | Total | Pass % |")
|
||||||
|
lines.append("|---|---:|---:|---:|---:|---:|---:|")
|
||||||
|
# Sort: worst pass rate first, breaking ties by total desc
|
||||||
|
cats = sorted(scoreboard["categories"], key=lambda c: (c["pass_rate"], -c["total"]))
|
||||||
|
for c in cats:
|
||||||
|
lines.append(
|
||||||
|
f"| {c['category']} | {c['pass']} | {c['fail']} | {c['skip']} | "
|
||||||
|
f"{c['timeout']} | {c['total']} | {c['pass_rate']}% |"
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Per-category top failures")
|
||||||
|
lines.append("")
|
||||||
|
for c in cats:
|
||||||
|
if not c["top_failures"]:
|
||||||
|
continue
|
||||||
|
lines.append(f"### {c['category']}")
|
||||||
|
lines.append("")
|
||||||
|
for reason, count in c["top_failures"]:
|
||||||
|
lines.append(f"- **{count}x** {reason}")
|
||||||
|
lines.append("")
|
||||||
|
path.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--limit", type=int, default=0, help="max tests to run (0 = all)")
|
||||||
|
ap.add_argument("--filter", type=str, default=None, help="path prefix filter")
|
||||||
|
ap.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE)
|
||||||
|
ap.add_argument(
|
||||||
|
"--output-json",
|
||||||
|
type=str,
|
||||||
|
default=str(REPO / "lib" / "js" / "test262-scoreboard.json"),
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"--output-md",
|
||||||
|
type=str,
|
||||||
|
default=str(REPO / "lib" / "js" / "test262-scoreboard.md"),
|
||||||
|
)
|
||||||
|
ap.add_argument("--progress", action="store_true", help="print per-batch progress")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
if not SX_SERVER.exists():
|
||||||
|
print(f"ERROR: sx_server.exe not found at {SX_SERVER}", file=sys.stderr)
|
||||||
|
print("Build with: cd hosts/ocaml && dune build", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if not UPSTREAM.exists():
|
||||||
|
print(f"ERROR: test262-upstream not found at {UPSTREAM}", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
"Clone with: cd lib/js && git clone --depth 1 "
|
||||||
|
"https://github.com/tc39/test262.git test262-upstream",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
pinned_commit = ""
|
||||||
|
try:
|
||||||
|
pinned_commit = subprocess.check_output(
|
||||||
|
["git", "-C", str(UPSTREAM), "rev-parse", "HEAD"], text=True
|
||||||
|
).strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
all_paths = discover_tests(args.filter)
|
||||||
|
if args.limit:
|
||||||
|
all_paths = all_paths[: args.limit]
|
||||||
|
print(f"Discovered {len(all_paths)} test files.", file=sys.stderr)
|
||||||
|
|
||||||
|
# Load all (parse frontmatter, decide skips up front)
|
||||||
|
tests: list[TestCase] = []
|
||||||
|
skipped: list[TestResult] = []
|
||||||
|
for p in all_paths:
|
||||||
|
t = load_test(p)
|
||||||
|
if not t:
|
||||||
|
continue
|
||||||
|
skip, why = should_skip(t)
|
||||||
|
if skip:
|
||||||
|
skipped.append(
|
||||||
|
TestResult(rel=t.rel, category=t.category, status="skip", reason=why)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
tests.append(t)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Will run {len(tests)} tests ({len(skipped)} skipped up front).",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
results: list[TestResult] = list(skipped)
|
||||||
|
batch_size = args.batch_size
|
||||||
|
epoch_start = 100
|
||||||
|
n_batches = (len(tests) + batch_size - 1) // batch_size
|
||||||
|
t_run_start = time.monotonic()
|
||||||
|
|
||||||
|
for bi in range(n_batches):
|
||||||
|
batch = tests[bi * batch_size : (bi + 1) * batch_size]
|
||||||
|
timeout_s = min(BATCH_TIMEOUT_S, max(30, len(batch) * PER_TEST_S))
|
||||||
|
epoch_map, timed_out, elapsed = run_batch(batch, epoch_start, timeout_s)
|
||||||
|
for idx, t in enumerate(batch):
|
||||||
|
epoch = epoch_start + idx
|
||||||
|
res = epoch_map.get(epoch)
|
||||||
|
if res is None:
|
||||||
|
# No result for this epoch — batch probably timed out before
|
||||||
|
# reaching it, or sx_server died.
|
||||||
|
status = "timeout" if timed_out else "fail"
|
||||||
|
reason = "batch timeout before epoch" if timed_out else "no result from sx_server"
|
||||||
|
results.append(
|
||||||
|
TestResult(
|
||||||
|
rel=t.rel, category=t.category, status=status, reason=reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
kind, payload = res
|
||||||
|
if t.fm.negative_phase:
|
||||||
|
ok, why = classify_negative_result(t.fm, kind, payload)
|
||||||
|
else:
|
||||||
|
ok, why = classify_positive_result(kind, payload)
|
||||||
|
results.append(
|
||||||
|
TestResult(
|
||||||
|
rel=t.rel,
|
||||||
|
category=t.category,
|
||||||
|
status="pass" if ok else "fail",
|
||||||
|
reason=why,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
epoch_start += batch_size
|
||||||
|
|
||||||
|
if args.progress or bi % 10 == 0:
|
||||||
|
done_n = min((bi + 1) * batch_size, len(tests))
|
||||||
|
pass_so_far = sum(1 for r in results if r.status == "pass")
|
||||||
|
print(
|
||||||
|
f" [batch {bi + 1}/{n_batches}] {done_n}/{len(tests)} tests "
|
||||||
|
f"{elapsed:.1f}s{' TIMEOUT' if timed_out else ''} "
|
||||||
|
f"running-pass={pass_so_far}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
t_run_elapsed = time.monotonic() - t_run_start
|
||||||
|
print(f"\nFinished run in {t_run_elapsed:.1f}s", file=sys.stderr)
|
||||||
|
|
||||||
|
scoreboard = aggregate(results)
|
||||||
|
scoreboard["pinned_commit"] = pinned_commit
|
||||||
|
scoreboard["elapsed_seconds"] = round(t_run_elapsed, 1)
|
||||||
|
|
||||||
|
# Per-test detail is too large — omit from JSON by default; the aggregated
|
||||||
|
# scoreboard is what's useful.
|
||||||
|
out_json = Path(args.output_json)
|
||||||
|
out_json.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_json.write_text(json.dumps(scoreboard, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
out_md = Path(args.output_md)
|
||||||
|
write_markdown(scoreboard, out_md, pinned_commit)
|
||||||
|
|
||||||
|
t = scoreboard["totals"]
|
||||||
|
print(
|
||||||
|
f"\nScoreboard: {t['pass']}/{t['total']} passed ({t['pass_rate']}%) "
|
||||||
|
f"fail={t['fail']} skip={t['skip']} timeout={t['timeout']}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(f"JSON: {out_json}", file=sys.stderr)
|
||||||
|
print(f"MD: {out_md}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
35
lib/js/test262-scoreboard.json
Normal file
35
lib/js/test262-scoreboard.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"totals": {
|
||||||
|
"pass": 0,
|
||||||
|
"fail": 1,
|
||||||
|
"skip": 0,
|
||||||
|
"timeout": 7,
|
||||||
|
"total": 8,
|
||||||
|
"pass_rate": 0.0
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"category": "built-ins/Math",
|
||||||
|
"total": 8,
|
||||||
|
"pass": 0,
|
||||||
|
"fail": 1,
|
||||||
|
"skip": 0,
|
||||||
|
"timeout": 7,
|
||||||
|
"pass_rate": 0.0,
|
||||||
|
"top_failures": [
|
||||||
|
[
|
||||||
|
"SyntaxError (parse/unsupported syntax)",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"top_failure_modes": [
|
||||||
|
[
|
||||||
|
"SyntaxError (parse/unsupported syntax)",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33",
|
||||||
|
"elapsed_seconds": 40.1
|
||||||
|
}
|
||||||
21
lib/js/test262-scoreboard.md
Normal file
21
lib/js/test262-scoreboard.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# test262 scoreboard
|
||||||
|
|
||||||
|
Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`
|
||||||
|
|
||||||
|
**Total:** 0/8 passed (0.0%), 1 failed, 0 skipped, 7 timeouts.
|
||||||
|
|
||||||
|
## Top failure modes
|
||||||
|
|
||||||
|
- **1x** SyntaxError (parse/unsupported syntax)
|
||||||
|
|
||||||
|
## Categories (worst pass-rate first)
|
||||||
|
|
||||||
|
| Category | Pass | Fail | Skip | Timeout | Total | Pass % |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| built-ins/Math | 0 | 1 | 0 | 7 | 8 | 0.0% |
|
||||||
|
|
||||||
|
## Per-category top failures
|
||||||
|
|
||||||
|
### built-ins/Math
|
||||||
|
|
||||||
|
- **1x** SyntaxError (parse/unsupported syntax)
|
||||||
31
lib/js/test262-slice/README.md
Normal file
31
lib/js/test262-slice/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# JS-on-SX cherry-picked conformance slice
|
||||||
|
|
||||||
|
A hand-picked slice inspired by test262 expression tests. Each test is one
|
||||||
|
JS expression in a `.js` file, paired with an `.expected` file containing
|
||||||
|
the SX-printed result that `js-eval` should produce.
|
||||||
|
|
||||||
|
Run via:
|
||||||
|
|
||||||
|
bash lib/js/conformance.sh
|
||||||
|
|
||||||
|
The slice intentionally avoids anything not yet implemented (statements,
|
||||||
|
`var`/`let`, `function`, regex, template strings, prototypes, `new`,
|
||||||
|
`this`, classes, async). Those land in later phases.
|
||||||
|
|
||||||
|
## Expected value format
|
||||||
|
|
||||||
|
`js-eval` returns SX values. The epoch protocol prints them thus:
|
||||||
|
|
||||||
|
| JS value | Expected file contents |
|
||||||
|
|------------------|-----------------------|
|
||||||
|
| `42` | `42` |
|
||||||
|
| `3.14` | `3.14` |
|
||||||
|
| `true` / `false` | `true` / `false` |
|
||||||
|
| `"hi"` | `"hi"` |
|
||||||
|
| `null` | `nil` |
|
||||||
|
| `undefined` | `"js-undefined"` |
|
||||||
|
| `[1,2,3]` | `(1 2 3)` |
|
||||||
|
| `{}` | `{}` |
|
||||||
|
|
||||||
|
The runner does a substring match — the `.expected` file can contain just
|
||||||
|
the distinguishing part of the result.
|
||||||
1
lib/js/test262-slice/arithmetic/add.expected
Normal file
1
lib/js/test262-slice/arithmetic/add.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
1
lib/js/test262-slice/arithmetic/add.js
Normal file
1
lib/js/test262-slice/arithmetic/add.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 + 2
|
||||||
1
lib/js/test262-slice/arithmetic/big_expr.expected
Normal file
1
lib/js/test262-slice/arithmetic/big_expr.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
5
|
||||||
1
lib/js/test262-slice/arithmetic/big_expr.js
Normal file
1
lib/js/test262-slice/arithmetic/big_expr.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 + 2 * 3 - 4 / 2
|
||||||
1
lib/js/test262-slice/arithmetic/bitnot.expected
Normal file
1
lib/js/test262-slice/arithmetic/bitnot.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-6
|
||||||
1
lib/js/test262-slice/arithmetic/bitnot.js
Normal file
1
lib/js/test262-slice/arithmetic/bitnot.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
~5
|
||||||
1
lib/js/test262-slice/arithmetic/chained.expected
Normal file
1
lib/js/test262-slice/arithmetic/chained.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
10
|
||||||
1
lib/js/test262-slice/arithmetic/chained.js
Normal file
1
lib/js/test262-slice/arithmetic/chained.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 + 2 + 3 + 4
|
||||||
1
lib/js/test262-slice/arithmetic/div.expected
Normal file
1
lib/js/test262-slice/arithmetic/div.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
1
lib/js/test262-slice/arithmetic/div.js
Normal file
1
lib/js/test262-slice/arithmetic/div.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
12 / 4
|
||||||
1
lib/js/test262-slice/arithmetic/mixed_concat.expected
Normal file
1
lib/js/test262-slice/arithmetic/mixed_concat.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"12"
|
||||||
1
lib/js/test262-slice/arithmetic/mixed_concat.js
Normal file
1
lib/js/test262-slice/arithmetic/mixed_concat.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 + "2"
|
||||||
1
lib/js/test262-slice/arithmetic/mod.expected
Normal file
1
lib/js/test262-slice/arithmetic/mod.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
lib/js/test262-slice/arithmetic/mod.js
Normal file
1
lib/js/test262-slice/arithmetic/mod.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
10 % 3
|
||||||
1
lib/js/test262-slice/arithmetic/neg.expected
Normal file
1
lib/js/test262-slice/arithmetic/neg.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-5
|
||||||
1
lib/js/test262-slice/arithmetic/neg.js
Normal file
1
lib/js/test262-slice/arithmetic/neg.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-5
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
9
|
||||||
1
lib/js/test262-slice/arithmetic/paren_precedence.js
Normal file
1
lib/js/test262-slice/arithmetic/paren_precedence.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(1 + 2) * 3
|
||||||
1
lib/js/test262-slice/arithmetic/pos.expected
Normal file
1
lib/js/test262-slice/arithmetic/pos.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
5
|
||||||
1
lib/js/test262-slice/arithmetic/pos.js
Normal file
1
lib/js/test262-slice/arithmetic/pos.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
+5
|
||||||
1
lib/js/test262-slice/arithmetic/pow.expected
Normal file
1
lib/js/test262-slice/arithmetic/pow.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1024
|
||||||
1
lib/js/test262-slice/arithmetic/pow.js
Normal file
1
lib/js/test262-slice/arithmetic/pow.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2 ** 10
|
||||||
1
lib/js/test262-slice/arithmetic/pow_right_assoc.expected
Normal file
1
lib/js/test262-slice/arithmetic/pow_right_assoc.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
512
|
||||||
1
lib/js/test262-slice/arithmetic/pow_right_assoc.js
Normal file
1
lib/js/test262-slice/arithmetic/pow_right_assoc.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2 ** 3 ** 2
|
||||||
1
lib/js/test262-slice/arithmetic/precedence.expected
Normal file
1
lib/js/test262-slice/arithmetic/precedence.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7
|
||||||
1
lib/js/test262-slice/arithmetic/precedence.js
Normal file
1
lib/js/test262-slice/arithmetic/precedence.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1 + 2 * 3
|
||||||
1
lib/js/test262-slice/arithmetic/string_concat.expected
Normal file
1
lib/js/test262-slice/arithmetic/string_concat.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"ab"
|
||||||
1
lib/js/test262-slice/arithmetic/string_concat.js
Normal file
1
lib/js/test262-slice/arithmetic/string_concat.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"a" + "b"
|
||||||
1
lib/js/test262-slice/arithmetic/sub.expected
Normal file
1
lib/js/test262-slice/arithmetic/sub.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
6
|
||||||
1
lib/js/test262-slice/arithmetic/sub.js
Normal file
1
lib/js/test262-slice/arithmetic/sub.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
10 - 4
|
||||||
1
lib/js/test262-slice/async/async_arrow.expected
Normal file
1
lib/js/test262-slice/async/async_arrow.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
5
lib/js/test262-slice/async/async_arrow.js
Normal file
5
lib/js/test262-slice/async/async_arrow.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
var double = async x => x * 2;
|
||||||
|
var r = null;
|
||||||
|
double(21).then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
5
lib/js/test262-slice/async/async_arrow_multiparam.js
Normal file
5
lib/js/test262-slice/async/async_arrow_multiparam.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
var add = async (a, b) => a + b;
|
||||||
|
var r = null;
|
||||||
|
add(10, 32).then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/async_fn_basic.expected
Normal file
1
lib/js/test262-slice/async/async_fn_basic.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
5
lib/js/test262-slice/async/async_fn_basic.js
Normal file
5
lib/js/test262-slice/async/async_fn_basic.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
async function f() { return 42; }
|
||||||
|
var r = null;
|
||||||
|
f().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/async_fn_throws.expected
Normal file
1
lib/js/test262-slice/async/async_fn_throws.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nope
|
||||||
5
lib/js/test262-slice/async/async_fn_throws.js
Normal file
5
lib/js/test262-slice/async/async_fn_throws.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
async function f() { throw "nope"; }
|
||||||
|
var r = null;
|
||||||
|
f().catch(e => { r = e; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/async_nested_calls.expected
Normal file
1
lib/js/test262-slice/async/async_nested_calls.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
33
|
||||||
7
lib/js/test262-slice/async/async_nested_calls.js
Normal file
7
lib/js/test262-slice/async/async_nested_calls.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
async function inner(x) { return x * 2; }
|
||||||
|
async function middle(x) { var r = await inner(x); return r + 1; }
|
||||||
|
async function outer(x) { var r = await middle(x); return r * 3; }
|
||||||
|
var result = null;
|
||||||
|
outer(5).then(v => { result = v; });
|
||||||
|
__drain();
|
||||||
|
result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
100
|
||||||
5
lib/js/test262-slice/async/async_returns_promise.js
Normal file
5
lib/js/test262-slice/async/async_returns_promise.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
async function wrap(x) { return Promise.resolve(x); }
|
||||||
|
var r = null;
|
||||||
|
wrap(100).then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/await_basic.expected
Normal file
1
lib/js/test262-slice/async/await_basic.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
70
|
||||||
9
lib/js/test262-slice/async/await_basic.js
Normal file
9
lib/js/test262-slice/async/await_basic.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
async function add(a, b) { return a + b; }
|
||||||
|
async function main() {
|
||||||
|
var r = await add(3, 4);
|
||||||
|
return r * 10;
|
||||||
|
}
|
||||||
|
var result = null;
|
||||||
|
main().then(v => { result = v; });
|
||||||
|
__drain();
|
||||||
|
result
|
||||||
1
lib/js/test262-slice/async/await_chain.expected
Normal file
1
lib/js/test262-slice/async/await_chain.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
11
lib/js/test262-slice/async/await_chain.js
Normal file
11
lib/js/test262-slice/async/await_chain.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
async function step() { return 1; }
|
||||||
|
async function main() {
|
||||||
|
var a = await step();
|
||||||
|
var b = await step();
|
||||||
|
var c = await step();
|
||||||
|
return a + b + c;
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/await_in_loop.expected
Normal file
1
lib/js/test262-slice/async/await_in_loop.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
5
|
||||||
12
lib/js/test262-slice/async/await_in_loop.js
Normal file
12
lib/js/test262-slice/async/await_in_loop.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
async function one() { return 1; }
|
||||||
|
async function main() {
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < 5; i = i + 1) {
|
||||||
|
sum = sum + await one();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/await_nonpromise.expected
Normal file
1
lib/js/test262-slice/async/await_nonpromise.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
43
|
||||||
8
lib/js/test262-slice/async/await_nonpromise.js
Normal file
8
lib/js/test262-slice/async/await_nonpromise.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
async function main() {
|
||||||
|
var x = await 42;
|
||||||
|
return x + 1;
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/await_promise_all.expected
Normal file
1
lib/js/test262-slice/async/await_promise_all.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
6
|
||||||
8
lib/js/test262-slice/async/await_promise_all.js
Normal file
8
lib/js/test262-slice/async/await_promise_all.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
async function main() {
|
||||||
|
var vs = await Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]);
|
||||||
|
return vs[0] + vs[1] + vs[2];
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/async/await_rejected.expected
Normal file
1
lib/js/test262-slice/async/await_rejected.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
caught:no
|
||||||
9
lib/js/test262-slice/async/await_rejected.js
Normal file
9
lib/js/test262-slice/async/await_rejected.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
async function bad() { throw "no"; }
|
||||||
|
async function main() {
|
||||||
|
try { await bad(); return "unreachable"; }
|
||||||
|
catch (e) { return "caught:" + e; }
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
bad
|
||||||
9
lib/js/test262-slice/async/await_throws_error_object.js
Normal file
9
lib/js/test262-slice/async/await_throws_error_object.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
async function failing() { throw new Error("bad"); }
|
||||||
|
async function main() {
|
||||||
|
try { await failing(); return "unreachable"; }
|
||||||
|
catch (e) { return e.message; }
|
||||||
|
}
|
||||||
|
var r = null;
|
||||||
|
main().then(v => { r = v; });
|
||||||
|
__drain();
|
||||||
|
r
|
||||||
1
lib/js/test262-slice/closures/adder.expected
Normal file
1
lib/js/test262-slice/closures/adder.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
44
|
||||||
1
lib/js/test262-slice/closures/adder.js
Normal file
1
lib/js/test262-slice/closures/adder.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
function mk(n) { return x => x + n; } let add7 = mk(7); add7(10) + add7(20)
|
||||||
1
lib/js/test262-slice/closures/counter.expected
Normal file
1
lib/js/test262-slice/closures/counter.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
4
|
||||||
1
lib/js/test262-slice/closures/counter.js
Normal file
1
lib/js/test262-slice/closures/counter.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
function mkc() { let n = 0; return () => { n = n + 1; return n; }; } let c = mkc(); c(); c(); c(); c()
|
||||||
1
lib/js/test262-slice/closures/multi_closure.expected
Normal file
1
lib/js/test262-slice/closures/multi_closure.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
1
lib/js/test262-slice/closures/multi_closure.js
Normal file
1
lib/js/test262-slice/closures/multi_closure.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
function mkPair() { let x = 0; let y = 0; let setX = v => { x = v; }; let setY = v => { y = v; }; let get = () => x + y; setX(17); setY(25); return get(); } mkPair()
|
||||||
1
lib/js/test262-slice/closures/nested_scope.expected
Normal file
1
lib/js/test262-slice/closures/nested_scope.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
1
lib/js/test262-slice/closures/nested_scope.js
Normal file
1
lib/js/test262-slice/closures/nested_scope.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
function outer() { let a = 1; function inner() { let b = 2; function deepest() { return a + b; } return deepest(); } return inner(); } outer()
|
||||||
1
lib/js/test262-slice/closures/sum_sq.expected
Normal file
1
lib/js/test262-slice/closures/sum_sq.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
55
|
||||||
1
lib/js/test262-slice/closures/sum_sq.js
Normal file
1
lib/js/test262-slice/closures/sum_sq.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
function sumSq(xs) { let t = 0; for (let i = 0; i < xs.length; i = i + 1) t = t + xs[i] * xs[i]; return t; } sumSq([1,2,3,4,5])
|
||||||
1
lib/js/test262-slice/coercion/implicit_str_add.expected
Normal file
1
lib/js/test262-slice/coercion/implicit_str_add.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"n=42"
|
||||||
1
lib/js/test262-slice/coercion/implicit_str_add.js
Normal file
1
lib/js/test262-slice/coercion/implicit_str_add.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"n=" + 42
|
||||||
1
lib/js/test262-slice/coercion/loose_str_num.expected
Normal file
1
lib/js/test262-slice/coercion/loose_str_num.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
true
|
||||||
1
lib/js/test262-slice/coercion/loose_str_num.js
Normal file
1
lib/js/test262-slice/coercion/loose_str_num.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"5" == 5
|
||||||
1
lib/js/test262-slice/coercion/typeof_bool.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_bool.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"boolean"
|
||||||
1
lib/js/test262-slice/coercion/typeof_bool.js
Normal file
1
lib/js/test262-slice/coercion/typeof_bool.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof true
|
||||||
1
lib/js/test262-slice/coercion/typeof_fn.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_fn.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"function"
|
||||||
1
lib/js/test262-slice/coercion/typeof_fn.js
Normal file
1
lib/js/test262-slice/coercion/typeof_fn.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof (x => x)
|
||||||
1
lib/js/test262-slice/coercion/typeof_null.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_null.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"object"
|
||||||
1
lib/js/test262-slice/coercion/typeof_null.js
Normal file
1
lib/js/test262-slice/coercion/typeof_null.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof null
|
||||||
1
lib/js/test262-slice/coercion/typeof_num.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_num.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"number"
|
||||||
1
lib/js/test262-slice/coercion/typeof_num.js
Normal file
1
lib/js/test262-slice/coercion/typeof_num.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof 42
|
||||||
1
lib/js/test262-slice/coercion/typeof_str.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_str.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"string"
|
||||||
1
lib/js/test262-slice/coercion/typeof_str.js
Normal file
1
lib/js/test262-slice/coercion/typeof_str.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof "x"
|
||||||
1
lib/js/test262-slice/coercion/typeof_undef.expected
Normal file
1
lib/js/test262-slice/coercion/typeof_undef.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"undefined"
|
||||||
1
lib/js/test262-slice/coercion/typeof_undef.js
Normal file
1
lib/js/test262-slice/coercion/typeof_undef.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
typeof undefined
|
||||||
1
lib/js/test262-slice/collections/array_empty.expected
Normal file
1
lib/js/test262-slice/collections/array_empty.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
lib/js/test262-slice/collections/array_empty.js
Normal file
1
lib/js/test262-slice/collections/array_empty.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[].length
|
||||||
1
lib/js/test262-slice/collections/array_index.expected
Normal file
1
lib/js/test262-slice/collections/array_index.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
20
|
||||||
1
lib/js/test262-slice/collections/array_index.js
Normal file
1
lib/js/test262-slice/collections/array_index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[10,20,30][1]
|
||||||
1
lib/js/test262-slice/collections/array_length.expected
Normal file
1
lib/js/test262-slice/collections/array_length.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
1
lib/js/test262-slice/collections/array_length.js
Normal file
1
lib/js/test262-slice/collections/array_length.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[1,2,3].length
|
||||||
1
lib/js/test262-slice/collections/array_nested.expected
Normal file
1
lib/js/test262-slice/collections/array_nested.expected
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3
|
||||||
1
lib/js/test262-slice/collections/array_nested.js
Normal file
1
lib/js/test262-slice/collections/array_nested.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[[1,2],[3,4]][1][0]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user