flan/spike/js/survey.sh
Joseph Ferano 8e5fcf54cc The two sweeps do not race a printing fixture against a clock
dev-chatty.flan outlives the surveys' twenty seconds by design, and unlike
dev-repl it prints while it does -- so the two backends stop at different
lines and the diff reports on scheduling rather than on lowering. It joins
dev-loop and dev-watch in the excluded-by-name list in both sweeps, with the
distinction written down.

rt_flush_out is guarded on __wasm__: the pipe it is careful about belongs to a
merged flan dev, which is only ever a native host, and wasm32 need not answer
for a descriptor mode its runtime may model differently.

And three comments that went false with the _exit: the atexit registration in
the merged entry point is no longer there for rt_die, which unlinks the socket
for itself now, so both places that said so say what it is actually left
covering.
2026-09-18 08:00:57 +07:00

171 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Does the JS dialect agree with LLVM, or refuse in its own words?
#
# Shaped after spike/x86/survey.sh and for the same reason: the only honest
# test of a backend is what the program prints and what it exits with. Every
# program in test/programs is built both ways -- native through LLVM, and one
# .js run by node -- and stdout, stderr and the exit status are all diffed.
#
# The difference from the x86 sweep is what a refusal means. That backend is
# *behind*: every node it refuses is one it will lower eventually, so a
# refusal there is a regression and the strict mode fails on one. This is a
# *dialect*: docs/DISCUSS.md item 5 decided that object mapping leaves the
# memory model behind, so a program using pointers, allocators, a Map, a Pool,
# the FFI or conditions is refused permanently and correctly. A refusal here
# is therefore an expected outcome and not a failure -- what fails is a
# DIFFER, which is a wrong answer, and a CRASH, which is a program the backend
# emitted and node would not run.
#
# Five outcomes:
#
# MATCH built both ways, same stdout, same stderr, same exit status
# DIFFER built both ways, and disagreed
# REFUSED Js.Unsupported -- named by the backend, with a location (exit 3)
# CRASH emitted JS that node refused to run, or that threw
# SKIP no main, does not compile at all, or does not terminate
#
# Over test/programs, and over spike/js's own probes, which are here for the
# paths the corpus does not walk -- p1-int-semantics.flan in particular is the
# evidence for the wrapping, division and shift decisions in lib/js.ml's
# header, and it is in the corpus rather than run by hand so that the evidence
# is the sweep.
#
# Usage: spike/js/survey.sh [name-substring ...]
set -u
orig=$(pwd)
here=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$here/../.." && pwd)
cd "$root" || exit 1
# FLAN is how the dune @js alias hands this script a compiler dune has already
# built; building it here would be a second dune inside the first one's lock.
# See spike/x86/survey.sh, which says the same thing at more length.
if [ -n "${FLAN:-}" ]; then
case $FLAN in /*) flan=$FLAN;; *) flan=$orig/$FLAN;; esac
else
dune build --root . bin/main.exe 2>&1 | head -30
flan=$root/_build/default/bin/main.exe
fi
test -x "$flan" || { echo "build failed"; exit 1; }
# node is the host. Guarded rather than assumed: this sweep is opt-in and a
# machine without node should say so and stop, not report zero of everything.
node=${NODE:-node}
if ! command -v "$node" > /dev/null 2>&1; then
echo "js survey: no $node on PATH, nothing run"
exit 0
fi
corpus=${SURVEY_CORPUS:-$root}
out=$(mktemp -d); trap 'rm -rf "$out"' EXIT
# The ones that run until something stops them, excluded by name for the reason
# the x86 sweep excludes them: a timeout cannot tell them from a hang -- and in
# dev-chatty's case cannot even give the two sides the same truncation, since
# it prints 4K a frame for as long as it is allowed to.
forever="dev-loop dev-watch dev-chatty"
TIMEOUT=${TIMEOUT:-20}
# Extra flags, given to *both* sides, the way the x86 sweep hands them over.
# SURVEY_FLAGS=--no-bounds-checks is the one with a use here: this backend
# emits a *second* indexing path when the checks are off -- a bare a[i] and a
# bare s.a[s.o + i], with no $at around either -- and a path the sweep never
# walks is a path nobody has run. Swept: 23 MATCH, 0 DIFFER, with arith.flan
# the one CRASH, and it is not one -- that program indexes out of range on
# purpose, so with the checks off the native build segfaults and node throws,
# and neither answer is defined. Nothing else changes.
read -r -a extra <<<"${SURVEY_FLAGS:-}"
declare -a match=() differ=() refused=() crash=() skip=()
for src in "$corpus"/test/programs/*.flan "$corpus"/spike/js/*.flan; do
name=$(basename "$src" .flan)
if [ $# -gt 0 ]; then
want=0
for pat in "$@"; do case "$name" in *"$pat"*) want=1;; esac; done
[ $want = 1 ] || continue
fi
case " $forever " in *" $name "*) skip+=("$name:runs-forever"); continue;; esac
# LLVM first. A program that does not compile at all, or has no main, is not
# this backend's business -- the frontend refused it either way.
if ! "$flan" build "$src" "${extra[@]}" -o "$out/$name.llvm" \
>"$out/$name.llvm.err" 2>&1; then
if grep -q "in function \`_start\|undefined reference to \`main\|crt1.o" "$out/$name.llvm.err"; then
skip+=("$name:no-main")
else
skip+=("$name:does-not-compile")
fi
continue
fi
"$flan" build "$src" --target=js "${extra[@]}" -o "$out/$name.js" \
>"$out/$name.js.err" 2>&1
rc=$?
if [ $rc = 3 ]; then
why=$(head -1 "$out/$name.js.err" | sed 's/^js: //' | sed 's/^[^ ]*flan:[0-9]*:[0-9]*: //')
refused+=("$name:$why")
continue
fi
if [ $rc != 0 ]; then
crash+=("$name:compiler:$(head -1 "$out/$name.js.err")")
continue
fi
( cd "$out" && timeout "$TIMEOUT" "$out/$name.llvm" \
>"$out/$name.llvm.out" 2>"$out/$name.llvm.diag" )
a=$?
( cd "$out" && timeout "$TIMEOUT" "$node" "$out/$name.js" \
>"$out/$name.js.out" 2>"$out/$name.js.diag" )
b=$?
# node's own failure -- a SyntaxError, a ReferenceError, a thrown Error --
# is not a disagreement about a value. It is a backend that emitted
# something it should have refused, and it gets its own bucket so that the
# two are never confused in the counts.
if grep -q "^[A-Za-z]*Error:\|node:internal" "$out/$name.js.diag" 2>/dev/null; then
crash+=("$name:node:$(grep -m1 "^[A-Za-z]*Error:" "$out/$name.js.diag" | head -c 120)")
continue
fi
if [ "$a" = "$b" ] && cmp -s "$out/$name.llvm.out" "$out/$name.js.out" \
&& cmp -s "$out/$name.llvm.diag" "$out/$name.js.diag"; then
match+=("$name")
else
differ+=("$name:llvm=$a/js=$b")
if [ "${SURVEY_SHOW:-}" = 1 ]; then
echo "--- $name: llvm exit $a, js exit $b"
diff "$out/$name.llvm.out" "$out/$name.js.out" | head -20
diff "$out/$name.llvm.diag" "$out/$name.js.diag" | head -20
fi
fi
done
echo
echo "MATCH ${#match[@]}"
echo "DIFFER ${#differ[@]}"
[ "${#differ[@]}" = 0 ] || printf ' %s\n' "${differ[@]}"
echo "REFUSED ${#refused[@]}"
if [ "${#refused[@]}" != 0 ] && [ "${SURVEY_QUIET:-}" != 1 ]; then
printf '%s\n' "${refused[@]}" | sed 's/^[^:]*://' | cut -c1-60 | sort | uniq -c \
| sort -rn | sed 's/^/ /'
fi
echo "CRASH ${#crash[@]}"
[ "${#crash[@]}" = 0 ] || printf ' %s\n' "${crash[@]}"
echo "SKIP ${#skip[@]}"
if [ "${#skip[@]}" != 0 ] && [ "${SURVEY_QUIET:-}" != 1 ]; then
printf '%s\n' "${skip[@]}" | sed 's/^[^:]*://' | sort | uniq -c \
| sed 's/^/ /'
fi
# Strict mode, for the @js alias. A refusal is *not* a failure here -- see the
# header -- so only a wrong answer and a program node could not run are.
if [ "${SURVEY_STRICT:-}" = 1 ]; then
if [ "${#differ[@]}" != 0 ] || [ "${#crash[@]}" != 0 ]; then
echo
echo "js survey FAILED: ${#differ[@]} differ, ${#crash[@]} crash"
exit 1
fi
echo
echo "js survey ok: ${#match[@]} match, ${#refused[@]} refused by name"
fi