diff --git a/.gitignore b/.gitignore index 25e003da..8e0c979c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ tests/ui/failing/* -tests/ui/passing/* \ No newline at end of file +tests/ui/passing/* +tests/ui/debug/* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6e3a24..c6e70ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,10 +27,15 @@ retroactive best-effort summary; earlier changes were not formally tracked. - Restructured `printer.rs` into a declarative 3-phase pipeline: `collect_items` -> `collect_and_analyze_items` -> `assemble_smir`, with `CollectedCrate`/`DerivedInfo` interface types enforcing the boundary between collection and assembly - Added `AllocMap` with debug-mode coherence checking (`verify_coherence`): asserts that every `AllocId` in stored bodies exists in the alloc map and that no body is walked twice; zero cost in release builds - Removed dead static-item fixup from `assemble_smir` (violated the phase boundary, misclassified statics as functions; never triggered in integration tests) +- Rewrote `run_ui_tests.sh`: flag-based CLI (`--verbose`, `--save-generated-output`, `--save-debug-output`), build-once-then-invoke (eliminates per-test cargo overhead), arch-aware skip logic for cross-platform test lists +- UI test runners now extract and pass `//@ compile-flags:`, `//@ edition:`, and `//@ rustc-env:` directives from test files (previously silently ignored) - Switched from compiler build to `rustup`-managed toolchain ([#33]) - Removed forked rust dependency ([#19]) ### Fixed +- Fixed `get_prov_ty` to recursively walk nested struct/tuple fields when resolving provenance types; previously used exact byte-offset matching which panicked on pointers inside nested structs (e.g., `&str` at offset 56 inside `TestDesc` inside `TestDescAndFn`) +- Removed incorrect `builtin_deref` assertions from VTable and Static allocation collection that rejected valid non-pointer types (raw `*const ()` vtable pointers, non-pointer statics) +- Replaced panicking `unwrap`/`assert` calls in `get_prov_ty` with graceful fallbacks for layout failures, non-rigid types, and unexpected offsets - Fixed early `return` in `BodyAnalyzer::visit_terminator` that skipped `super_terminator()`, causing alloc/type/span collection to miss everything inside `Call` terminators with non-`ZeroSized` function operands (const-evaluated function pointers); bug present since [`aff2dd0`](https://github.com/runtimeverification/stable-mir-json/commit/aff2dd0) - Avoided duplicate `inst.body()` calls that were reallocating `AllocId`s ([#120]) - Prevented svg/png generation when `dot` is unavailable ([#117]) diff --git a/Makefile b/Makefile index 3d41d14e..f649218d 100644 --- a/Makefile +++ b/Makefile @@ -63,12 +63,7 @@ remake-ui-tests: test-ui: VERBOSE?=0 test-ui: - # Check if RUST_DIR_ROOT is set - if [ -z "$$RUST_DIR_ROOT" ]; then \ - echo "Error: RUST_DIR_ROOT is not set. Please set it to the absolute path to rust compiler checkout."; \ - exit 1; \ - fi - bash tests/ui/run_ui_tests.sh "$$RUST_DIR_ROOT" "${VERBOSE}" + bash tests/ui/run_ui_tests.sh $(if $(filter 1,$(VERBOSE)),--verbose) "$$RUST_DIR_ROOT" .PHONY: dot svg png d2 clean-graphs check-graphviz diff --git a/src/printer.rs b/src/printer.rs index 9f74261f..f2209626 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -844,40 +844,72 @@ fn update_link_map<'tcx>( /// Returns the field index (source order) for a given offset and layout if /// the layout contains fields (shared between all variants), otherwise None. /// NB No search for fields within variants (needs recursive call). -fn field_for_offset(l: LayoutShape, offset: usize) -> Option { - // FieldIdx - match l.fields { +fn field_for_offset(l: &LayoutShape, offset: usize) -> Option { + match &l.fields { FieldsShape::Primitive | FieldsShape::Union(_) | FieldsShape::Array { .. } => None, + FieldsShape::Arbitrary { offsets } => offsets + .iter() + .enumerate() + .find(|(_, o)| o.bytes() == offset) + .map(|(i, _)| i), + } +} + +/// Find the field whose byte range contains the given offset. +/// Returns (field_index, field_start_byte_offset). +fn field_containing_offset(l: &LayoutShape, offset: usize) -> Option<(usize, usize)> { + match &l.fields { FieldsShape::Arbitrary { offsets } => { - let fields: Vec = offsets.into_iter().map(|o| o.bytes()).collect(); - fields - .into_iter() + let mut indexed: Vec<(usize, usize)> = offsets + .iter() .enumerate() - .find(|(_, o)| *o == offset) - .map(|(i, _)| i) + .map(|(i, o)| (i, o.bytes())) + .collect(); + indexed.sort_by_key(|&(_, o)| o); + // The containing field is the one with the largest start offset <= target. + indexed.into_iter().rev().find(|&(_, o)| o <= offset) } + _ => None, } } fn get_prov_ty(ty: stable_mir::ty::Ty, offset: &usize) -> Option { use stable_mir::ty::RigidTy; let ty_kind = ty.kind(); + debug_log_println!("get_prov_ty: {:?} offset={}", ty_kind, offset); // if ty is a pointer, box, or Ref, expect no offset and dereference - if let Some(ty) = ty_kind.builtin_deref(true) { - assert!(*offset == 0); - return Some(ty.ty); + if let Some(derefed) = ty_kind.builtin_deref(true) { + if *offset != 0 { + eprintln!( + "get_prov_ty: unexpected non-zero offset {} for builtin_deref type {:?}", + offset, ty_kind + ); + return None; + } + debug_log_println!("get_prov_ty: resolved -> pointee {:?}", derefed.ty.kind()); + return Some(derefed.ty); } // Otherwise the allocation is a reference within another kind of data. // Decompose this outer data type to determine the reference type - let layout = ty - .layout() - .map(|l| l.shape()) - .expect("Unable to get layout for {ty_kind:?}"); - let ref_ty = match ty_kind - .rigid() - .expect("Non-rigid-ty allocation found! {ty_kind:?}") - { + let layout = match ty.layout().map(|l| l.shape()) { + Ok(l) => l, + Err(_) => { + eprintln!("get_prov_ty: unable to get layout for {:?}", ty_kind); + return None; + } + }; + let rigid = match ty_kind.rigid() { + Some(r) => r, + None => { + eprintln!( + "get_prov_ty: non-rigid type in allocation: {:?} (offset={})", + ty_kind, offset + ); + return None; + } + }; + let ref_ty = match rigid { // homogenous, so no choice. Could check alignment of the offset... RigidTy::Array(ty, _) | RigidTy::Slice(ty) => Some(*ty), // cases covered above @@ -887,16 +919,24 @@ fn get_prov_ty(ty: stable_mir::ty::Ty, offset: &usize) -> Option { unreachable!("Covered by builtin_deref above") } - // For other structs, consult layout to determine field type + // For structs, find the field containing this offset and recurse. + // The provenance offset may point into a nested struct field, so we + // walk down through the field hierarchy until we reach the pointer. RigidTy::Adt(adt_def, args) if ty_kind.is_struct() => { - let field_idx = field_for_offset(layout, *offset).unwrap(); + let (field_idx, field_start) = field_containing_offset(&layout, *offset)?; // NB struct, single variant - let fields = adt_def.variants().pop().map(|v| v.fields()).unwrap(); - fields.get(field_idx).map(|f| f.ty_with_args(args)) + let fields = adt_def.variants().pop().map(|v| v.fields())?; + let field_ty = fields.get(field_idx)?.ty_with_args(args); + let relative_offset = *offset - field_start; + debug_log_println!( + "get_prov_ty: struct {:?} offset={} -> field {} (start={}) type {:?}, relative_offset={}", + adt_def, offset, field_idx, field_start, field_ty.kind(), relative_offset + ); + return get_prov_ty(field_ty, &relative_offset); } RigidTy::Adt(_adt_def, _args) if ty_kind.is_enum() => { // we have to figure out which variant we are dealing with (requires the data) - match field_for_offset(layout, *offset) { + match field_for_offset(&layout, *offset) { None => // FIXME we'd have to figure out which variant we are dealing with (requires the data) { @@ -909,9 +949,20 @@ fn get_prov_ty(ty: stable_mir::ty::Ty, offset: &usize) -> Option { - let field_idx = field_for_offset(layout, *offset)?; - fields.get(field_idx).copied() + let (field_idx, field_start) = field_containing_offset(&layout, *offset)?; + let field_ty = *fields.get(field_idx)?; + let relative_offset = *offset - field_start; + debug_log_println!( + "get_prov_ty: tuple offset={} -> field {} (start={}) type {:?}, relative_offset={}", + offset, + field_idx, + field_start, + field_ty.kind(), + relative_offset + ); + return get_prov_ty(field_ty, &relative_offset); } RigidTy::FnPtr(_) => None, _unimplemented => { @@ -972,21 +1023,11 @@ fn collect_alloc( } } GlobalAlloc::Static(_) => { - assert!( - kind.clone().builtin_deref(true).is_some(), - "Allocated pointer is not a built-in pointer type: {:?}", - kind - ); val_collector .visited_allocs .insert(val, (ty, global_alloc.clone())); } GlobalAlloc::VTable(_, _) => { - assert!( - kind.clone().builtin_deref(true).is_some(), - "Allocated pointer is not a built-in pointer type: {:?}", - kind - ); val_collector .visited_allocs .insert(val, (ty, global_alloc.clone())); diff --git a/tests/ui/failing.tsv b/tests/ui/failing.tsv index 28da13f7..3d0b48b9 100644 --- a/tests/ui/failing.tsv +++ b/tests/ui/failing.tsv @@ -1,25 +1,2 @@ -tests/ui/async-await/issue-60709.rs 101 -tests/ui/async-await/issues/issue-59972.rs 101 -tests/ui/box/thin_align.rs 101 -tests/ui/box/thin_drop.rs 101 -tests/ui/box/thin_new.rs 101 -tests/ui/box/thin_zst.rs 101 -tests/ui/cfg/cfg_attr.rs 101 -tests/ui/cfg/cfgs-on-items.rs 101 -tests/ui/consts/const-eval/const_fn_ptr.rs 101 -tests/ui/consts/const-int-arithmetic-overflow.rs 101 -tests/ui/consts/const-trait-to-trait.rs 101 -tests/ui/env-macro/env-env-overload.rs 101 -tests/ui/env-macro/env-env.rs 101 -tests/ui/fmt/fmt_debug/none.rs 101 -tests/ui/issues/issue-11205.rs 101 tests/ui/issues/issue-26641.rs 101 -tests/ui/label/label_break_value_desugared_break.rs 101 -tests/ui/macros/macro-meta-items.rs 101 -tests/ui/rfcs/rfc-2091-track-caller/caller-location-fnptr-rt-ctfe-equiv.rs 101 -tests/ui/test-attrs/test-panic-while-printing.rs 101 -tests/ui/traits/const-traits/const-drop.rs 101 -tests/ui/try-block/issue-45124.rs 101 -tests/ui/try-block/try-block-in-match.rs 101 -tests/ui/try-block/try-block-in-return.rs 101 -tests/ui/try-block/try-block.rs 101 +tests/ui/sanitizer/cfi/drop-in-place.rs 101 diff --git a/tests/ui/passing.tsv b/tests/ui/passing.tsv index 8def20ae..d802f401 100644 --- a/tests/ui/passing.tsv +++ b/tests/ui/passing.tsv @@ -181,6 +181,8 @@ tests/ui/associated-types/issue-54467.rs tests/ui/associated-types/issue-55846.rs tests/ui/associated-types/object-method-numbering.rs tests/ui/async-await/context-is-sorta-unwindsafe.rs +tests/ui/async-await/issue-60709.rs +tests/ui/async-await/issues/issue-59972.rs tests/ui/attributes/tool_attributes.rs tests/ui/augmented-assignments-rpass.rs tests/ui/auto-instantiate.rs @@ -350,6 +352,10 @@ tests/ui/box/into-boxed-slice.rs tests/ui/box/new-box-syntax.rs tests/ui/box/new-box.rs tests/ui/box/new.rs +tests/ui/box/thin_align.rs +tests/ui/box/thin_drop.rs +tests/ui/box/thin_new.rs +tests/ui/box/thin_zst.rs tests/ui/box/unit/expr-block-generic-unique1.rs tests/ui/box/unit/expr-block-generic-unique2.rs tests/ui/box/unit/expr-if-unique.rs @@ -412,7 +418,9 @@ tests/ui/cfg/cfg-macros-notfoo.rs tests/ui/cfg/cfg-target-abi.rs tests/ui/cfg/cfg-target-compact.rs tests/ui/cfg/cfg-target-vendor.rs +tests/ui/cfg/cfg_attr.rs tests/ui/cfg/cfg_stmt_expr.rs +tests/ui/cfg/cfgs-on-items.rs tests/ui/cfg/conditional-compile.rs tests/ui/cfg/true-false.rs tests/ui/cfguard-run.rs @@ -629,6 +637,7 @@ tests/ui/consts/const-enum-vec-index.rs tests/ui/consts/const-enum-vec-ptr.rs tests/ui/consts/const-enum-vector.rs tests/ui/consts/const-err-rpass.rs +tests/ui/consts/const-eval/const_fn_ptr.rs tests/ui/consts/const-eval/enum_discr.rs tests/ui/consts/const-eval/float_methods.rs tests/ui/consts/const-eval/heap/alloc_intrinsic_nontransient.rs @@ -654,6 +663,7 @@ tests/ui/consts/const-fn-type-name.rs tests/ui/consts/const-fn-val.rs tests/ui/consts/const-fn.rs tests/ui/consts/const-index-feature-gate.rs +tests/ui/consts/const-int-arithmetic-overflow.rs tests/ui/consts/const-int-arithmetic.rs tests/ui/consts/const-int-conversion-rpass.rs tests/ui/consts/const-int-overflowing-rpass.rs @@ -678,6 +688,7 @@ tests/ui/consts/const-repeated-values.rs tests/ui/consts/const-size_of-align_of.rs tests/ui/consts/const-size_of_val-align_of_val.rs tests/ui/consts/const-struct.rs +tests/ui/consts/const-trait-to-trait.rs tests/ui/consts/const-tuple-struct.rs tests/ui/consts/const-typeid-of-rpass.rs tests/ui/consts/const-unit-struct.rs @@ -959,6 +970,8 @@ tests/ui/enum/issue-19340-2.rs tests/ui/enum/issue-23304-1.rs tests/ui/enum/issue-23304-2.rs tests/ui/enum/issue-42747.rs +tests/ui/env-macro/env-env-overload.rs +tests/ui/env-macro/env-env.rs tests/ui/env-macro/option_env-not-defined.rs tests/ui/errors/remap-path-prefix-macro.rs tests/ui/explicit-i-suffix.rs @@ -998,6 +1011,7 @@ tests/ui/float/classify-runtime-const.rs tests/ui/float/conv-bits-runtime-const.rs tests/ui/float/int-to-float-miscompile-issue-105626.rs tests/ui/fmt/fmt_debug/full.rs +tests/ui/fmt/fmt_debug/none.rs tests/ui/fmt/fmt_debug/shallow.rs tests/ui/fmt/format-args-capture-macro-hygiene-pass.rs tests/ui/fmt/format-args-capture.rs @@ -1249,6 +1263,7 @@ tests/ui/issues/issue-10802.rs tests/ui/issues/issue-10806.rs tests/ui/issues/issue-11047.rs tests/ui/issues/issue-11085.rs +tests/ui/issues/issue-11205.rs tests/ui/issues/issue-11267.rs tests/ui/issues/issue-11382.rs tests/ui/issues/issue-11552.rs @@ -1647,6 +1662,7 @@ tests/ui/iterators/iter-sum-overflow-debug.rs tests/ui/iterators/iter-sum-overflow-ndebug.rs tests/ui/iterators/iter-sum-overflow-overflow-checks.rs tests/ui/iterators/skip-count-overflow.rs +tests/ui/label/label_break_value_desugared_break.rs tests/ui/last-use-in-cap-clause.rs tests/ui/last-use-is-capture.rs tests/ui/late-bound-lifetimes/issue-36381.rs @@ -1742,6 +1758,7 @@ tests/ui/macros/macro-lifetime-used-with-labels.rs tests/ui/macros/macro-lifetime-used-with-static.rs tests/ui/macros/macro-lifetime.rs tests/ui/macros/macro-literal.rs +tests/ui/macros/macro-meta-items.rs tests/ui/macros/macro-metavar-expr-concat/allowed-operations.rs tests/ui/macros/macro-metavar-expr-concat/repetitions.rs tests/ui/macros/macro-metavar-expr-concat/unicode-expansion.rs @@ -2271,6 +2288,7 @@ tests/ui/rfcs/rfc-2008-non-exhaustive/structs_same_crate.rs tests/ui/rfcs/rfc-2008-non-exhaustive/variants_same_crate.rs tests/ui/rfcs/rfc-2027-dyn-compatible-for-dispatch/manual-self-impl-for-unsafe-obj.rs tests/ui/rfcs/rfc-2091-track-caller/call-chain.rs +tests/ui/rfcs/rfc-2091-track-caller/caller-location-fnptr-rt-ctfe-equiv.rs tests/ui/rfcs/rfc-2091-track-caller/caller-location-intrinsic.rs tests/ui/rfcs/rfc-2091-track-caller/const-caller-location.rs tests/ui/rfcs/rfc-2091-track-caller/intrinsic-wrapper.rs @@ -2308,7 +2326,6 @@ tests/ui/runtime/stdout-before-main.rs tests/ui/runtime/stdout-during-shutdown-unix.rs tests/ui/runtime/stdout-during-shutdown-windows.rs tests/ui/sanitizer/cfi/complex-receiver.rs -tests/ui/sanitizer/cfi/drop-in-place.rs tests/ui/sanitizer/cfi/fn-ptr.rs tests/ui/sanitizer/cfi/self-ref.rs tests/ui/sanitizer/cfi/sized-associated-ty.rs @@ -2527,6 +2544,7 @@ tests/ui/syntax-extension-minor.rs tests/ui/tail-call-arg-leak.rs tests/ui/tail-cps.rs tests/ui/test-attrs/test-main-not-dead.rs +tests/ui/test-attrs/test-panic-while-printing.rs tests/ui/test-attrs/test-runner-hides-main.rs tests/ui/thread-local/tls.rs tests/ui/threads-sendsync/child-outlives-parent.rs @@ -2602,6 +2620,7 @@ tests/ui/traits/bug-7295.rs tests/ui/traits/coercion-generic.rs tests/ui/traits/coercion.rs tests/ui/traits/conditional-dispatch.rs +tests/ui/traits/const-traits/const-drop.rs tests/ui/traits/const-traits/specialization/const-default-const-specialized.rs tests/ui/traits/const-traits/specialization/non-const-default-const-specialized.rs tests/ui/traits/const-traits/trait-where-clause-run.rs @@ -2713,6 +2732,10 @@ tests/ui/traits/with-bounds-default.rs tests/ui/transmute-non-immediate-to-immediate.rs tests/ui/transmute/transmute-zst-generics.rs tests/ui/trivial_casts-rpass.rs +tests/ui/try-block/issue-45124.rs +tests/ui/try-block/try-block-in-match.rs +tests/ui/try-block/try-block-in-return.rs +tests/ui/try-block/try-block.rs tests/ui/try-block/try-is-identifier-edition2015.rs tests/ui/try-from-int-error-partial-eq.rs tests/ui/try-operator-hygiene.rs diff --git a/tests/ui/remake_ui_tests.sh b/tests/ui/remake_ui_tests.sh index 29436693..0ca685a7 100755 --- a/tests/ui/remake_ui_tests.sh +++ b/tests/ui/remake_ui_tests.sh @@ -35,6 +35,42 @@ if [ -n "${KEEP_OUTPUT}" ]; then mkdir -p "$FAILING_DIR" "$PASSING_DIR" fi +# Extract //@ compile-flags: and //@ edition: directives from a test file. +# Prints space-separated flags to stdout. +extract_test_flags() { + local file="$1" + local flags="" + + # Extract //@ compile-flags: directives (everything after "compile-flags:") + local compile_flags + compile_flags=$(grep -s '^[[:space:]]*//@[[:space:]]*compile-flags:' "$file" \ + | sed 's/^.*compile-flags:[[:space:]]*//' || true) + if [ -n "$compile_flags" ]; then + flags="$compile_flags" + fi + + # Extract //@ edition: directive (e.g., "//@ edition: 2021") + local edition + edition=$(grep -s '^[[:space:]]*//@[[:space:]]*edition:' "$file" \ + | sed 's/^.*edition:[[:space:]]*//' | head -1 || true) + if [ -n "$edition" ]; then + flags="$flags --edition $edition" + fi + + # Extract //@ rustc-env: directives (e.g., "//@ rustc-env:MY_VAR=value") + # These set environment variables for the rustc process via --env-set. + local rustc_envs + rustc_envs=$(grep -s '^[[:space:]]*//@[[:space:]]*rustc-env:' "$file" \ + | sed 's/^.*rustc-env:[[:space:]]*//' || true) + if [ -n "$rustc_envs" ]; then + while IFS= read -r env_pair; do + flags="$flags --env-set $env_pair -Zunstable-options" + done <<< "$rustc_envs" + fi + + echo "$flags" +} + echo "Running UI tests..." while read -r test; do full_path="$RUST_DIR/$test" @@ -45,7 +81,9 @@ while read -r test; do fi echo "Running test: $test" - cargo run -- -Zno-codegen "$full_path" > tmp.stdout 2> tmp.stderr + test_flags=$(extract_test_flags "$full_path") + # shellcheck disable=SC2086 # intentional word-splitting of test_flags + cargo run -- -Zno-codegen ${test_flags} "$full_path" > tmp.stdout 2> tmp.stderr status=$? base_test="$(basename "$test")" json_file="${PWD}/$(basename "$test" .rs).smir.json" diff --git a/tests/ui/run_ui_tests.sh b/tests/ui/run_ui_tests.sh index 99f8f518..fbaf8189 100755 --- a/tests/ui/run_ui_tests.sh +++ b/tests/ui/run_ui_tests.sh @@ -1,72 +1,265 @@ #!/usr/bin/env bash +set -euo pipefail -set -u +usage() { + cat <<'EOF' +Usage: run_ui_tests.sh [--verbose] [--save-generated-output] [--save-debug-output] RUST_DIR_ROOT -USAGE="Usage: $0 RUST_DIR_ROOT [VERBOSE]\n -'RUST_DIR_ROOT' is the Rust directory to take ui tests from. Optional 'VERBOSE' can be set to '1' for verbose output." +Options: + --verbose Print passing and skipped tests. + --save-generated-output Do not delete generated *.smir.json files. + --save-debug-output On failure, print stderr snippet inline and save + full stderr to /debug/.stderr. + --help, -h Show this help. -if [ $# -lt 1 ]; then - echo -e "$USAGE" - exit 1 -else - VERBOSE="$2" -fi +Environment: + RUN_SMIR Optional override for the runner command. + Example: RUN_SMIR=./target/debug/stable_mir_json + (If you include flags, they will be split on spaces.) +EOF +} + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +log() { + printf '%s\n' "$*" +} + +# ------------------------- +# Extract test directives +# ------------------------- + +# Extract //@ compile-flags:, //@ edition:, and //@ rustc-env: directives +# from a test file. Prints space-separated flags to stdout. +extract_test_flags() { + local file="$1" + local flags="" + + # Extract //@ compile-flags: directives (everything after "compile-flags:") + local compile_flags + compile_flags=$(grep -s '^[[:space:]]*//@[[:space:]]*compile-flags:' "$file" \ + | sed 's/^.*compile-flags:[[:space:]]*//' || true) + if [ -n "$compile_flags" ]; then + flags="$compile_flags" + fi + + # Extract //@ edition: directive (e.g., "//@ edition: 2021") + local edition + edition=$(grep -s '^[[:space:]]*//@[[:space:]]*edition:' "$file" \ + | sed 's/^.*edition:[[:space:]]*//' | head -1 || true) + if [ -n "$edition" ]; then + flags="$flags --edition $edition" + fi + + # Extract //@ rustc-env: directives (e.g., "//@ rustc-env:MY_VAR=value") + # These set environment variables for the rustc process via --env-set. + local rustc_envs + rustc_envs=$(grep -s '^[[:space:]]*//@[[:space:]]*rustc-env:' "$file" \ + | sed 's/^.*rustc-env:[[:space:]]*//' || true) + if [ -n "$rustc_envs" ]; then + while IFS= read -r env_pair; do + flags="$flags --env-set $env_pair -Zunstable-options" + done <<< "$rustc_envs" + fi + + echo "$flags" +} + +# ------------------------- +# Arg parsing +# ------------------------- +VERBOSE=0 +SAVE_GENERATED_OUTPUT=0 +SAVE_DEBUG_OUTPUT=0 +RUST_DIR_ROOT="" + +while (( $# > 0 )); do + case "$1" in + --verbose) + VERBOSE=1 + shift + ;; + --save-generated-output) + SAVE_GENERATED_OUTPUT=1 + shift + ;; + --save-debug-output) + SAVE_DEBUG_OUTPUT=1 + shift + ;; + --help|-h) + usage + exit 0 + ;; + --*) + die "unknown option: $1" + ;; + *) + if [[ -z "$RUST_DIR_ROOT" ]]; then + RUST_DIR_ROOT=$1 + else + die "unexpected argument: $1" + fi + shift + ;; + esac +done + +[[ -n "$RUST_DIR_ROOT" ]] || { usage; exit 1; } +[[ -d "$RUST_DIR_ROOT" ]] || die "RUST_DIR_ROOT is not a directory: $RUST_DIR_ROOT" -RUST_DIR_ROOT="$1" -UI_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# ------------------------- +# Paths +# ------------------------- +UI_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" PASSING_TSV="${UI_DIR}/passing.tsv" +[[ -f "$PASSING_TSV" ]] || die "Missing TSV file: $PASSING_TSV" -KEEP_FILES=${KEEP_FILES:-""} +DEBUG_DIR="${UI_DIR}/debug" +if (( SAVE_DEBUG_OUTPUT )); then + rm -rf "$DEBUG_DIR" + mkdir -p "$DEBUG_DIR" +fi + +# ------------------------- +# Runner setup +# ------------------------- +declare -a RUN_SMIR_CMD + +if [[ -z "${RUN_SMIR:-}" ]]; then + # Build once upfront, then invoke the binary directly to avoid cargo's + # per-invocation freshness check (~2-3s overhead * thousands of tests). + log "RUN_SMIR unset; building and using binary directly" + cargo build + + SMIR_BIN="./target/debug/stable_mir_json" + [[ -x "$SMIR_BIN" ]] || die "failed to build: $SMIR_BIN" -if [ -z "${RUN_SMIR:-""}" ]; then - echo "RUN_SMIR unset, using cargo to run the tests" - RUN_SMIR="cargo run -- -Zno-codegen" + SYSROOT_LIB="$(rustc --print sysroot)/lib" + DYLD_LIBRARY_PATH="${SYSROOT_LIB}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + LD_LIBRARY_PATH="${SYSROOT_LIB}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + export DYLD_LIBRARY_PATH LD_LIBRARY_PATH + + RUN_SMIR_CMD=( "$SMIR_BIN" -Zno-codegen ) +else + # Allow RUN_SMIR to be either a path or a simple "path flags..." string. + # Note: this splits on spaces (no shell quoting). + read -r -a RUN_SMIR_CMD <<<"$RUN_SMIR" + [[ -n "${RUN_SMIR_CMD[0]:-}" ]] || die "RUN_SMIR is set but empty" + [[ -x "${RUN_SMIR_CMD[0]}" ]] || die "RUN_SMIR binary is not executable: ${RUN_SMIR_CMD[0]}" + RUN_SMIR_CMD+=( -Zno-codegen ) fi -echo "Running regression tests for passing UI cases..." +# ------------------------- +# Arch filtering +# ------------------------- +HOST_ARCH="$(uname -m)" +case "$HOST_ARCH" in + arm64|aarch64) + SKIP_ARCH_RE='/(x86_64|x86|i686|i386|s390x|powerpc|mips|riscv|loongarch|sparc|hexagon|bpf|avr|msp430|nvptx)/' + HOST_ONLY_RE='^//@ (\[.*\])?only-(aarch64|arm)' + ;; + x86_64) + SKIP_ARCH_RE='/(aarch64|arm|s390x|powerpc|mips|riscv|loongarch|sparc|hexagon|bpf|avr|msp430|nvptx)/' + HOST_ONLY_RE='^//@ (\[.*\])?only-(x86_64|x86)' + ;; + *) + SKIP_ARCH_RE='^$' # no filtering on unknown arch + HOST_ONLY_RE='' # no directive filtering either + ;; +esac + +ANY_ARCH_ONLY_RE='^//@ (\[.*\])?only-(x86_64|x86|i686|aarch64|arm|s390x|riscv|mips|powerpc|loongarch|sparc)' + +log "Running regression tests for passing UI cases (host: $HOST_ARCH)..." + +start_time=$SECONDS failed=0 passed=0 +skipped=0 total=0 -while read -r test; do - test_path="${RUST_DIR_ROOT}/${test}" - test_name="$(basename "$test" .rs)" - json_file="${PWD}/${test_name}.smir.json" +while IFS= read -r test; do + [[ -n "$test" ]] || continue + + test_path="${RUST_DIR_ROOT}/${test}" + test_name="$(basename "$test" .rs)" + json_file="${PWD}/${test_name}.smir.json" - ${RUN_SMIR} "$test_path" > /dev/null 2>&1 - status=$? + (( ++total )) - total=$((total + 1)) + # Skip tests gated on a different architecture. + # Check 1: path contains a foreign-arch directory name. + # Check 2: file contains a rustc `//@ only-` directive for a different arch. + skip_test=0 + if grep -qE "$SKIP_ARCH_RE" <<<"$test"; then + skip_test=1 + elif [[ -f "$test_path" ]] && grep -qE "$ANY_ARCH_ONLY_RE" "$test_path"; then + if [[ -n "${HOST_ONLY_RE:-}" ]] && ! grep -qE "$HOST_ONLY_RE" "$test_path"; then + skip_test=1 + fi + fi - if [ $status -ne 0 ]; then - echo "❌ FAILED: $test_path (exit $status)" - failed=$((failed + 1)) - else - if [ "$VERBOSE" -eq "1" ]; then - echo "✅ PASSING: $test_path" - fi - passed=$((passed + 1)) + if (( skip_test )); then + (( ++skipped )) + if (( VERBOSE )); then + log "⏭️ SKIPPED (arch): $test_path" fi + continue + fi + + # Extract //@ compile-flags:, //@ edition:, and //@ rustc-env: directives. + test_flags=$(extract_test_flags "$test_path") - # Clean up generated JSON - [ -z "$KEEP_FILES" ] && [ -f "$json_file" ] && rm -f "$json_file" -done < "$PASSING_TSV" + # shellcheck disable=SC2086 # intentional word-splitting of $test_flags + if (( SAVE_DEBUG_OUTPUT )); then + test_stderr=$(mktemp) + "${RUN_SMIR_CMD[@]}" ${test_flags} "$test_path" >/dev/null 2>"$test_stderr" && rc=0 || rc=$? + else + "${RUN_SMIR_CMD[@]}" ${test_flags} "$test_path" >/dev/null 2>&1 && rc=0 || rc=$? + fi -echo "—— Summary ——" -echo "Total tests : $total" -echo "Passed : $passed" -echo "Failed : $failed" + if (( rc == 0 )); then + (( ++passed )) + if (( VERBOSE )); then + log "✅ PASSING: $test_path" + fi + else + log "❌ FAILED: $test_path (exit $rc)" + if (( SAVE_DEBUG_OUTPUT )) && [[ -s "$test_stderr" ]]; then + tail -4 "$test_stderr" | sed 's/^/ /' + cp -- "$test_stderr" "${DEBUG_DIR}/${test_name}.stderr" + fi + (( ++failed )) + fi + (( SAVE_DEBUG_OUTPUT )) && rm -f -- "$test_stderr" -if [ $total -gt 0 ]; then - # Calculate ratios as decimal fractions (e.g. 0.75) - ratio_passed=$(awk "BEGIN { printf \"%.2f\", $passed/$total }") - ratio_failed=$(awk "BEGIN { printf \"%.2f\", $failed/$total }") + # Clean up generated JSON + if (( ! SAVE_GENERATED_OUTPUT )) && [[ -f "$json_file" ]]; then + rm -f -- "$json_file" + fi +done <"$PASSING_TSV" - echo - echo "Passing ratio : $ratio_passed" - echo "Failing ratio : $ratio_failed" +elapsed=$(( SECONDS - start_time )) +printf '—— Summary ——\n' +printf 'Total tests : %d\n' "$total" +printf 'Skipped : %d\n' "$skipped" +printf 'Passed : %d\n' "$passed" +printf 'Failed : %d\n' "$failed" +printf 'Elapsed : %dm%02ds\n' $(( elapsed / 60 )) $(( elapsed % 60 )) + +run=$(( total - skipped )) +if (( run > 0 )); then + ratio_passed="$(awk "BEGIN { printf \"%.2f\", $passed/$run }")" + ratio_failed="$(awk "BEGIN { printf \"%.2f\", $failed/$run }")" + printf '\nPassing ratio : %s\nFailing ratio : %s\n' "$ratio_passed" "$ratio_failed" fi -if [ $failed -gt 0 ]; then - exit 1 +if (( SAVE_DEBUG_OUTPUT && failed > 0 )); then + printf '\nDebug output saved to: %s\n' "$DEBUG_DIR" fi + +(( failed == 0 )) || exit 1