aboutsummaryrefslogtreecommitdiff
path: root/src/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools')
-rwxr-xr-xsrc/tools/parity.sh160
1 files changed, 160 insertions, 0 deletions
diff --git a/src/tools/parity.sh b/src/tools/parity.sh
new file mode 100755
index 0000000..2739a85
--- /dev/null
+++ b/src/tools/parity.sh
@@ -0,0 +1,160 @@
+#!/bin/bash
+# compiles the same programs with both compilers and compares what they print.
+# zc0 (stage 0, in c) compiles runtime + program into an image that runs on a
+# microcore with the image embedded; the self-hosted compiler compiles the
+# same source inside the vm (build/bin/zy < prog.zc). a program zc0 rejects is
+# outside its subset and is skipped. for every other program the two outputs
+# must be equal. the bootstrap's fixpoint cannot see a language rule both
+# compilers got wrong the same way, but this can see one they disagree on.
+#
+# the image is built the way src/tools/zcc builds a run-only program, on the
+# default map so that it behaves like the resident compiler's programs do:
+# rt.zc, core.zc, src/runtime/run.zc, a `namespace;` (the program starts in the
+# global namespace, as under zy) and the program go through build/bin/zc0,
+# build/bin/mkgenome checks the image, and a microcore embeds it (c23 #embed).
+# the core is compiled per program (about a second); it is kept under
+# build/parity/ and reused until the program, the compilers or the runtime change.
+#
+# stdin: zy reads the program from stdin and the program then reads what follows
+# a backtick that starts a line (see examples/learn/24-backtick.zc); the image
+# gets that same text, or nothing when the source has no such line.
+#
+# tests/parity.known lists the programs that differ on purpose (zc0 has no
+# ownership analysis, for one). they are still compared and shown as known, and
+# do not fail the run; one that agrees again is reported as stale.
+set -e
+usage() {
+ printf '%s\n' 'usage: src/tools/parity.sh [-v] [-t SECONDS] [FILE.zc...]' \
+ 'compiles each program with zc0 and with the self-hosted compiler and compares stdout.' \
+ 'without FILE the corpus is tests/programs/*.zc, examples/*.zc and examples/learn/*.zc.' \
+ 'a program zc0 rejects is skipped and counted. both runs see the same stdin:' \
+ 'the text after a backtick line in the source, else nothing.' \
+ 'prints one line per mismatch and a summary; exit status 1 when anything differs' \
+ '(programs listed in tests/parity.known are shown but do not count).' \
+ 'options:' \
+ ' -v also list the skipped programs and why' \
+ ' -t SECONDS time limit for each run (default 30); a program that hits it on both sides is skipped' \
+ ' --help, -h show this help and exit' \
+ 'work files are under build/parity/. environment: CC selects the c compiler (one with #embed).'
+}
+verbose=0; limit=30
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --help|-h) usage; exit 0 ;;
+ -v) verbose=1; shift ;;
+ -t) [ $# -ge 2 ] || { echo "parity: -t needs a number of seconds" >&2; exit 2; }
+ case "$2" in ''|*[!0-9]*|0) echo "parity: -t needs a positive number of seconds" >&2; exit 2 ;; esac
+ limit=$2; shift 2 ;;
+ --) shift; break ;;
+ -*) echo "parity: unknown option $1; use --help for usage" >&2; exit 2 ;;
+ *) break ;;
+ esac
+done
+cd "$(dirname "$0")/../.."
+for f in "$@"; do [ -f "$f" ] || { echo "parity: no such file: $f" >&2; exit 2; }; done
+if [ $# -gt 0 ]; then progs=("$@"); else progs=(tests/programs/*.zc examples/*.zc examples/learn/*.zc); fi
+
+make -s build/bin/zc0 build/bin/mkgenome build/bin/zy >&2
+CC=${CC:-$(sh src/tools/findcc.sh)}
+root=$PWD
+ZC0=build/bin/zc0; MKG=build/bin/mkgenome; ZY=build/bin/zy
+W=build/parity
+KNOWN=tests/parity.known
+mkdir -p "$W"
+# what every image starts with, and how many lines that is (zc0 counts from the top)
+SRCS="src/runtime/rt.zc src/core/core.zc src/runtime/run.zc"
+{ cat $SRCS; echo 'namespace;'; } > "$W/prefix.new"
+if cmp -s "$W/prefix.new" "$W/prefix.zc"; then rm "$W/prefix.new"; else mv "$W/prefix.new" "$W/prefix.zc"; fi
+nprefix=$(wc -l < "$W/prefix.zc")
+# what the compiled image depends on besides the program
+DEPS="$ZC0 $MKG $W/prefix.zc src/micro/micro.c src/micro/port_linux.c src/micro/micro_port.h src/core/fops.h src/core/bops.h src/core/stack.h src/core/genome.h build/core/map/memmap.h"
+
+# one line of a program's output: newlines as /, control bytes as ?, cut to 60 columns
+short() {
+ local s
+ s=$(head -c 400 "$1" | tr '\n' '/' | tr -c '[:print:]' '?')
+ if [ ${#s} -gt 60 ]; then s="${s:0:60}..."; fi
+ printf '%s' "$s"
+}
+stale() { # $1 = built file: missing, or older than a dependency or the program $2?
+ local d
+ [ -x "$1" ] || return 0
+ for d in $DEPS "$2"; do [ "$d" -nt "$1" ] && return 0; done
+ return 1
+}
+isknown() { [ -f "$KNOWN" ] && grep -q "^$1[ #]" "$KNOWN"; }
+reason() { grep "^$1[ #]" "$KNOWN" | head -n1 | sed 's/^[^#]*# *//'; }
+# zc0 reports lines of the whole input; the program's own line is what helps
+rebase() {
+ local msg=$1 re='^zc0: line ([0-9]+): (.*)$'
+ if [[ $msg =~ $re ]]; then msg="zc0: line $(( BASH_REMATCH[1] - nprefix )): ${BASH_REMATCH[2]}"; fi
+ printf '%s' "$msg"
+}
+
+mkdir -p "$W/root"
+for f in *; do [ "$f" = build ] || ln -sfn "$root/$f" "$W/root/$f"; done
+compared=0; differ=0; known=0; skipped=0; timedout=0; broken=0
+skips=""
+for p in "${progs[@]}"; do
+ name=${p%.zc}; name=${name//\//_}
+ d=$W/$name
+ mkdir -p "$d"
+ if ! cat "$W/prefix.zc" "$p" | "$ZC0" > "$d/img.bin" 2> "$d/zc0.err"; then
+ skipped=$((skipped+1))
+ skips="$skips$p: $(rebase "$(head -n1 "$d/zc0.err")")"$'\n'
+ continue
+ fi
+ if stale "$d/run" "$p"; then
+ if ! "$MKG" < "$d/img.bin" > "$d/genome.bin" 2> "$d/build.err"; then
+ echo "broken: $p: zc0 accepts it but mkgenome rejects the image: $(head -n1 "$d/build.err")"
+ broken=$((broken+1)); continue
+ fi
+ if ! $CC -O2 -w -fwrapv -pthread -DZY_THREADS src/micro/micro.c src/micro/port_linux.c \
+ -Isrc/core -Ibuild/core/map -Isrc/micro --embed-dir="$d" -o "$d/run" 2> "$d/build.err"; then
+ echo "broken: $p: the c compiler failed on the zc0 image: $(head -n1 "$d/build.err")"
+ broken=$((broken+1)); continue
+ fi
+ fi
+ # the image already holds the program. both sides then read the same stdin
+ sed -n '/^`/,$p' "$p" | tail -c +2 > "$d/stdin"
+ # from a directory that shows the repository through links, so that a program that
+ # reads a file by its path finds it, and one that writes a file leaves it under build/
+ find "$W/root" -mindepth 1 -maxdepth 1 ! -type l -exec rm -rf {} +
+ case $p in /*) pabs=$p ;; *) pabs=$root/$p ;; esac
+ rc0=0; rcy=0
+ ( cd "$W/root" && timeout -k 1 "$limit" "$root/$d/run" < "$root/$d/stdin" > "$root/$d/out0" 2> "$root/$d/err0" ) || rc0=$?
+ ( cd "$W/root" && timeout -k 1 "$limit" "$root/$ZY" < "$pabs" > "$root/$d/outy" 2> "$root/$d/erry" ) || rcy=$?
+ if [ $rc0 = 124 ] && [ $rcy = 124 ]; then
+ timedout=$((timedout+1))
+ skips="$skips$p: no result within ${limit}s on either side"$'\n'
+ continue
+ fi
+ compared=$((compared+1))
+ msg=""
+ if [ $rc0 = 124 ] || [ $rcy = 124 ]; then
+ [ $rc0 = 124 ] && a="(timed out after ${limit}s)" || a="[$(short "$d/out0")]"
+ [ $rcy = 124 ] && b="(timed out after ${limit}s)" || b="[$(short "$d/outy")]"
+ msg="$p: zc0 $a, self-hosted $b"
+ elif ! cmp -s "$d/out0" "$d/outy"; then
+ msg="$p: zc0 [$(short "$d/out0")], self-hosted [$(short "$d/outy")]"
+ fi
+ if [ -z "$msg" ]; then
+ isknown "$p" && echo "stale: $p agrees now; remove it from $KNOWN"
+ elif isknown "$p"; then
+ known=$((known+1))
+ echo "known: $msg ($(reason "$p"))"
+ else
+ differ=$((differ+1))
+ echo "differ: $msg"
+ fi
+done
+if [ $verbose = 1 ] && [ -n "$skips" ]; then
+ echo "skipped:"
+ printf '%s' "$skips" | sed 's/^/ /'
+fi
+extra=""
+[ $timedout -gt 0 ] && extra="$extra, $timedout skipped (timeout)"
+[ $known -gt 0 ] && extra="$extra, $known known ($KNOWN)"
+[ $broken -gt 0 ] && extra="$extra, $broken did not build"
+echo "parity: $compared compared, $differ differ, $skipped skipped (outside zc0)$extra"
+[ $differ -eq 0 ] && [ $broken -eq 0 ]