#!/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" # the marker turns zc0's line tables on for the program, named as under zy, so faults # and rt.trace() print the same backtrace on both sides (src/tools/automap.sh does the same) { cat $SRCS; echo 'namespace;'; echo '//:lines '; } > "$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" # links left by an earlier run go first: a link to a file a program wrote into the tree once # (bareio's note) would otherwise lead the next write back into the repository find "$W/root" -mindepth 1 -maxdepth 1 -type l -delete 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 ]