#!/usr/bin/env bash set -euo pipefail if [[ "${NYASH_CLI_VERBOSE:-0}" == "1" ]]; then set -x fi usage() { cat << USAGE Nyash parity runner — compare two execution paths on the same .hako Usage: tools/parity.sh [options] Options: --lhs Left mode: pyvm|llvmlite|vm (default: pyvm) --rhs Right mode: pyvm|llvmlite|vm (default: llvmlite) --timeout Timeout seconds for each run (default: 12) --show-diff Show unified diff when different Compares stdout (normalized) and exit codes. Returns 0 when equal. USAGE } APP="" LHS="pyvm" RHS="llvmlite" TIMEOUT="12" SHOW_DIFF=0 while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0;; --lhs) LHS="$2"; shift 2;; --rhs) RHS="$2"; shift 2;; --timeout) TIMEOUT="$2"; shift 2;; --show-diff) SHOW_DIFF=1; shift;; *) APP="$1"; shift;; esac done if [[ -z "$APP" ]]; then usage; exit 1 fi if [[ ! -f "$APP" ]]; then echo "error: app not found: $APP" >&2 exit 2 fi ROOT=$(cd "$(dirname "$0")/.." && pwd) NYASH_BIN="$ROOT/target/release/nyash" if [[ ! -x "$NYASH_BIN" ]]; then echo "[build] nyash not found; building release ..." >&2 (cd "$ROOT" && cargo build --release >/dev/null) fi has_cmd() { command -v "$1" >/dev/null 2>&1; } normalize() { # Remove runner/plugin noise and blank lines sed -E \ -e 's/\r$//' \ -e '/^\[ConsoleBox\]/d' \ -e '/^\[FileBox\]/d' \ -e '/^\[plugin-loader\]/d' \ -e '/^\[Runner\//d' \ -e '/^DEBUG:/d' \ -e '/^🔌/d' \ -e '/^✅/d' \ -e '/^🚀/d' \ -e '/^❌/d' \ -e '/^⚡/d' \ -e '/^🦀/d' \ -e '/^🧠/d' \ -e '/^📊/d' \ -e '/^\[TRACE\]/d' \ -e '/^Result(Type)?\(/d' \ -e '/^Result:/d' \ -e 's/__EXIT_CODE__=[-0-9]+$//' \ -e 's/[[:space:]]+$//' \ -e '/^$/d' } run_pyvm() { local app="$1" local out code if has_cmd timeout; then out=$(NYASH_VM_USE_PY=1 timeout "${TIMEOUT}s" "$NYASH_BIN" --backend vm "$app" 2>&1) || code=$? else out=$(NYASH_VM_USE_PY=1 "$NYASH_BIN" --backend vm "$app" 2>&1) || code=$? fi code=${code:-0} # Ensure a trailing newline so the exit-code marker becomes a separate line printf '%s\n' "$out" | normalize echo "__EXIT_CODE__=$code" } run_vm() { local app="$1" local out code if has_cmd timeout; then out=$(timeout "${TIMEOUT}s" "$NYASH_BIN" --backend vm "$app" 2>&1) || code=$? else out=$("$NYASH_BIN" --backend vm "$app" 2>&1) || code=$? fi code=${code:-0} printf '%s\n' "$out" | normalize echo "__EXIT_CODE__=$code" } run_llvmlite() { local app="$1" if ! has_cmd llvm-config-18; then echo "error: llvm-config-18 not found (required for llvmlite parity)." >&2 exit 3 fi local stem exe stem=$(basename "$app"); stem=${stem%.hako} exe="$ROOT/app_parity_${stem}" NYASH_LLVM_FEATURE=llvm NYASH_LLVM_SKIP_VERIFY=1 "${ROOT}/tools/build_llvm.sh" "$app" -o "$exe" >/dev/null || true if [[ ! -x "$exe" ]]; then echo "error: failed to build llvmlite executable: $exe" >&2 exit 4 fi local out code if has_cmd timeout; then out=$(timeout "${TIMEOUT}s" "$exe" 2>&1) || code=$? else out=$("$exe" 2>&1) || code=$? fi code=${code:-0} printf '%s' "$out" | normalize echo "__EXIT_CODE__=$code" } run_mode() { local mode="$1" app="$2" case "$mode" in pyvm) run_pyvm "$app" ;; vm) run_vm "$app" ;; llvmlite) run_llvmlite "$app" ;; *) echo "error: unknown mode: $mode" >&2; exit 5;; esac } LEFT=$(run_mode "$LHS" "$APP") RIGHT=$(run_mode "$RHS" "$APP") LCODE=$(printf '%s\n' "$LEFT" | sed -n 's/^__EXIT_CODE__=//p') RCODE=$(printf '%s\n' "$RIGHT" | sed -n 's/^__EXIT_CODE__=//p') # Drop explicit exit marker lines and also any trailing "__EXIT_CODE__=N" suffix accidentally glued to stdout LOUT=$(printf '%s\n' "$LEFT" | sed -E -e '/^__EXIT_CODE__=/d' -e 's/__EXIT_CODE__=[-0-9]+$//') ROUT=$(printf '%s\n' "$RIGHT" | sed -E -e '/^__EXIT_CODE__=/d' -e 's/__EXIT_CODE__=[-0-9]+$//') STATUS=0 if [[ "$LCODE" != "$RCODE" ]]; then echo "[parity] exit code differs: $LHS=$LCODE, $RHS=$RCODE" >&2 STATUS=1 fi if [[ "$LOUT" != "$ROUT" ]]; then echo "[parity] stdout differs" >&2 if [[ "$SHOW_DIFF" -eq 1 ]]; then diff -u <(printf '%s\n' "$LOUT") <(printf '%s\n' "$ROUT") || true fi STATUS=1 fi if [[ "$STATUS" -eq 0 ]]; then echo "✅ parity OK ($LHS == $RHS)" >&2 else echo "❌ parity mismatch ($LHS != $RHS)" >&2 fi exit "$STATUS"