#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPILER="$ROOT_DIR/build/bin/compiler" DEFAULT_TEST_ROOT="$ROOT_DIR/test" TMP_DIR="$ROOT_DIR/build/test_passes" CC_BIN="${CC:-cc}" LLC_BIN="${LLC:-llc}" CLANG_BIN="${CLANG:-clang}" RUNTIME_SRC="$ROOT_DIR/sylib/sylib.c" RUNTIME_OBJ="$TMP_DIR/sylib.o" debug=false run_exec=false test_root="$DEFAULT_TEST_ROOT" stop_on_fail=false strict_mem2reg=false usage() { cat < 指定测试根目录,默认: $DEFAULT_TEST_ROOT --stop-on-fail 遇到第一个失败立即退出 --strict-mem2reg 将优化后残留标量 alloca 视为失败;默认只作为警告统计 -h, --help 显示帮助 环境变量: LLC= 指定 llc,默认 llc CLANG= 指定 clang,默认 clang CC= 指定 C 编译器,用于编译 sylib.c,默认 cc EOF } while [[ $# -gt 0 ]]; do case "$1" in --run) run_exec=true shift ;; --debug) debug=true shift ;; --test-root) if [[ $# -lt 2 ]]; then echo "--test-root 需要目录参数" >&2 exit 1 fi test_root="$2" shift 2 ;; --stop-on-fail) stop_on_fail=true shift ;; --strict-mem2reg) strict_mem2reg=true shift ;; -h|--help) usage exit 0 ;; *) echo "未知参数: $1" >&2 usage >&2 exit 1 ;; esac done if [[ ! -x "$COMPILER" ]]; then echo "未找到编译器: $COMPILER" >&2 echo "请先构建编译器,例如: cmake -S . -B build && cmake --build build -j" >&2 exit 1 fi if [[ ! -d "$test_root" ]]; then echo "测试目录不存在: $test_root" >&2 exit 1 fi mkdir -p "$TMP_DIR" runtime_ready=0 if [[ "$run_exec" == true ]]; then if ! command -v "$LLC_BIN" >/dev/null 2>&1; then echo "未找到 llc: $LLC_BIN" >&2 exit 1 fi if ! command -v "$CLANG_BIN" >/dev/null 2>&1; then echo "未找到 clang: $CLANG_BIN" >&2 exit 1 fi if [[ -f "$RUNTIME_SRC" ]]; then if "$CC_BIN" -c "$RUNTIME_SRC" -o "$RUNTIME_OBJ" >/dev/null 2>&1; then runtime_ready=1 else echo "[WARN] 运行库编译失败,将只链接目标文件: $RUNTIME_SRC" >&2 fi else echo "[WARN] 未找到运行库源码,将只链接目标文件: $RUNTIME_SRC" >&2 fi fi normalize_file() { sed 's/\r$//' "$1" } make_case_out_dir() { local input=$1 local rel rel=$(realpath --relative-to="$test_root" "$(dirname "$input")") echo "$TMP_DIR/$rel" } extract_ir() { local raw_file=$1 local ll_file=$2 # 编译器在 debug 模式下可能把诊断也写到 stdout;这里保留 LLVM-like IR 行。 grep -E '^(define |declare |@|[[:space:]]|})|^[A-Za-z_.$%][A-Za-z0-9_.$%]*:$' \ "$raw_file" > "$ll_file" || true } record_failure() { local bucket=$1 local message=$2 case "$bucket" in ir) ir_failures+=("$message") ;; opt) opt_failures+=("$message") ;; run) run_failures+=("$message") ;; esac if [[ "$stop_on_fail" == true ]]; then echo "" echo "遇到失败,按 --stop-on-fail 停止。失败文件保留在: $TMP_DIR" exit 1 fi } record_warning() { local bucket=$1 local message=$2 case "$bucket" in opt) opt_warnings+=("$message") ;; esac } check_scalar_mem2reg() { local ll_file=$1 grep -nE '=[[:space:]]*alloca[[:space:]]+(i32|float|i1)\b' "$ll_file" || true } compare_result() { local input=$1 local expected_file=$2 local stdout_file=$3 local status=$4 local actual_file="${stdout_file%.stdout}.actual.out" { cat "$stdout_file" if [[ -s "$stdout_file" ]] && [[ "$(tail -c 1 "$stdout_file" | wc -l)" -eq 0 ]]; then printf '\n' fi printf '%s\n' "$status" } > "$actual_file" local expected_text local actual_text expected_text=$(normalize_file "$expected_file") actual_text=$(normalize_file "$actual_file") if [[ "$expected_text" == "$actual_text" ]]; then echo " [RUN] OK" return 0 fi echo " [RUN] FAIL: 输出或退出码不匹配" echo " expected: $expected_file" echo " actual: $actual_file" if [[ "$debug" == true ]]; then diff -u <(printf '%s\n' "$expected_text") <(printf '%s\n' "$actual_text") || true fi record_failure run "$input: output mismatch" return 1 } mapfile -t test_files < <(find "$test_root" -type f -name '*.sy' | sort) if [[ ${#test_files[@]} -eq 0 ]]; then echo "未在目录中找到 .sy 测试: $test_root" >&2 exit 1 fi ir_total=0 ir_pass=0 opt_total=0 opt_pass=0 run_total=0 run_pass=0 ir_failures=() opt_failures=() opt_warnings=() run_failures=() echo "测试根目录: $test_root" echo "输出目录: $TMP_DIR" echo "测试数量: ${#test_files[@]}" if [[ "$run_exec" == true ]]; then echo "运行验证: 开启" else echo "运行验证: 关闭(加 --run 可开启语义对拍)" fi echo "" for input in "${test_files[@]}"; do ir_total=$((ir_total + 1)) opt_total=$((opt_total + 1)) out_dir=$(make_case_out_dir "$input") mkdir -p "$out_dir" base=$(basename "$input") stem=${base%.sy} raw_ir="$out_dir/$stem.raw.ll" ll_file="$out_dir/$stem.ll" log_file="$out_dir/$stem.compiler.log" stdout_file="$out_dir/$stem.stdout" obj_file="$out_dir/$stem.o" exe_file="$out_dir/$stem" input_dir=$(dirname "$input") stdin_file="$input_dir/$stem.in" expected_file="$input_dir/$stem.out" echo "[TEST] ${input#$ROOT_DIR/}" if [[ "$debug" == true ]]; then echo " [CMD] $COMPILER --emit-ir $input" fi compiler_status=0 "$COMPILER" --emit-ir "$input" > "$raw_ir" 2> "$log_file" || compiler_status=$? extract_ir "$raw_ir" "$ll_file" if [[ $compiler_status -ne 0 ]]; then echo " [IR] FAIL: 编译器返回 $compiler_status" record_failure ir "$input: compiler failed ($compiler_status)" continue fi if ! grep -qE '^define ' "$ll_file"; then echo " [IR] FAIL: 未提取到有效函数定义" record_failure ir "$input: invalid IR" continue fi ir_pass=$((ir_pass + 1)) echo " [IR] OK" scalar_allocas=$(check_scalar_mem2reg "$ll_file") if [[ -n "$scalar_allocas" ]]; then if [[ "$strict_mem2reg" == true ]]; then echo " [OPT] FAIL: 优化后仍有可提升标量 alloca" else echo " [OPT] WARN: 优化后仍有标量 alloca 残留" fi if [[ "$debug" == true ]]; then echo "$scalar_allocas" | sed 's/^/ /' fi if [[ "$strict_mem2reg" == true ]]; then record_failure opt "$input: scalar alloca remains" else opt_pass=$((opt_pass + 1)) record_warning opt "$input: scalar alloca remains" fi else opt_pass=$((opt_pass + 1)) echo " [OPT] OK: 未发现标量 alloca 残留" fi if [[ "$run_exec" != true ]]; then continue fi if [[ ! -f "$expected_file" ]]; then echo " [RUN] SKIP: 未找到期望输出 $expected_file" continue fi run_total=$((run_total + 1)) if ! "$LLC_BIN" -filetype=obj "$ll_file" -o "$obj_file" > "$stdout_file" 2>&1; then echo " [RUN] FAIL: llc 生成对象文件失败" record_failure run "$input: llc failed" continue fi if [[ $runtime_ready -eq 1 ]]; then if ! "$CLANG_BIN" "$obj_file" "$RUNTIME_OBJ" -o "$exe_file" >> "$stdout_file" 2>&1; then echo " [RUN] FAIL: clang 链接失败" record_failure run "$input: clang link failed" continue fi else if ! "$CLANG_BIN" "$obj_file" -o "$exe_file" >> "$stdout_file" 2>&1; then echo " [RUN] FAIL: clang 链接失败" record_failure run "$input: clang link failed" continue fi fi run_status=0 if [[ -f "$stdin_file" ]]; then "$exe_file" < "$stdin_file" > "$stdout_file" 2>&1 || run_status=$? else "$exe_file" > "$stdout_file" 2>&1 || run_status=$? fi if compare_result "$input" "$expected_file" "$stdout_file" "$run_status"; then run_pass=$((run_pass + 1)) fi done echo "" echo "测试完成。" echo "IR 生成: $ir_pass / $ir_total" echo "Pass 优化检查: $opt_pass / $opt_total" if [[ "$run_exec" == true ]]; then echo "运行结果: $run_pass / $run_total" fi if [[ ${#ir_failures[@]} -gt 0 ]]; then echo "" echo "IR 失败列表:" for item in "${ir_failures[@]}"; do echo " $item" done fi if [[ ${#opt_failures[@]} -gt 0 ]]; then echo "" echo "优化检查失败列表:" for item in "${opt_failures[@]}"; do echo " $item" done fi if [[ ${#opt_warnings[@]} -gt 0 ]]; then echo "" echo "优化警告列表(默认不算失败;加 --strict-mem2reg 可升级为失败):" for item in "${opt_warnings[@]}"; do echo " $item" done fi if [[ ${#run_failures[@]} -gt 0 ]]; then echo "" echo "运行失败列表:" for item in "${run_failures[@]}"; do echo " $item" done fi if [[ ${#ir_failures[@]} -gt 0 || ${#opt_failures[@]} -gt 0 || ${#run_failures[@]} -gt 0 ]]; then echo "" echo "失败产物已保留在: $TMP_DIR" exit 1 fi echo "" echo "全部检查通过。"