diff --git a/.github/workflows/kani.yml b/.github/workflows/kani.yml index 8673ecfa2e220..12cae5c94def2 100644 --- a/.github/workflows/kani.yml +++ b/.github/workflows/kani.yml @@ -2,6 +2,35 @@ name: Kani on: workflow_dispatch: + inputs: + proof_groups: + description: Optional JSON array of flt2dec groups (empty runs the full matrix) + type: string + default: '' + proof_platform: + description: Platform for explicitly selected manual proof groups + type: choice + options: [both, ubuntu-latest, macos-latest] + default: both + grisu_probe: + description: Optional diagnostic property for explicitly selected Grisu shortest groups + type: choice + options: [none, entry, step, integral-step] + default: none + native_solver: + description: Optional SAT solver for the native pass of explicitly selected proof groups + type: choice + options: [default, kissat] + default: default + harness_timeout: + description: Optional per-harness timeout in minutes for explicitly selected proof groups (empty keeps the catalog value) + type: string + default: '' + proof_backend: + description: Optional backend for every harness of explicitly selected proof groups + type: choice + options: [catalog, native, cvc5] + default: catalog merge_group: pull_request: branches: [ main ] @@ -10,15 +39,75 @@ on: - 'library/**' - '.github/workflows/kani.yml' - 'scripts/run-kani.sh' + - 'scripts/kani-explicit-for-counter.patch' + - 'scripts/setup-flat-smt-cbmc.sh' + - 'scripts/cbmc-skip-unused-loop-assigns.patch' + - 'scripts/kani-std-analysis/test_cbmc_loop_assigns.cpp' + - 'scripts/kani-std-analysis/test_cbmc_loop_assigns.sh' + - 'scripts/setup-cvc5.sh' + - 'scripts/kani-std-analysis/cvc5-flat-smt.sh' + - 'scripts/kani-std-analysis/test_cvc5_flat_smt.sh' + - 'scripts/kani-std-analysis/overflow-result.rs' + - 'scripts/kani-std-analysis/flt2dec_harnesses.py' + - 'scripts/kani-std-analysis/test_flt2dec_harnesses.py' + - 'scripts/kani-std-analysis/test_flt2dec_runtime.rs' + - 'scripts/kani-std-analysis/test_flt2dec_runtime.sh' + - 'scripts/kani-std-analysis/test_run_kani_list.sh' + - 'scripts/kani-std-analysis/test_cbmc_tap_retry.sh' + - 'scripts/kani-std-analysis/test_kani_runtime_reuse.sh' + - 'scripts/kani-std-analysis/test_kani_bootstrap.sh' + - 'scripts/kani-std-analysis/z3-flat-smt.sh' + - 'scripts/kani-std-analysis/test_z3_flat_smt.sh' defaults: run: shell: bash jobs: + flt2dec_runtime: + name: Test flt2dec runtime (${{ matrix.os }}) + needs: flt2dec_matrix + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix).os }} + steps: + - uses: actions/checkout@v4 + - run: rustup toolchain install nightly-2026-09-25 --profile minimal + - run: bash scripts/kani-std-analysis/test_flt2dec_runtime.sh + + build-flat-smt-cbmc: + name: Build patched CBMC (${{ matrix.os }}) + needs: flt2dec_matrix + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix).os }} + steps: + - uses: actions/checkout@v4 + with: + path: head + - name: Cache patched verifier + id: cbmc + uses: actions/cache@v4 + with: + path: head/cbmc_build/bin + key: cbmc-flat-smt-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('head/scripts/setup-flat-smt-cbmc.sh', 'head/scripts/cbmc-skip-unused-loop-assigns.patch') }} + - name: Install build dependencies + if: runner.os == 'Linux' && steps.cbmc.outputs.cache-hit != 'true' + run: sudo apt-get update && sudo apt-get install -y cmake flex bison + - name: Build pinned verifier with proof fixes + run: bash head/scripts/setup-flat-smt-cbmc.sh + - name: Check loop write-set enforcement + run: bash head/scripts/kani-std-analysis/test_cbmc_loop_assigns.sh head/cbmc_build/bin + check-kani-on-std: - name: Verify std library (partition ${{ matrix.partition }}) - runs-on: ${{ matrix.os }} + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Verify std library (partition ${{ matrix.partition }}) + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -35,10 +124,10 @@ jobs: WORKER_INDEX: ${{ matrix.partition }} # Total number of workers running this step WORKER_TOTAL: 4 - # Cap parallel harness verification on ubuntu-latest: its 4-core/16 GB - # runners get OOM-killed when 4 memory-hungry harnesses (up to ~10 GB - # each, e.g. the ffi::c_str ones) run concurrently. - KANI_JOBS: ${{ matrix.os == 'ubuntu-latest' && 2 || '' }} + # The same catalog assigns every excluded proof to a dedicated job on each OS. + KANI_SEPARATE_FLT2DEC: "true" + # Run one verifier per runner to bound proof concurrency. + KANI_JOBS: 1 steps: - name: Remove unnecessary software to free up disk space @@ -61,13 +150,324 @@ jobs: if: matrix.os == 'ubuntu-latest' run: sudo apt-get install -y jq - # Step 3: Run Kani on the std library (default configuration) + - name: Trust CBMC Homebrew tap + if: runner.os == 'macOS' + run: brew trust diffblue/cbmc + + # Step 3: Run Kani on the std library - name: Run Kani Verification - run: head/scripts/run-kani.sh --path ${{github.workspace}}/head + run: | + # Run one proof at a time on each runner. + export KANI_JOBS=1 + export RAYON_NUM_THREADS=1 + head/scripts/run-kani.sh --path ${{github.workspace}}/head \ + --kani-args --harness-timeout 30m --fail-fast + flt2dec_matrix: + name: Prepare flt2dec proof groups + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.groups.outputs.matrix }} + matrix_long: ${{ steps.groups.outputs.matrix_long }} + matrix_dragon_a: ${{ steps.groups.outputs.matrix_dragon_a }} + matrix_dragon_b: ${{ steps.groups.outputs.matrix_dragon_b }} + matrix_dragon_c: ${{ steps.groups.outputs.matrix_dragon_c }} + matrix_dragon_d: ${{ steps.groups.outputs.matrix_dragon_d }} + matrix_dragon_e: ${{ steps.groups.outputs.matrix_dragon_e }} + matrix_dragon_f: ${{ steps.groups.outputs.matrix_dragon_f }} + core_groups: ${{ steps.groups.outputs.core_groups }} + long_groups: ${{ steps.groups.outputs.long_groups }} + dragon_a_groups: ${{ steps.groups.outputs.dragon_a_groups }} + dragon_b_groups: ${{ steps.groups.outputs.dragon_b_groups }} + dragon_c_groups: ${{ steps.groups.outputs.dragon_c_groups }} + dragon_d_groups: ${{ steps.groups.outputs.dragon_d_groups }} + dragon_e_groups: ${{ steps.groups.outputs.dragon_e_groups }} + dragon_f_groups: ${{ steps.groups.outputs.dragon_f_groups }} + steps: + - uses: actions/checkout@v4 + - name: Check proof routing + run: | + bash scripts/kani-std-analysis/test_run_kani_list.sh + bash scripts/kani-std-analysis/test_cbmc_tap_retry.sh + bash scripts/kani-std-analysis/test_kani_runtime_reuse.sh + bash scripts/kani-std-analysis/test_kani_bootstrap.sh + bash scripts/kani-std-analysis/test_cvc5_flat_smt.sh + python3 -I -m unittest discover -s scripts/kani-std-analysis -p test_flt2dec_harnesses.py + - name: Build proof matrix + id: groups + env: + SELECTED_PROOF_GROUPS: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups || '' }} + SELECTED_PROOF_PLATFORM: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && inputs.proof_platform || 'both' }} + SELECTED_GRISU_PROBE: ${{ github.event_name == 'workflow_dispatch' && inputs.grisu_probe || 'none' }} + run: | + matrix_args=(--matrix --matrix-grisu-probe "$SELECTED_GRISU_PROBE") + if [[ -n "$SELECTED_PROOF_GROUPS" ]]; then + matrix_args+=(--matrix-groups "$SELECTED_PROOF_GROUPS" --matrix-platform "$SELECTED_PROOF_PLATFORM") + fi + # One matrix holds at most 256 jobs, so generator and cached proofs run in a second job + # and the Dragon fixed-K proofs run in six more (dragon-a to dragon-f). + core_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job core) + long_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job long) + dragon_a_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-a) + dragon_b_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-b) + dragon_c_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-c) + dragon_d_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-d) + dragon_e_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-e) + dragon_f_matrix=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py "${matrix_args[@]}" --matrix-job dragon-f) + core_groups=$(jq '.group | length' <<< "$core_matrix") + long_groups=$(jq '.group | length' <<< "$long_matrix") + dragon_a_groups=$(jq '.group | length' <<< "$dragon_a_matrix") + dragon_b_groups=$(jq '.group | length' <<< "$dragon_b_matrix") + dragon_c_groups=$(jq '.group | length' <<< "$dragon_c_matrix") + dragon_d_groups=$(jq '.group | length' <<< "$dragon_d_matrix") + dragon_e_groups=$(jq '.group | length' <<< "$dragon_e_matrix") + dragon_f_groups=$(jq '.group | length' <<< "$dragon_f_matrix") + printf 'Core groups: %s, long groups: %s, dragon-a groups: %s, dragon-b groups: %s, dragon-c groups: %s, dragon-d groups: %s, dragon-e groups: %s, dragon-f groups: %s\n' \ + "$core_groups" "$long_groups" "$dragon_a_groups" "$dragon_b_groups" "$dragon_c_groups" "$dragon_d_groups" "$dragon_e_groups" "$dragon_f_groups" + { + printf 'matrix=%s\n' "$core_matrix" + printf 'matrix_long=%s\n' "$long_matrix" + printf 'matrix_dragon_a=%s\n' "$dragon_a_matrix" + printf 'matrix_dragon_b=%s\n' "$dragon_b_matrix" + printf 'matrix_dragon_c=%s\n' "$dragon_c_matrix" + printf 'matrix_dragon_d=%s\n' "$dragon_d_matrix" + printf 'matrix_dragon_e=%s\n' "$dragon_e_matrix" + printf 'matrix_dragon_f=%s\n' "$dragon_f_matrix" + printf 'core_groups=%s\n' "$core_groups" + printf 'long_groups=%s\n' "$long_groups" + printf 'dragon_a_groups=%s\n' "$dragon_a_groups" + printf 'dragon_b_groups=%s\n' "$dragon_b_groups" + printf 'dragon_c_groups=%s\n' "$dragon_c_groups" + printf 'dragon_d_groups=%s\n' "$dragon_d_groups" + printf 'dragon_e_groups=%s\n' "$dragon_e_groups" + printf 'dragon_f_groups=%s\n' "$dragon_f_groups" + } >> "$GITHUB_OUTPUT" + + check-flt2dec: + name: Verify flt2dec (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.core_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix) }} + env: &flt2dec_env + KANI_JOBS: 1 + RAYON_NUM_THREADS: 1 + # Reachability verdicts do not need the large diagnostic traces that + # CBMC otherwise builds for every satisfied Kani reachability check. + KANI_PATCHED_CBMC: 'true' + KANI_FLAT_SMT_CVC5: ${{ matrix.group.backend == 'cvc5' && 'true' || 'false' }} + # Manual runs can raise the per-harness timeout and move every selected harness to one backend. + KANI_HARNESS_TIMEOUT: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && inputs.harness_timeout != '' && inputs.harness_timeout || matrix.group.timeout_minutes }}m + KANI_PROOF_BACKEND: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && inputs.proof_backend || 'catalog' }} + KANI_CACHED_MULTIPLICATION: ${{ contains(join(matrix.group.harnesses, ' '), '::cached_mul_pow5_') }} + # Record memory use where proofs stop without a verdict. + KANI_MEMORY_DIAGNOSTICS: ${{ contains(join(matrix.group.harnesses, ' '), '::cached_mul_pow5_') || startsWith(matrix.group.catalog_name || matrix.group.name, 'dragon-') }} + # Manual runs can compare SAT solvers. Other runs keep the default solver. + KANI_NATIVE_SOLVER: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && inputs.native_solver || 'default' }} + KANI_MANUAL_PROBE: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && matrix.group.probe_property != '' }} + KANI_PROBE_PROPERTY: ${{ matrix.group.probe_property || '' }} + KANI_OBJECT_BITS: ${{ contains(join(matrix.group.harnesses, ' '), '::cached_mul_pow5_') && '16' || ((matrix.group.catalog_name || matrix.group.name) == 'dragon-exact-loop-frame' || startsWith(matrix.group.catalog_name || matrix.group.name, 'grisu-shortest-')) && '10' || (matrix.group.kind == 'contract' || matrix.group.kind == 'equivalence') && '12' || '14' }} + KANI_SYMEX_CACHE_DEREFERENCES: ${{ (matrix.group.backend == 'cvc5' || matrix.group.kind == 'probe' || startsWith(matrix.group.catalog_name || matrix.group.name, 'bigint-shift-equivalence-') || startsWith(matrix.group.catalog_name || matrix.group.name, 'cached-multiplication-equivalence-') || startsWith(matrix.group.catalog_name || matrix.group.name, 'grisu-')) && 'true' || 'false' }} + # Dragon's fixed limb indices can use the default array field splitting. + KANI_ARRAY_FIELD_SENSITIVITY: 'true' + HARNESS_GROUP: ${{ matrix.group.catalog_name || matrix.group.name }} + HARNESS_KIND: ${{ matrix.group.kind }} + steps: &flt2dec_steps + - name: Remove unnecessary software to free up disk space + if: matrix.os == 'ubuntu-latest' + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /usr/local/.ghcup + + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: head + submodules: true + + - name: Trust CBMC Homebrew tap + if: runner.os == 'macOS' + run: brew trust diffblue/cbmc + + - name: Restore patched verifier + uses: actions/cache/restore@v4 + with: + path: head/cbmc_build/bin + key: cbmc-flat-smt-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('head/scripts/setup-flat-smt-cbmc.sh', 'head/scripts/cbmc-skip-unused-loop-assigns.patch') }} + fail-on-cache-miss: true + + - name: Add memory for cached multiplication and Dragon proofs + if: runner.os == 'Linux' && env.KANI_MEMORY_DIAGNOSTICS == 'true' + run: | + sudo fallocate -l 24G /mnt/pr601-proof-swap + sudo chmod 600 /mnt/pr601-proof-swap + sudo mkswap /mnt/pr601-proof-swap + sudo swapon /mnt/pr601-proof-swap + free -h + df -h /mnt + + - name: Verify selected proof groups + env: + RETAIN_PROOF_MODELS: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' }} + run: | + if [[ "$KANI_MEMORY_DIAGNOSTICS" == true ]]; then + if [[ "$RUNNER_OS" == Linux ]]; then + ulimit -v 33554432 + fi + ( + while sleep 30; do + date -u +%FT%TZ + ps -A -o pid=,rss=,comm= | awk '$3 ~ /(^|\/)(cbmc|cvc5|kissat|goto-instrument|goto-cc|kani-driver|kani-compiler|rustc)$/ { print "verifier RSS KiB:", $0 }' + done + ) & + proof_sampler_pid=$! + trap 'kill "$proof_sampler_pid" 2>/dev/null || true' EXIT + fi + if [[ ! "$KANI_HARNESS_TIMEOUT" =~ ^[1-9][0-9]*m$ ]]; then + echo "Expected a positive whole number of minutes, got $KANI_HARNESS_TIMEOUT" >&2 + exit 1 + fi + # Keep every group within four hours of proof budgets per job: + # two 120-minute or four 60-minute generators, or eight 30-minute cases. + output_format=terse + # Dragon generators and manual probes show their CBMC phases until they converge. + if [[ "$HARNESS_KIND" == contract || "$HARNESS_KIND" == equivalence || "$HARNESS_KIND" == probe || "$HARNESS_GROUP" == dragon-* || "$KANI_MANUAL_PROBE" == true ]]; then + output_format=regular + fi + temporary_args=() + if [[ "$RETAIN_PROOF_MODELS" == true ]]; then + temporary_args=(--keep-temps) + fi + for proof_backend in native cvc5; do + backend_args=(--backend "$proof_backend") + if [[ "$KANI_PROOF_BACKEND" != catalog ]]; then + [[ "$KANI_PROOF_BACKEND" == "$proof_backend" ]] || continue + backend_args=() + fi + harness_names=$(python3 -I head/scripts/kani-std-analysis/flt2dec_harnesses.py \ + --group "$HARNESS_GROUP" "${backend_args[@]}") + [[ -n "$harness_names" ]] || continue + harness_args=() + while IFS= read -r harness; do + [[ -n "$harness" ]] || exit 1 + harness_args+=(--harness "$harness") + done <<< "$harness_names" + printf 'Selected %s harnesses for %s\n' "$(( ${#harness_args[@]} / 2 ))" "$proof_backend" + export KANI_FLAT_SMT_CVC5=false + solver_args=() + if [[ "$KANI_NATIVE_SOLVER" != default ]]; then + solver_args=(--solver "$KANI_NATIVE_SOLVER") + fi + if [[ "$proof_backend" == cvc5 ]]; then + export KANI_FLAT_SMT_CVC5=true + solver_args=(--solver minisat) + fi + head/scripts/run-kani.sh --path "${GITHUB_WORKSPACE}/head" \ + --kani-args --jobs 1 --harness-timeout "$KANI_HARNESS_TIMEOUT" --fail-fast --output-format="$output_format" \ + "${solver_args[@]}" "${temporary_args[@]}" --exact "${harness_args[@]}" + done + - name: Retain selected proof models + if: always() && github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' + uses: actions/upload-artifact@v4 + with: + name: flt2dec-models-${{ matrix.group.name }}-${{ runner.os }}-${{ runner.arch }} + retention-days: 7 + if-no-files-found: ignore + path: | + **/kani_verify_std/**/out/*grisu*.out + **/kani_verify_std/**/out/*dragon*.out + !**/*.symtab.out + + # One matrix holds at most 256 jobs, so generator and cached proofs run in this second job. + check-flt2dec-long: + name: Verify flt2dec long (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.long_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_long) }} + # The aliases reuse the environment and steps of check-flt2dec. + env: *flt2dec_env + steps: *flt2dec_steps + + # The Dragon fixed-K proofs of windows 000..033 run in a third matrix job. + check-flt2dec-dragon-a: + name: Verify flt2dec dragon-a (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_a_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_a) }} + env: *flt2dec_env + steps: *flt2dec_steps + + # The Dragon fixed-K proofs of windows 034..067 run in a fourth matrix job. + check-flt2dec-dragon-b: + name: Verify flt2dec dragon-b (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_b_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_b) }} + env: *flt2dec_env + steps: *flt2dec_steps + + # The Dragon fixed-K proofs of windows 068..097 run in a fifth matrix job. + check-flt2dec-dragon-c: + name: Verify flt2dec dragon-c (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_c_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_c) }} + env: *flt2dec_env + steps: *flt2dec_steps + + # The Dragon fixed-K proofs of windows 098..127 run in a sixth matrix job. + check-flt2dec-dragon-d: + name: Verify flt2dec dragon-d (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_d_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_d) }} + env: *flt2dec_env + steps: *flt2dec_steps + + # The pieces of the split pairs at offsets 0..7 run in a seventh matrix job. + check-flt2dec-dragon-e: + name: Verify flt2dec dragon-e (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_e_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_e) }} + env: *flt2dec_env + steps: *flt2dec_steps + + # The pieces at offsets 8..15 run in an eighth matrix job. + check-flt2dec-dragon-f: + name: Verify flt2dec dragon-f (${{ matrix.group.name }}, ${{ matrix.os }}) + needs: [flt2dec_matrix, build-flat-smt-cbmc] + if: needs.flt2dec_matrix.outputs.dragon_f_groups != '0' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.flt2dec_matrix.outputs.matrix_dragon_f) }} + env: *flt2dec_env + steps: *flt2dec_steps + kani_autoharness: - name: Verify std library using autoharness - runs-on: ${{ matrix.os }} + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Verify std library using autoharness + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -93,6 +493,10 @@ jobs: with: submodules: true + - name: Trust CBMC Homebrew tap + if: runner.os == 'macOS' + run: brew trust diffblue/cbmc + # Step 2: Run Kani autoharness on the std library for selected functions. # Uses "--include-pattern" to make sure we do not try to run across all # possible functions as that may take a lot longer than expected. Instead, @@ -108,7 +512,24 @@ jobs: # timeout on macOS and the ubuntu runner died while verifying them. - name: Run Kani Verification run: | + python3 -I -B -m unittest discover \ + -s scripts/kani-std-analysis -p test_log_parser.py + scripts/run-kani.sh --run list --kani-args --format json + remaining_manual_harnesses=$(python3 -I scripts/kani-std-analysis/flt2dec_harnesses.py \ + --remaining kani-list.json) + manual_harness_args=() + while IFS= read -r harness; do + [[ -n "$harness" ]] || continue + manual_harness_args+=(--harness "$harness") + done <<< "$remaining_manual_harnesses" + if (( ${#manual_harness_args[@]} == 0 )); then + echo "No remaining manual harnesses were found" >&2 + exit 1 + fi + # Manual filters leave Kani's automatic harness selection intact. + # Dedicated flt2dec proofs run in the check-flt2dec and check-flt2dec-long matrices. scripts/run-kani.sh --run autoharness --kani-args \ + --exact "${manual_harness_args[@]}" \ --include-pattern "<(.+)[[:space:]]as[[:space:]](.+)>::disjoint_bitor" \ --include-pattern "<(.+)[[:space:]]as[[:space:]](.+)>::unchecked_disjoint_bitor" \ --include-pattern "<(.+)[[:space:]]as[[:space:]]iter::range::Step>::backward_unchecked" \ @@ -213,9 +634,9 @@ jobs: --include-pattern time::Duration::from_secs \ --exclude-pattern time::Duration::from_secs_f \ --exclude-pattern ::precondition_check \ - --harness-timeout 10m \ + --harness-timeout 30m --fail-fast \ --default-unwind 1000 \ - --jobs=3 --output-format=terse | tee autoharness-verification.log + --jobs=1 --output-format=terse | tee autoharness-verification.log gzip autoharness-verification.log - name: Upload Autoharness Verification Log @@ -228,8 +649,9 @@ jobs: retention-days: 3 run_kani_metrics: - name: Kani Metrics - runs-on: ${{ matrix.os }} + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Kani Metrics + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -257,7 +679,7 @@ jobs: # The Kani metrics collection uses a Python script (kani_std_analysis.py), so make sure Python is installed - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' @@ -298,9 +720,10 @@ jobs: retention-days: 3 run-log-analysis: - name: Build JSON from logs + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Build JSON from logs + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' needs: [run_kani_metrics, kani_autoharness] - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.os == 'ubuntu-latest' && 'ubuntu-24.04' || matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -350,8 +773,9 @@ jobs: if-no-files-found: error run-kani-list: - name: Kani List - runs-on: ubuntu-latest + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Kani List + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' + runs-on: ubuntu-24.04 steps: - name: Remove unnecessary software to free up disk space run: | @@ -384,7 +808,7 @@ jobs: # Step 3: Add output to job summary - name: Add Kani List output to job summary - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const fs = require('fs'); @@ -395,8 +819,9 @@ jobs: .write(); run-autoharness-analyzer: - name: Kani Autoharness Analyzer - runs-on: ubuntu-latest + name: ${{ github.event_name == 'workflow_dispatch' && inputs.proof_groups != '' && 'Manual ' || '' }}Kani Autoharness Analyzer + if: github.event_name != 'workflow_dispatch' || inputs.proof_groups == '' + runs-on: ubuntu-24.04 steps: # Step 1: Check out the repository - name: Checkout Repository @@ -412,13 +837,15 @@ jobs: - name: Add Autoharness Analyzer output to job summary run: | pushd scripts/autoharness_analyzer - echo "# Autoharness Failure Summary" >> "$GITHUB_STEP_SUMMARY" - echo "## Crate core, all functions" >> "$GITHUB_STEP_SUMMARY" - cat core_autoharness_data.md >> "$GITHUB_STEP_SUMMARY" - echo "## Crate core, unsafe functions" >> "$GITHUB_STEP_SUMMARY" - cat core_autoharness_data.md >> "$GITHUB_STEP_SUMMARY" - echo "## Crate std, all functions" >> "$GITHUB_STEP_SUMMARY" - cat std_autoharness_data.md >> "$GITHUB_STEP_SUMMARY" - echo "## Crate std, unsafe functions" >> "$GITHUB_STEP_SUMMARY" - cat std_unsafe_autoharness_data.md >> "$GITHUB_STEP_SUMMARY" + { + echo "# Autoharness Failure Summary" + echo "## Crate core, all functions" + cat core_autoharness_data.md + echo "## Crate core, unsafe functions" + cat core_autoharness_data.md + echo "## Crate std, all functions" + cat std_autoharness_data.md + echo "## Crate std, unsafe functions" + cat std_unsafe_autoharness_data.md + } >> "$GITHUB_STEP_SUMMARY" popd diff --git a/.gitignore b/.gitignore index a98335bf7c3e4..fe2dc40198de1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ Session.vim /book/ /build/ /kani_build/ +/cbmc_build/ /target library/target scripts/autoharness_analyzer/target/ diff --git a/library/core/src/lib.rs b/library/core/src/lib.rs index 350a348cc0c80..22c5389b9e656 100644 --- a/library/core/src/lib.rs +++ b/library/core/src/lib.rs @@ -64,6 +64,8 @@ #![no_core] #![rustc_coherence_is_core] #![rustc_preserve_ub_checks] +// Composed Kani stubs require more macro expansion depth. +#![cfg_attr(kani, recursion_limit = "512")] // // Lints: #![deny(rust_2021_incompatible_or_patterns)] diff --git a/library/core/src/num/imp/bignum.rs b/library/core/src/num/imp/bignum.rs index 5c2a19f39dfce..cfb31df3c62c3 100644 --- a/library/core/src/num/imp/bignum.rs +++ b/library/core/src/num/imp/bignum.rs @@ -147,6 +147,10 @@ macro_rules! define_bignum { use crate::{cmp, iter}; let mut sz = cmp::max(self.size, other.size); + #[cfg(kani)] + { + sz = crate::num::imp::bignum::kani_loop_size(sz); + } let mut carry = false; for (a, b) in iter::zip(&mut self.base[..sz], &other.base[..sz]) { let (v, c) = (*a).carrying_add(*b, carry); @@ -182,6 +186,8 @@ macro_rules! define_bignum { use crate::{cmp, iter}; let sz = cmp::max(self.size, other.size); + #[cfg(kani)] + let sz = crate::num::imp::bignum::kani_loop_size(sz); let mut noborrow = true; for (a, b) in iter::zip(&mut self.base[..sz], &other.base[..sz]) { let (v, c) = (*a).carrying_add(!*b, noborrow); @@ -197,6 +203,10 @@ macro_rules! define_bignum { /// mutable reference. pub fn mul_small(&mut self, other: $ty) -> &mut $name { let mut sz = self.size; + #[cfg(kani)] + { + sz = crate::num::imp::bignum::kani_loop_size(sz); + } let mut carry = 0; for a in &mut self.base[..sz] { let (v, c) = (*a).carrying_mul(other, carry); @@ -213,6 +223,12 @@ macro_rules! define_bignum { /// Multiplies itself by `2^bits` and returns its own mutable reference. pub fn mul_pow2(&mut self, bits: usize) -> &mut $name { + #[cfg(kani)] + let bits = crate::num::imp::bignum::kani_pow2_bits(bits); + #[cfg(kani)] + let size = crate::num::imp::bignum::kani_loop_size(self.size); + #[cfg(not(kani))] + let size = self.size; let digitbits = <$ty>::BITS as usize; let digits = bits / digitbits; let bits = bits % digitbits; @@ -222,7 +238,7 @@ macro_rules! define_bignum { debug_assert!(bits == 0 || (self.base[$n - digits - 1] >> (digitbits - bits)) == 0); // shift by `digits * digitbits` bits - for i in (0..self.size).rev() { + for i in (0..size).rev() { self.base[i + digits] = self.base[i]; } for i in 0..digits { @@ -230,7 +246,7 @@ macro_rules! define_bignum { } // shift by `bits` bits - let mut sz = self.size + digits; + let mut sz = size + digits; if bits > 0 { let last = sz; let overflow = self.base[last - 1] >> (digitbits - bits); @@ -276,14 +292,30 @@ macro_rules! define_bignum { self } + /// One step of `mul_digits`: returns `(carry', v)` such that + /// `carry' * 2^W + v = a * b + c + carry`, where `a` is digit `i` of the + /// first factor and `b` is digit `j` of the second factor. The positions do + /// not change the result. The Kani proofs of the cached products use them. + #[inline] + pub(crate) fn mul_digits_step( + _i: usize, + _j: usize, + a: $ty, + b: $ty, + c: $ty, + carry: $ty, + ) -> ($ty, $ty) { + use crate::num::imp::bignum::FullOps; + + a.full_mul_add(b, c, carry) + } + /// Multiplies itself by a number described by `other[0] + other[1] * 2^W + /// other[2] * 2^(2W) + ...` (where `W` is the number of bits in the digit type) /// and returns its own mutable reference. pub fn mul_digits<'a>(&'a mut self, other: &[$ty]) -> &'a mut $name { // the internal routine. works best when aa.len() <= bb.len(). fn mul_inner(ret: &mut [$ty; $n], aa: &[$ty], bb: &[$ty]) -> usize { - use crate::num::imp::bignum::FullOps; - let mut retsz = 0; for (i, &a) in aa.iter().enumerate() { if a == 0 { @@ -292,7 +324,7 @@ macro_rules! define_bignum { let mut sz = bb.len(); let mut carry = 0; for (j, &b) in bb.iter().enumerate() { - let (c, v) = a.full_mul_add(b, ret[i + j], carry); + let (c, v) = $name::mul_digits_step(i, j, a, b, ret[i + j], carry); ret[i + j] = v; carry = c; } @@ -405,6 +437,541 @@ pub type Digit = Digit32; #[cfg(target_pointer_width = "64")] pub type Digit = Digit64; +#[cfg(kani)] +pub(crate) const KANI_LIMBS: usize = 1280 / Digit::BITS as usize; + +#[cfg(kani)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum KaniInvariantOrdering { + Less, + Equal, + Greater, + Unrepresentable, +} + +#[cfg(all(kani, target_pointer_width = "64"))] +macro_rules! kani_for_each_limb { + ($model:ident) => { + $model!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) + }; +} + +#[cfg(all(kani, any(target_pointer_width = "16", target_pointer_width = "32")))] +macro_rules! kani_for_each_limb { + ($model:ident) => { + $model!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 + ) + }; +} + +// Proofs can expose fixed count bits while asserting equality with this identity. +// The arithmetic loops still run, and normal builds contain no instrumentation. +#[cfg(kani)] +pub(crate) fn kani_loop_size(size: usize) -> usize { + size +} + +#[cfg(kani)] +pub(crate) fn kani_pow2_bits(bits: usize) -> usize { + bits +} + +// Proofs can bound the limb count of the bigint operand of a cached product. +// A replacement asserts the actual size against its constant bound. +#[cfg(kani)] +pub(crate) fn kani_mul_limb_bound(_size: usize, _other_len: usize) -> usize { + KANI_LIMBS +} + +#[cfg(kani)] +impl crate::kani::Arbitrary for Big { + fn any() -> Self { + Self { size: crate::kani::any(), base: crate::kani::any() } + } +} + +#[cfg(kani)] +impl Big { + pub(crate) fn kani_with_arbitrary_limbs(size: usize) -> Self { + Self { size, base: crate::kani::any() } + } + + pub(crate) fn kani_valid_with_size(size: usize) -> Self { + assert!(size <= KANI_LIMBS); + let mut value = Self { size, base: [0; KANI_LIMBS] }; + macro_rules! initialize_limbs { + ($($index:literal),+ $(,)?) => {{ + $( + value.base[$index] = if $index < size { crate::kani::any() } else { 0 }; + )+ + }}; + } + kani_for_each_limb!(initialize_limbs); + value + } + + pub(crate) fn kani_size(&self) -> usize { + self.size + } + + // Keep these helpers exclusive to loop invariants. Shared callees can + // receive a mismatched DFCC parameter: model-checking/kani#4796. + pub(crate) fn kani_invariant_size(&self) -> usize { + self.size + } + + pub(crate) fn kani_invariant_valid_storage(&self) -> bool { + macro_rules! inactive_limbs_are_zero { + ($($index:literal),+ $(,)?) => { + true $(& ((self.size > $index) | (self.base[$index] == 0)))+ + }; + } + self.size <= KANI_LIMBS && kani_for_each_limb!(inactive_limbs_are_zero) + } + + pub(crate) fn kani_invariant_less_than(&self, other: &Self) -> bool { + macro_rules! compare_limbs { + ($($index:literal),+ $(,)?) => {{ + let less = false; + $( + let active = (self.size > $index) | (other.size > $index); + let equal = self.base[$index] == other.base[$index]; + let less = (active & (self.base[$index] < other.base[$index])) + | ((!active | equal) & less); + )+ + less + }}; + } + self.size <= KANI_LIMBS && other.size <= KANI_LIMBS && kani_for_each_limb!(compare_limbs) + } + + // Invariant-only arithmetic avoids shared callees and their DFCC parameters. + pub(crate) fn kani_invariant_cmp_scaled_sum( + &self, + left: &Self, + right: &Self, + decimal_digits: usize, + ) -> KaniInvariantOrdering { + if self.size <= KANI_LIMBS + && left.size <= KANI_LIMBS + && right.size <= KANI_LIMBS + && decimal_digits <= 16 + { + let factor: u128 = match decimal_digits { + 0 => 1, + 1 => 10, + 2 => 100, + 3 => 1_000, + 4 => 10_000, + 5 => 100_000, + 6 => 1_000_000, + 7 => 10_000_000, + 8 => 100_000_000, + 9 => 1_000_000_000, + 10 => 10_000_000_000, + 11 => 100_000_000_000, + 12 => 1_000_000_000_000, + 13 => 10_000_000_000_000, + 14 => 100_000_000_000_000, + 15 => 1_000_000_000_000_000, + 16 => 10_000_000_000_000_000, + _ => 0, + }; + macro_rules! compare_scaled_sum { + ($($index:literal),+ $(,)?) => {{ + let carry = 0u128; + let ordering = KaniInvariantOrdering::Equal; + $( + let a = if left.size > $index { left.base[$index] as u128 } else { 0 }; + let b = if right.size > $index { right.base[$index] as u128 } else { 0 }; + let product = (a + b) * factor + carry; + let digit = product as Digit; + let own = if self.size > $index { self.base[$index] } else { 0 }; + let ordering = if own < digit { + KaniInvariantOrdering::Less + } else if own > digit { + KaniInvariantOrdering::Greater + } else { + ordering + }; + let carry = product >> Digit::BITS; + )+ + if carry == 0 { ordering } else { KaniInvariantOrdering::Unrepresentable } + }}; + } + kani_for_each_limb!(compare_scaled_sum) + } else { + KaniInvariantOrdering::Unrepresentable + } + } + + // Contract-only twins of the invariant helpers above. A postcondition that + // shares the loop-invariant helpers can receive a mismatched DFCC parameter: + // model-checking/kani#4796. Each twin checks the sizes before any limb access. + pub(crate) fn kani_contract_zero() -> Self { + Self { size: 0, base: [0; KANI_LIMBS] } + } + + pub(crate) fn kani_contract_size(&self) -> usize { + self.size + } + + pub(crate) fn kani_contract_valid_storage(&self) -> bool { + macro_rules! inactive_limbs_are_zero { + ($($index:literal),+ $(,)?) => { + true $(& ((self.size > $index) | (self.base[$index] == 0)))+ + }; + } + self.size <= KANI_LIMBS && kani_for_each_limb!(inactive_limbs_are_zero) + } + + pub(crate) fn kani_contract_cmp_scaled_sum( + &self, + left: &Self, + right: &Self, + decimal_digits: usize, + ) -> KaniInvariantOrdering { + if self.size <= KANI_LIMBS + && left.size <= KANI_LIMBS + && right.size <= KANI_LIMBS + && decimal_digits <= 16 + { + let factor: u128 = match decimal_digits { + 0 => 1, + 1 => 10, + 2 => 100, + 3 => 1_000, + 4 => 10_000, + 5 => 100_000, + 6 => 1_000_000, + 7 => 10_000_000, + 8 => 100_000_000, + 9 => 1_000_000_000, + 10 => 10_000_000_000, + 11 => 100_000_000_000, + 12 => 1_000_000_000_000, + 13 => 10_000_000_000_000, + 14 => 100_000_000_000_000, + 15 => 1_000_000_000_000_000, + 16 => 10_000_000_000_000_000, + _ => 0, + }; + macro_rules! compare_scaled_sum { + ($($index:literal),+ $(,)?) => {{ + let carry = 0u128; + let ordering = KaniInvariantOrdering::Equal; + $( + let a = if left.size > $index { left.base[$index] as u128 } else { 0 }; + let b = if right.size > $index { right.base[$index] as u128 } else { 0 }; + let product = (a + b) * factor + carry; + let digit = product as Digit; + let own = if self.size > $index { self.base[$index] } else { 0 }; + let ordering = if own < digit { + KaniInvariantOrdering::Less + } else if own > digit { + KaniInvariantOrdering::Greater + } else { + ordering + }; + let carry = product >> Digit::BITS; + )+ + if carry == 0 { ordering } else { KaniInvariantOrdering::Unrepresentable } + }}; + } + kani_for_each_limb!(compare_scaled_sum) + } else { + KaniInvariantOrdering::Unrepresentable + } + } + + pub(crate) fn kani_limbs_mut(&mut self) -> &mut [Digit; KANI_LIMBS] { + &mut self.base + } + + pub(crate) fn kani_valid_storage(&self) -> bool { + let size = kani_loop_size(self.size); + // Constant indices avoid unfolding a symbolic slice iterator in every + // contract invocation. Eager Boolean operations preserve the predicate. + macro_rules! inactive_limbs_are_zero { + ($($index:literal),+ $(,)?) => { + true $(& ((size > $index) | (self.base[$index] == 0)))+ + }; + } + + size <= self.base.len() && kani_for_each_limb!(inactive_limbs_are_zero) + } + + // The equivalence proof in dragon_verify checks these constant-index + // models for every in-bounds storage size and arbitrary limb contents. + pub(crate) fn kani_cmp_model(&self, other: &Self) -> crate::cmp::Ordering { + use crate::cmp::Ordering::{Equal, Greater, Less}; + + assert!(self.size <= self.base.len() && other.size <= other.base.len()); + let size = kani_loop_size(self.size); + let other_size = kani_loop_size(other.size); + + macro_rules! compare_limbs { + ($($index:literal),+ $(,)?) => {{ + let less = false; + let greater = false; + $( + let active = (size > $index) | (other_size > $index); + let equal = self.base[$index] == other.base[$index]; + let preserve = !active | equal; + let less = (active & (self.base[$index] < other.base[$index])) + | (preserve & less); + let greater = (active & (self.base[$index] > other.base[$index])) + | (preserve & greater); + )+ + if less { Less } else if greater { Greater } else { Equal } + }}; + } + + // Like Ord::cmp, inspect both arrays through the larger active size. + // A differing higher limb supersedes every lower limb's ordering. + // Eager Boolean operations avoid a branch at each constant limb index. + kani_for_each_limb!(compare_limbs) + } + + pub(crate) fn kani_is_zero_model(&self) -> bool { + assert!(self.size <= self.base.len()); + let size = kani_loop_size(self.size); + + macro_rules! limbs_are_zero { + ($($index:literal),+ $(,)?) => { + true $(& ((size <= $index) | (self.base[$index] == 0)))+ + }; + } + + kani_for_each_limb!(limbs_are_zero) + } + + pub(crate) fn kani_same_storage(&self, other: &Self) -> bool { + macro_rules! equal_limbs { + ($($index:literal),+ $(,)?) => { + true $(& (self.base[$index] == other.base[$index]))+ + }; + } + + (self.size == other.size) & kani_for_each_limb!(equal_limbs) + } + + // Independent equivalence proofs compare every stored limb and the size. + // Addition and multiplication reserve one limb for a possible carry. + // Their size bounds are asserted here, including at generator call sites. + pub(crate) fn kani_add_model<'a>(&'a mut self, other: &Self) -> &'a mut Self { + assert!(self.size < self.base.len() && other.size < other.base.len()); + let mut sz = kani_loop_size(crate::cmp::max(self.size, other.size)); + + macro_rules! add_limbs { + ($($index:literal),+ $(,)?) => {{ + let carry = false; + $( + let carry = if $index < sz { + let (value, next) = self.base[$index].carrying_add(other.base[$index], carry); + self.base[$index] = value; + next + } else { + carry + }; + )+ + carry + }}; + } + + let carry = kani_for_each_limb!(add_limbs); + if carry { + self.base[sz] = 1; + sz += 1; + } + self.size = sz; + self + } + + pub(crate) fn kani_sub_model<'a>(&'a mut self, other: &Self) -> &'a mut Self { + assert!(self.size <= self.base.len() && other.size <= other.base.len()); + assert!(self.kani_cmp_model(other) != crate::cmp::Ordering::Less); + let sz = kani_loop_size(crate::cmp::max(self.size, other.size)); + + macro_rules! subtract_limbs { + ($($index:literal),+ $(,)?) => {{ + let noborrow = true; + $( + let noborrow = if $index < sz { + let (value, next) = self.base[$index].carrying_add(!other.base[$index], noborrow); + self.base[$index] = value; + next + } else { + noborrow + }; + )+ + noborrow + }}; + } + + let noborrow = kani_for_each_limb!(subtract_limbs); + assert!(noborrow); + self.size = sz; + self + } + + pub(crate) fn kani_mul_digits_model<'a>(&'a mut self, other: &[Digit]) -> &'a mut Self { + let size = kani_loop_size(self.size); + assert!(size <= KANI_LIMBS && other.len() <= KANI_LIMBS); + assert!(size + other.len() <= KANI_LIMBS); + let bound = kani_mul_limb_bound(size, other.len()); + let mut ret = [0; KANI_LIMBS]; + let retsz = if bound < other.len() || size < other.len() { + Self::kani_mul_digits_product(&mut ret, &self.base[..size], other, bound, KANI_LIMBS) + } else { + Self::kani_mul_digits_product(&mut ret, other, &self.base[..size], KANI_LIMBS, bound) + }; + self.base = ret; + self.size = retsz; + self + } + + fn kani_mul_digits_product( + ret: &mut [Digit; KANI_LIMBS], + aa: &[Digit], + bb: &[Digit], + aa_bound: usize, + bb_bound: usize, + ) -> usize { + // The caller gives the shorter operand as `aa`, so the bound `bb.len()` removes no + // row, but a constant `bb` (a power table) bounds the rows before the solver starts. + // The caller checks that each operand length is at most its limb bound, so the + // limb bounds remove no row or column either. + assert!(aa.len() <= bb.len()); + let mut retsz = 0; + macro_rules! multiply_rows { + ($($index:literal),+ $(,)?) => {$( + if $index < aa_bound && $index < aa.len() && $index < bb.len() { + let a = aa[$index]; + if a != 0 { + let sz = Self::kani_mul_digits_row(ret, a, bb, $index, bb_bound); + if retsz < $index + sz { + retsz = $index + sz; + } + } + } + )+}; + } + kani_for_each_limb!(multiply_rows); + retsz + } + + fn kani_mul_digits_row( + ret: &mut [Digit; KANI_LIMBS], + a: Digit, + bb: &[Digit], + index: usize, + bb_bound: usize, + ) -> usize { + let mut carry = 0; + macro_rules! multiply_columns { + ($($column:literal),+ $(,)?) => {$( + if $column < bb_bound && $column < bb.len() { + let output = index + $column; + assert!(output < KANI_LIMBS); + let (c, v) = + Self::mul_digits_step(index, $column, a, bb[$column], ret[output], carry); + ret[output] = v; + carry = c; + } + )+}; + } + kani_for_each_limb!(multiply_columns); + if carry > 0 { + ret[index + bb.len()] = carry; + bb.len() + 1 + } else { + bb.len() + } + } + + pub(crate) fn kani_mul_small_model(&mut self, other: Digit) -> &mut Self { + assert!(self.size < self.base.len()); + let mut sz = kani_loop_size(self.size); + + macro_rules! multiply_limbs { + ($($index:literal),+ $(,)?) => {{ + let carry = 0; + $( + let carry = if $index < sz { + let (value, next) = self.base[$index].carrying_mul(other, carry); + self.base[$index] = value; + next + } else { + carry + }; + )+ + carry + }}; + } + + let carry = kani_for_each_limb!(multiply_limbs); + if carry > 0 { + self.base[sz] = carry; + sz += 1; + } + self.size = sz; + self + } + + // Contract proofs include every storage size, including the empty zero + // representation. Leading zero limbs within the active prefix are valid. + pub(crate) fn kani_mul_pow2_model(&mut self, bits: usize) -> &mut Self { + assert!(self.kani_valid_storage()); + let size = kani_loop_size(self.size); + let bits = kani_pow2_bits(bits); + let digit_bits = Digit::BITS as usize; + let digits = bits / digit_bits; + let bits = bits % digit_bits; + assert!(digits < KANI_LIMBS); + assert!(size + digits <= KANI_LIMBS); + assert!(bits == 0 || size + digits > 0); + assert!(bits == 0 || (self.base[KANI_LIMBS - digits - 1] >> (digit_bits - bits)) == 0); + + let old = self.base; + let overflow = if bits > 0 && size > 0 { old[size - 1] >> (digit_bits - bits) } else { 0 }; + let new_size = size + digits + usize::from(overflow > 0); + assert!(new_size <= KANI_LIMBS); + + macro_rules! shift_limbs { + ($($index:literal),+ $(,)?) => {{ + $( + self.base[$index] = if $index < digits { + 0 + } else if bits == 0 { + old[$index - digits] + } else { + let high = old[$index - digits] << bits; + let low = if $index > digits { + old[$index - digits - 1] >> (digit_bits - bits) + } else { + 0 + }; + high | low + }; + )+ + }}; + } + + kani_for_each_limb!(shift_limbs); + self.size = new_size; + self + } + + pub(crate) fn kani_any_valid() -> Self { + let value: Self = crate::kani::any(); + crate::kani::assume(value.kani_valid_storage()); + value + } +} + // this one is used for testing only. #[doc(hidden)] pub mod tests { diff --git a/library/core/src/num/imp/flt2dec/bit_scan_verify.rs b/library/core/src/num/imp/flt2dec/bit_scan_verify.rs new file mode 100644 index 0000000000000..8a30043e6afe7 --- /dev/null +++ b/library/core/src/num/imp/flt2dec/bit_scan_verify.rs @@ -0,0 +1,46 @@ +//! Exact bit-scan models for the flt2dec generator proofs. + +use crate::kani; + +// Expose fixed shifts to symbolic execution before expanding normalization and +// scaling. The independent proof below checks every input, including zero. +pub(crate) fn leading_zeros_u64(mut value: u64) -> u32 { + if value == 0 { + 64 + } else { + let mut count = 0; + macro_rules! scan_half { + ($bits:literal) => { + if value >> (64 - $bits) == 0 { + count += $bits; + value <<= $bits; + } + }; + } + scan_half!(32); + scan_half!(16); + scan_half!(8); + scan_half!(4); + scan_half!(2); + count + u32::from(value >> 63 == 0) + } +} + +pub(crate) fn leading_zeros_u32(value: u32) -> u32 { + leading_zeros_u64(u64::from(value)) - 32 +} + +#[kani::proof] +#[kani::solver(kissat)] +fn check_leading_zeros_models_agree() { + let wide: u64 = kani::any(); + let narrow: u32 = kani::any(); + assert!(leading_zeros_u64(wide) == wide.leading_zeros()); + assert!(leading_zeros_u32(narrow) == narrow.leading_zeros()); + kani::cover(wide == 0, "u64 zero has 64 leading zeros"); + kani::cover(wide == 1, "u64 one has 63 leading zeros"); + kani::cover(wide == 1 << 63, "u64 top bit has no leading zeros"); + kani::cover(narrow == 0, "u32 zero has 32 leading zeros"); + kani::cover(narrow == 1, "u32 one has 31 leading zeros"); + kani::cover(narrow == 1 << 31, "u32 top bit has no leading zeros"); +} diff --git a/library/core/src/num/imp/flt2dec/estimator_verify.rs b/library/core/src/num/imp/flt2dec/estimator_verify.rs new file mode 100644 index 0000000000000..e3d75c2f8d95a --- /dev/null +++ b/library/core/src/num/imp/flt2dec/estimator_verify.rs @@ -0,0 +1,143 @@ +//! An exact exponent-estimator model with a separate equivalence proof. + +use crate::kani; + +fn scale_from_bits(bits: i64, exp: i16) -> i16 { + (((bits + exp as i64) * 1292913986) >> 32) as i16 +} + +// Compute the original formula in each leaf, before merging the branches. +// When the binary exponent is fixed, equal decimal exponents can then fold +// together without expanding unreachable bigint scaling paths. +pub(crate) fn estimate_scaling_factor(mant: u64, exp: i16) -> i16 { + assert!(mant > 0); + + macro_rules! choose_bits { + ($base:expr;) => { + scale_from_bits($base, exp) + }; + ($base:expr; $half:literal $(, $rest:literal)*) => { + if mant <= (1_u64 << ($base + $half - 1)) { + choose_bits!($base; $($rest),*) + } else { + choose_bits!($base + $half; $($rest),*) + } + }; + } + + // The remaining tree has exactly the 64 leaves for bit counts 0..=63. + // Every shift in it is at most 62, and mant == 1 selects bit count zero. + if mant > (1_u64 << 63) { + scale_from_bits(64, exp) + } else { + choose_bits!(0; 32, 16, 8, 4, 2, 1) + } +} + +// Partition mant - 1 by bit length. Zero gives mant == 1; bit lengths 1..=64 +// cover every other nonzero u64 mantissa. Only the predecessor u64::MAX is +// excluded, since adding one would produce the out-of-domain mantissa zero. +fn check_estimator_model_agrees() { + assert!(BITS <= 64); + let mant = if BITS == 0 { + 1 + } else { + let leading = 1_u64 << (BITS - 1); + let predecessor = leading | (kani::any::() & (leading - 1)); + kani::assume(predecessor < u64::MAX); + predecessor + 1 + }; + let exp: i16 = kani::any(); + + assert_eq!( + estimate_scaling_factor(mant, exp), + super::estimator::estimate_scaling_factor(mant, exp) + ); + if BITS == 0 { + kani::cover(mant == 1, "smallest nonzero mantissa"); + } + if BITS == 63 { + kani::cover(mant == (1_u64 << 63), "largest power-of-two boundary"); + } + if BITS == 64 { + kani::cover(mant == u64::MAX, "largest mantissa"); + } + kani::cover(exp == i16::MIN, "smallest binary exponent"); + kani::cover(exp == i16::MAX, "largest binary exponent"); +} + +macro_rules! check_bit_count { + ($name:ident, $bits:literal) => { + #[kani::proof] + #[kani::solver(kissat)] + fn $name() { + check_estimator_model_agrees::<$bits>(); + } + }; +} + +check_bit_count!(check_estimator_model_agrees_00, 0); +check_bit_count!(check_estimator_model_agrees_01, 1); +check_bit_count!(check_estimator_model_agrees_02, 2); +check_bit_count!(check_estimator_model_agrees_03, 3); +check_bit_count!(check_estimator_model_agrees_04, 4); +check_bit_count!(check_estimator_model_agrees_05, 5); +check_bit_count!(check_estimator_model_agrees_06, 6); +check_bit_count!(check_estimator_model_agrees_07, 7); +check_bit_count!(check_estimator_model_agrees_08, 8); +check_bit_count!(check_estimator_model_agrees_09, 9); +check_bit_count!(check_estimator_model_agrees_10, 10); +check_bit_count!(check_estimator_model_agrees_11, 11); +check_bit_count!(check_estimator_model_agrees_12, 12); +check_bit_count!(check_estimator_model_agrees_13, 13); +check_bit_count!(check_estimator_model_agrees_14, 14); +check_bit_count!(check_estimator_model_agrees_15, 15); +check_bit_count!(check_estimator_model_agrees_16, 16); +check_bit_count!(check_estimator_model_agrees_17, 17); +check_bit_count!(check_estimator_model_agrees_18, 18); +check_bit_count!(check_estimator_model_agrees_19, 19); +check_bit_count!(check_estimator_model_agrees_20, 20); +check_bit_count!(check_estimator_model_agrees_21, 21); +check_bit_count!(check_estimator_model_agrees_22, 22); +check_bit_count!(check_estimator_model_agrees_23, 23); +check_bit_count!(check_estimator_model_agrees_24, 24); +check_bit_count!(check_estimator_model_agrees_25, 25); +check_bit_count!(check_estimator_model_agrees_26, 26); +check_bit_count!(check_estimator_model_agrees_27, 27); +check_bit_count!(check_estimator_model_agrees_28, 28); +check_bit_count!(check_estimator_model_agrees_29, 29); +check_bit_count!(check_estimator_model_agrees_30, 30); +check_bit_count!(check_estimator_model_agrees_31, 31); +check_bit_count!(check_estimator_model_agrees_32, 32); +check_bit_count!(check_estimator_model_agrees_33, 33); +check_bit_count!(check_estimator_model_agrees_34, 34); +check_bit_count!(check_estimator_model_agrees_35, 35); +check_bit_count!(check_estimator_model_agrees_36, 36); +check_bit_count!(check_estimator_model_agrees_37, 37); +check_bit_count!(check_estimator_model_agrees_38, 38); +check_bit_count!(check_estimator_model_agrees_39, 39); +check_bit_count!(check_estimator_model_agrees_40, 40); +check_bit_count!(check_estimator_model_agrees_41, 41); +check_bit_count!(check_estimator_model_agrees_42, 42); +check_bit_count!(check_estimator_model_agrees_43, 43); +check_bit_count!(check_estimator_model_agrees_44, 44); +check_bit_count!(check_estimator_model_agrees_45, 45); +check_bit_count!(check_estimator_model_agrees_46, 46); +check_bit_count!(check_estimator_model_agrees_47, 47); +check_bit_count!(check_estimator_model_agrees_48, 48); +check_bit_count!(check_estimator_model_agrees_49, 49); +check_bit_count!(check_estimator_model_agrees_50, 50); +check_bit_count!(check_estimator_model_agrees_51, 51); +check_bit_count!(check_estimator_model_agrees_52, 52); +check_bit_count!(check_estimator_model_agrees_53, 53); +check_bit_count!(check_estimator_model_agrees_54, 54); +check_bit_count!(check_estimator_model_agrees_55, 55); +check_bit_count!(check_estimator_model_agrees_56, 56); +check_bit_count!(check_estimator_model_agrees_57, 57); +check_bit_count!(check_estimator_model_agrees_58, 58); +check_bit_count!(check_estimator_model_agrees_59, 59); +check_bit_count!(check_estimator_model_agrees_60, 60); +check_bit_count!(check_estimator_model_agrees_61, 61); +check_bit_count!(check_estimator_model_agrees_62, 62); +check_bit_count!(check_estimator_model_agrees_63, 63); +check_bit_count!(check_estimator_model_agrees_64, 64); diff --git a/library/core/src/num/imp/flt2dec/mod.rs b/library/core/src/num/imp/flt2dec/mod.rs index cf895b3e89e66..59650ca168868 100644 --- a/library/core/src/num/imp/flt2dec/mod.rs +++ b/library/core/src/num/imp/flt2dec/mod.rs @@ -683,3 +683,302 @@ where } } } + +#[cfg(kani)] +mod bit_scan_verify; +#[cfg(kani)] +mod estimator_verify; +#[cfg(kani)] +mod rounding_verify; + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +pub mod flt2dec_verify { + use super::*; + use crate::kani; + + // Use the real decoder so the exponent, rounding interval, and tie-breaking + // flag stay related. Choosing these fields independently admits values that + // neither primitive float type can produce. The sign does not affect Decoded. + pub(crate) fn arbitrary_finite_f32() -> Decoded { + assert!(GROUP < 4); + let bits = (GROUP << 29) | (kani::any::() & 0x1fff_ffff); + kani::assume(bits > 0 && bits < 0x7f80_0000); + finite_decoded(decode(f32::from_bits(bits)).1) + } + + pub(crate) fn arbitrary_finite_f64() -> Decoded { + assert!(GROUP < 32); + let bits = (GROUP << 58) | (kani::any::() & 0x03ff_ffff_ffff_ffff); + kani::assume(bits > 0 && bits < 0x7ff0_0000_0000_0000); + finite_decoded(decode(f64::from_bits(bits)).1) + } + + // One eighth of a group: SLICE fixes the next three exponent bits, so a + // slice holds eight exponents. The eight slices of a group form the group. + pub(crate) fn arbitrary_finite_f32_slice() -> Decoded { + assert!(GROUP < 4 && SLICE < 8); + let bits = (GROUP << 29) | (SLICE << 26) | (kani::any::() & 0x03ff_ffff); + kani::assume(bits > 0 && bits < 0x7f80_0000); + finite_decoded(decode(f32::from_bits(bits)).1) + } + + pub(crate) fn arbitrary_finite_f64_slice() -> Decoded { + assert!(GROUP < 32 && SLICE < 8); + let bits = (GROUP << 58) | (SLICE << 55) | (kani::any::() & 0x007f_ffff_ffff_ffff); + kani::assume(bits > 0 && bits < 0x7ff0_0000_0000_0000); + finite_decoded(decode(f64::from_bits(bits)).1) + } + + // Additional CI probes test whether fixing the exponent makes the real + // arithmetic tractable. Every significand bit remains symbolic, and the + // exhaustive groups below remain part of the required verification. + pub(crate) fn arbitrary_finite_f64_exponent() -> Decoded { + assert!(EXPONENT < 0x7ff); + let bits = (EXPONENT << 52) | (kani::any::() & 0x000f_ffff_ffff_ffff); + kani::assume(bits > 0); + finite_decoded(decode(f64::from_bits(bits)).1) + } + + // Additional diagnostics can span several exponents without fixing any + // significand bits. The complete finite partitions remain required above. + pub(crate) fn arbitrary_finite_f64_range() -> Decoded { + assert!(FIRST > 0 && FIRST < END && END <= 0x7ff0_0000_0000_0000); + let bits: u64 = kani::any(); + kani::assume(bits >= FIRST && bits < END); + kani::cover(bits == FIRST, "cached-power probe includes its first input"); + kani::cover(bits == END - 1, "cached-power probe includes its last input"); + finite_decoded(decode(f64::from_bits(bits)).1) + } + + fn finite_decoded(decoded: FullDecoded) -> Decoded { + match decoded { + FullDecoded::Finite(d) => d, + FullDecoded::Nan | FullDecoded::Infinite | FullDecoded::Zero => { + unreachable!("the input bits represent a positive finite nonzero float") + } + } + } + + // Exhaust the high exponent bits: 4 groups for f32 and 32 for f64. + // The remaining exponent bits and every significand bit stay symbolic. + // Their union is the original positive finite nonzero input domain, + // including subnormals. Every strategy instantiates the complete list. + // Fallback reachability is existential over the union. Check it in f64 + // group 16; individual groups need not contain a fallback case. + macro_rules! for_each_finite_partition { + ($proof:ident) => { + $proof!(f32_00, arbitrary_finite_f32, 0, false); + $proof!(f32_01, arbitrary_finite_f32, 1, false); + $proof!(f32_02, arbitrary_finite_f32, 2, false); + $proof!(f32_03, arbitrary_finite_f32, 3, false); + $proof!(f64_00, arbitrary_finite_f64, 0, false); + $proof!(f64_01, arbitrary_finite_f64, 1, false); + $proof!(f64_02, arbitrary_finite_f64, 2, false); + $proof!(f64_03, arbitrary_finite_f64, 3, false); + $proof!(f64_04, arbitrary_finite_f64, 4, false); + $proof!(f64_05, arbitrary_finite_f64, 5, false); + $proof!(f64_06, arbitrary_finite_f64, 6, false); + $proof!(f64_07, arbitrary_finite_f64, 7, false); + $proof!(f64_08, arbitrary_finite_f64, 8, false); + $proof!(f64_09, arbitrary_finite_f64, 9, false); + $proof!(f64_10, arbitrary_finite_f64, 10, false); + $proof!(f64_11, arbitrary_finite_f64, 11, false); + $proof!(f64_12, arbitrary_finite_f64, 12, false); + $proof!(f64_13, arbitrary_finite_f64, 13, false); + $proof!(f64_14, arbitrary_finite_f64, 14, false); + $proof!(f64_15, arbitrary_finite_f64, 15, false); + $proof!(f64_16, arbitrary_finite_f64, 16, true); + $proof!(f64_17, arbitrary_finite_f64, 17, false); + $proof!(f64_18, arbitrary_finite_f64, 18, false); + $proof!(f64_19, arbitrary_finite_f64, 19, false); + $proof!(f64_20, arbitrary_finite_f64, 20, false); + $proof!(f64_21, arbitrary_finite_f64, 21, false); + $proof!(f64_22, arbitrary_finite_f64, 22, false); + $proof!(f64_23, arbitrary_finite_f64, 23, false); + $proof!(f64_24, arbitrary_finite_f64, 24, false); + $proof!(f64_25, arbitrary_finite_f64, 25, false); + $proof!(f64_26, arbitrary_finite_f64, 26, false); + $proof!(f64_27, arbitrary_finite_f64, 27, false); + $proof!(f64_28, arbitrary_finite_f64, 28, false); + $proof!(f64_29, arbitrary_finite_f64, 29, false); + $proof!(f64_30, arbitrary_finite_f64, 30, false); + $proof!(f64_31, arbitrary_finite_f64, 31, false); + }; + } + pub(crate) use for_each_finite_partition; + + // Upper bound on the (symbolic) digit-buffer length used by the proofs of + // `digits_to_dec_str` / `digits_to_exp_str`. Their `assume_init` safety + // obligations depend only on control flow driven by `buf.len()`, `exp`, and + // the digit-count arguments, and every length-dependent branch is a + // comparison against a small value (`buf.len() == 1` in `digits_to_exp_str`, + // `exp < buf.len()` and `frac_digits > buf.len() - exp` in + // `digits_to_dec_str`), so a symbolic length in `1..=4` reaches every branch + // and therefore every distinct set of initialized `parts`; a longer buffer + // only adds digits to a `Part::Copy` slice. + const PROOF_BUFLEN: usize = 4; + + // A digit buffer of symbolic length `1..=PROOF_BUFLEN` whose first digit is + // nonzero, as the callees require. + fn any_digits(buf: &[u8; PROOF_BUFLEN]) -> &[u8] { + kani::assume(buf[0] > b'0'); + let n: usize = kani::any(); + kani::assume(n >= 1 && n <= PROOF_BUFLEN); + &buf[..n] + } + + // `digits_to_dec_str` writes 2, 3, or 4 `parts` depending on `exp` and + // `frac_digits`, then `assume_init_ref`s exactly the prefix it wrote. Kani + // checks that no uninitialized `Part` is ever read and that no UB occurs. + #[kani::proof] + fn check_digits_to_dec_str() { + let buf: [u8; PROOF_BUFLEN] = kani::any(); + let exp: i16 = kani::any(); + let frac_digits: usize = kani::any(); + let mut parts: [MaybeUninit>; 4] = [const { MaybeUninit::uninit() }; 4]; + let _ = digits_to_dec_str(any_digits(&buf), exp, frac_digits, &mut parts); + } + + // `digits_to_exp_str` writes a variable prefix of up to 6 `parts` and + // `assume_init_ref`s `parts[..n + 2]` for the `n` it actually wrote; the + // `buf.len() == 1` case takes its own (3-part) path. + #[kani::proof] + fn check_digits_to_exp_str() { + let buf: [u8; PROOF_BUFLEN] = kani::any(); + let exp: i16 = kani::any(); + let min_ndigits: usize = kani::any(); + let upper: bool = kani::any(); + let mut parts: [MaybeUninit>; 6] = [const { MaybeUninit::uninit() }; 6]; + let _ = digits_to_exp_str(any_digits(&buf), exp, min_ndigits, upper, &mut parts); + } + + // An arbitrary sign-formatting option. + fn any_sign() -> Sign { + if kani::any() { Sign::Minus } else { Sign::MinusPlus } + } + + // A stub digit generator standing in for `grisu`/`dragon` `format_shortest`. + // It writes one arbitrary nonzero digit into the scratch buffer and returns + // it with an arbitrary exponent. This isolates the `to_shortest_*` + // functions' own `unsafe` (the `assume_init` on `parts` and the delegation + // to the already-verified `digits_to_*_str`) from the loopy strategy code, + // which is verified separately. A generic `fn` is required here rather than + // a closure so it satisfies the higher-ranked lifetime in the `F` bound. + fn stub_shortest<'a>(_d: &Decoded, buf: &'a mut [MaybeUninit]) -> (&'a [u8], i16) { + let digit: u8 = kani::any(); + kani::assume(digit > b'0'); + buf[0] = MaybeUninit::new(digit); + let exp: i16 = kani::any(); + // SAFETY: we just initialized the element `..1`. + (unsafe { buf[..1].assume_init_ref() }, exp) + } + + // `to_shortest_str` handles NaN/Inf/Zero by writing `parts[..1]` and the + // finite case by delegating to `digits_to_dec_str`. An arbitrary `f64` + // reaches every `FullDecoded` arm. + #[kani::proof] + fn check_to_shortest_str() { + let v: f64 = kani::any(); + let sign = any_sign(); + let frac_digits: usize = kani::any(); + let mut buf: [MaybeUninit; MAX_SIG_DIGITS] = + [const { MaybeUninit::uninit() }; MAX_SIG_DIGITS]; + let mut parts: [MaybeUninit>; 4] = [const { MaybeUninit::uninit() }; 4]; + let _ = to_shortest_str(stub_shortest, v, sign, frac_digits, &mut buf, &mut parts); + } + + // `to_shortest_exp_str` is the exponential-form analogue; its finite arm + // delegates to `digits_to_dec_str` or `digits_to_exp_str` per `dec_bounds`. + #[kani::proof] + fn check_to_shortest_exp_str() { + let v: f64 = kani::any(); + let sign = any_sign(); + let lo: i16 = kani::any(); + let hi: i16 = kani::any(); + kani::assume(lo <= hi); + let upper: bool = kani::any(); + let mut buf: [MaybeUninit; MAX_SIG_DIGITS] = + [const { MaybeUninit::uninit() }; MAX_SIG_DIGITS]; + let mut parts: [MaybeUninit>; 6] = [const { MaybeUninit::uninit() }; 6]; + let _ = to_shortest_exp_str(stub_shortest, v, sign, (lo, hi), upper, &mut buf, &mut parts); + } + + // For `f64`, `decode` bottoms out at `decoded.exp == -1076` (normal-min, + // which subtracts 2 from `integer_decode`'s minimum of `-1074`), where + // `estimate_max_buf_len` returns 828. 1024 (the size the real `fmt` callers + // use) covers every reachable decoded exponent for the + // `buf.len() >= maxlen` assertions in both `to_exact_*` functions. + const PROOF_EXACT_BUFLEN: usize = 1024; + + // Stub `format_exact` for `to_exact_exp_str`, which always passes the result + // to `digits_to_exp_str` (it calls the generator with `limit = i16::MIN`, so + // the real one never returns an empty buffer here). Returns one nonzero + // digit with an arbitrary exponent. + fn stub_exact_full<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + _limit: i16, + ) -> (&'a [u8], i16) { + let digit: u8 = kani::any(); + kani::assume(digit > b'0'); + buf[0] = MaybeUninit::new(digit); + let exp: i16 = kani::any(); + // SAFETY: we just initialized the element `..1`. + (unsafe { buf[..1].assume_init_ref() }, exp) + } + + // Stub `format_exact` for `to_exact_fixed_str`, which branches on + // `exp <= limit`. That arm requires an empty result (the source + // `debug_assert_eq!`s `buf.len() == 0`); the other arm needs a valid nonzero + // digit with `exp > limit`. Couple the result to `limit` so both caller + // arms are exercised soundly. + fn stub_exact_limited<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + limit: i16, + ) -> (&'a [u8], i16) { + if kani::any() { + let exp: i16 = kani::any(); + kani::assume(exp <= limit); + // SAFETY: an empty prefix is trivially initialized. + (unsafe { buf[..0].assume_init_ref() }, exp) + } else { + let digit: u8 = kani::any(); + kani::assume(digit > b'0'); + buf[0] = MaybeUninit::new(digit); + let exp: i16 = kani::any(); + kani::assume(exp > limit); + // SAFETY: we just initialized the element `..1`. + (unsafe { buf[..1].assume_init_ref() }, exp) + } + } + + // `to_exact_exp_str` writes `parts[..1]` for NaN/Inf, `parts[..3]`/`parts[..1]` + // for zero, and delegates to `digits_to_exp_str` for finite values. + #[kani::proof] + fn check_to_exact_exp_str() { + let v: f64 = kani::any(); + let sign = any_sign(); + let ndigits: usize = kani::any(); + kani::assume(ndigits > 0); + let upper: bool = kani::any(); + let mut buf: [MaybeUninit; PROOF_EXACT_BUFLEN] = + [const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN]; + let mut parts: [MaybeUninit>; 6] = [const { MaybeUninit::uninit() }; 6]; + let _ = to_exact_exp_str(stub_exact_full, v, sign, ndigits, upper, &mut buf, &mut parts); + } + + // `to_exact_fixed_str` additionally has a finite sub-branch (`exp <= limit`) + // that renders like zero; `stub_exact_limited` reaches both sub-branches. + #[kani::proof] + fn check_to_exact_fixed_str() { + let v: f64 = kani::any(); + let sign = any_sign(); + let frac_digits: usize = kani::any(); + let mut buf: [MaybeUninit; PROOF_EXACT_BUFLEN] = + [const { MaybeUninit::uninit() }; PROOF_EXACT_BUFLEN]; + let mut parts: [MaybeUninit>; 4] = [const { MaybeUninit::uninit() }; 4]; + let _ = to_exact_fixed_str(stub_exact_limited, v, sign, frac_digits, &mut buf, &mut parts); + } +} diff --git a/library/core/src/num/imp/flt2dec/rounding_verify.rs b/library/core/src/num/imp/flt2dec/rounding_verify.rs new file mode 100644 index 0000000000000..c00e35d22f374 --- /dev/null +++ b/library/core/src/num/imp/flt2dec/rounding_verify.rs @@ -0,0 +1,100 @@ +//! A bounded contract for the rounding helper used by the strategy proofs. + +use super::round_up; +use crate::kani; + +const PROOF_BUFLEN: usize = 32; + +// Read only the active slice, checking the byte condition needed by round_up. +pub(crate) fn bytes_below_max(digits: &[u8]) -> bool { + assert!(digits.len() <= PROOF_BUFLEN); + macro_rules! check_bytes { + ($($index:literal),+ $(,)?) => { + true $(& ((digits.len() <= $index) || digits[$index] < u8::MAX))+ + }; + } + check_bytes!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, + ) +} + +// Read every byte with constant indices in helper proofs. +pub(crate) fn prefix_checksum(digits: &[u8]) -> u8 { + assert!(digits.len() <= PROOF_BUFLEN); + macro_rules! read_bytes { + ($($index:literal),+ $(,)?) => { + 0 $(^ (if digits.len() > $index { digits[$index] } else { 0 }))+ + }; + } + read_bytes!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, + ) +} + +// The fixed capacity lets contract predicates use constant indices instead of +// unfolding a symbolic iterator each time a generator rounds its output. +pub(crate) fn prefix_all( + digits: &[u8; PROOF_BUFLEN], + len: usize, + predicate: impl Fn(u8) -> bool, +) -> bool { + macro_rules! check_bytes { + ($($index:literal),+ $(,)?) => { + true $(& ((len <= $index) | predicate(digits[$index])))+ + }; + } + + check_bytes!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, + ) +} + +// A fixed array gives the contract a sized write set. The adapter below copies +// only the active prefix back, so the contract cannot initialize unused bytes +// in a generator's MaybeUninit buffer. +#[kani::requires( + len <= PROOF_BUFLEN && prefix_all(digits, len, |digit| digit < u8::MAX) +)] +#[kani::ensures(|result| { + *result == old( + prefix_all(digits, len, |digit| digit == b'9') + .then_some(if len == 0 { b'1' } else { b'0' }) + ) +})] +#[kani::modifies(digits)] +pub(crate) fn round_up_contract(digits: &mut [u8; PROOF_BUFLEN], len: usize) -> Option { + round_up(&mut digits[..len]) +} + +// The caller's proof uses the contract for round_up_contract. No assumptions +// are made here: the contract checks the input byte constraint, and this +// adapter checks the bound before copying the caller's prefix. +pub(crate) fn stub_round_up(digits: &mut [u8]) -> Option { + let len = digits.len(); + assert!(len <= PROOF_BUFLEN); + let mut storage = [0; PROOF_BUFLEN]; + storage[..len].copy_from_slice(digits); + let result = round_up_contract(&mut storage, len); + digits.copy_from_slice(&storage[..len]); + result +} + +#[kani::proof_for_contract(round_up_contract)] +#[kani::unwind(33)] +fn check_round_up_contract() { + let mut digits: [u8; PROOF_BUFLEN] = kani::any(); + let len: usize = kani::any(); + let result = round_up_contract(&mut digits, len); + kani::cover(len == 0, "rounding accepts an empty prefix"); + kani::cover( + len == PROOF_BUFLEN && result == Some(b'0'), + "rounding carries across the full buffer", + ); + kani::cover( + len == PROOF_BUFLEN && result.is_none(), + "rounding can preserve the full buffer length", + ); +} diff --git a/library/core/src/num/imp/flt2dec/strategy/dragon.rs b/library/core/src/num/imp/flt2dec/strategy/dragon.rs index b63b93034d57f..5ce33281ccc77 100644 --- a/library/core/src/num/imp/flt2dec/strategy/dragon.rs +++ b/library/core/src/num/imp/flt2dec/strategy/dragon.rs @@ -8,7 +8,11 @@ use flt2dec::estimator::estimate_scaling_factor; use flt2dec::{Decoded, MAX_SIG_DIGITS, round_up}; use crate::cmp::Ordering; +#[cfg(kani)] +use crate::kani; use crate::mem::MaybeUninit; +#[cfg(kani)] +use crate::num::imp::bignum::KaniInvariantOrdering; use crate::num::imp::bignum::{Big, Digit}; use crate::num::imp::flt2dec; @@ -62,6 +66,8 @@ static POW5TO256: [Digit; 10] = [ #[doc(hidden)] pub fn mul_pow10(x: &mut Big, n: usize) -> &mut Big { + #[cfg(kani)] + let n = dragon_verify::proof_pow10_exponent(n); debug_assert!(n < 512); // Save ourself the left shift for the smallest cases. if n < 8 { @@ -132,30 +138,19 @@ fn div_rem_upto_16<'a>( (d, x) } -/// The shortest mode implementation for Dragon. -pub fn format_shortest<'a>( - d: &Decoded, - buf: &'a mut [MaybeUninit], -) -> (/*digits*/ &'a [u8], /*exp*/ i16) { - // the number `v` to format is known to be: - // - equal to `mant * 2^exp`; - // - preceded by `(mant - 2 * minus) * 2^exp` in the original type; and - // - followed by `(mant + 2 * plus) * 2^exp` in the original type. - // - // obviously, `minus` and `plus` cannot be zero. (for infinities, we use out-of-range values.) - // also we assume that at least one digit is generated, i.e., `mant` cannot be zero too. - // - // this also means that any number between `low = (mant - minus) * 2^exp` and - // `high = (mant + plus) * 2^exp` will map to this exact floating point number, - // with bounds included when the original mantissa was even (i.e., `!mant_was_odd`). - - assert!(d.mant > 0); - assert!(d.minus > 0); - assert!(d.plus > 0); - assert!(d.mant.checked_add(d.plus).is_some()); - assert!(d.mant.checked_sub(d.minus).is_some()); - assert!(buf.len() >= MAX_SIG_DIGITS); +/// The scaled state of `format_shortest` before digit generation. +struct ShortestScaled { + k: i16, + mant: Big, + minus: Big, + plus: Big, + scale: Big, +} +/// Converts `{mant, minus, plus} * 2^exp` into the fractional form `{mant, minus, plus} / scale` +/// and finds `k` such that `scale < mant + plus <= scale * 10` (`<=` on the left when inclusive). +#[inline] +fn scale_shortest(d: &Decoded) -> ShortestScaled { // `a.cmp(&b) < rounding` is `if d.inclusive {a <= b} else {a < b}` let rounding = if d.inclusive { Ordering::Greater } else { Ordering::Equal }; @@ -203,6 +198,39 @@ pub fn format_shortest<'a>( plus.mul_small(10); } + ShortestScaled { k, mant, minus, plus, scale } +} + +/// The shortest mode implementation for Dragon. +pub fn format_shortest<'a>( + d: &Decoded, + buf: &'a mut [MaybeUninit], +) -> (/*digits*/ &'a [u8], /*exp*/ i16) { + // the number `v` to format is known to be: + // - equal to `mant * 2^exp`; + // - preceded by `(mant - 2 * minus) * 2^exp` in the original type; and + // - followed by `(mant + 2 * plus) * 2^exp` in the original type. + // + // obviously, `minus` and `plus` cannot be zero. (for infinities, we use out-of-range values.) + // also we assume that at least one digit is generated, i.e., `mant` cannot be zero too. + // + // this also means that any number between `low = (mant - minus) * 2^exp` and + // `high = (mant + plus) * 2^exp` will map to this exact floating point number, + // with bounds included when the original mantissa was even (i.e., `!mant_was_odd`). + + assert!(d.mant > 0); + assert!(d.minus > 0); + assert!(d.plus > 0); + assert!(d.mant.checked_add(d.plus).is_some()); + assert!(d.mant.checked_sub(d.minus).is_some()); + assert!(buf.len() >= MAX_SIG_DIGITS); + + // `a.cmp(&b) < rounding` is `if d.inclusive {a <= b} else {a < b}` + let rounding = if d.inclusive { Ordering::Greater } else { Ordering::Equal }; + + // scale the inputs so that `scale < mant + plus <= scale * 10` (see `scale_shortest`). + let ShortestScaled { mut k, mut mant, mut minus, mut plus, scale } = scale_shortest(d); + // cache `(2, 4, 8) * scale` for digit generation. let mut scale2 = scale.clone(); scale2.mul_pow2(1); @@ -211,9 +239,57 @@ pub fn format_shortest<'a>( let mut scale8 = scale.clone(); scale8.mul_pow2(3); + #[cfg(kani)] + let mut down = false; + #[cfg(not(kani))] let mut down; + #[cfg(kani)] + let mut up = false; + #[cfg(not(kani))] let mut up; + #[cfg(kani)] + let mut d = 0; + #[cfg(not(kani))] + let mut d; + #[cfg(kani)] + let mut up_candidate = Big::from_small(0); + #[cfg(not(kani))] + let mut up_candidate; let mut i = 0; + #[cfg(kani)] + let proof_zero = Big::from_small(0); + #[cfg(kani)] + let proof_scale10 = scale.clone().mul_small(10).clone(); + #[cfg(kani)] + let proof_max_limbs = crate::cmp::max(mant.kani_size(), proof_scale10.kani_size()); + #[cfg_attr(kani, kani::loop_modifies(&i, &d, &mant, &minus, &plus, &up_candidate, &down, &up, &buf[..]))] + #[cfg_attr(kani, kani::loop_invariant( + i < MAX_SIG_DIGITS + && mant.kani_invariant_size() > 0 + && minus.kani_invariant_size() > 0 && plus.kani_invariant_size() > 0 + && mant.kani_invariant_valid_storage() + && minus.kani_invariant_valid_storage() && plus.kani_invariant_valid_storage() + && mant.kani_invariant_size() <= proof_max_limbs + && minus.kani_invariant_size() <= proof_max_limbs + && plus.kani_invariant_size() <= proof_max_limbs + && mant.kani_invariant_less_than(&proof_scale10) + && minus.kani_invariant_less_than(&proof_scale10) + && plus.kani_invariant_less_than(&proof_scale10) + && proof_zero.kani_invariant_less_than(&minus) + && proof_zero.kani_invariant_less_than(&plus) + && (i == 0 || match proof_scale10.kani_invariant_cmp_scaled_sum(&mant, &plus, 0) { + KaniInvariantOrdering::Less => (Ordering::Less as i8) >= rounding as i8, + KaniInvariantOrdering::Equal => (Ordering::Equal as i8) >= rounding as i8, + KaniInvariantOrdering::Greater => true, + KaniInvariantOrdering::Unrepresentable => false, + }) + && match scale.kani_invariant_cmp_scaled_sum(&minus, &plus, MAX_SIG_DIGITS - 1 - i) { + KaniInvariantOrdering::Less => true, + KaniInvariantOrdering::Equal | KaniInvariantOrdering::Greater + | KaniInvariantOrdering::Unrepresentable => false, + } + && initialized_digit_prefix(buf, i) + ))] loop { // invariants, where `d[0..n-1]` are digits generated so far: // - `v = mant / scale * 10^(k-n-1) + d[0..n-1] * 10^(k-n)` @@ -223,7 +299,7 @@ pub fn format_shortest<'a>( // where `d[i..j]` is a shorthand for `d[i] * 10^(j-i) + ... + d[j-1] * 10 + d[j]`. // generate one digit: `d[n] = floor(mant / scale) < 10`. - let (d, _) = div_rem_upto_16(&mut mant, &scale, &scale2, &scale4, &scale8); + (d, _) = div_rem_upto_16(&mut mant, &scale, &scale2, &scale4, &scale8); debug_assert!(d < 10); buf[i] = MaybeUninit::new(b'0' + d); i += 1; @@ -260,7 +336,8 @@ pub fn format_shortest<'a>( // - stop and round `up` (increase the last digit) when `scale < mant + plus` (or `<=`). // - keep generating otherwise. down = mant.cmp(&minus) < rounding; - up = scale.cmp(mant.clone().add(&plus)) < rounding; + up_candidate = mant.clone(); + up = scale.cmp(up_candidate.add(&plus)) < rounding; if down || up { break; } // we have the shortest representation, proceed to the rounding @@ -292,6 +369,92 @@ pub fn format_shortest<'a>( (unsafe { buf[..i].assume_init_ref() }, k) } +fn fill_zeroes(buf: &mut [MaybeUninit]) { + buf.fill(MaybeUninit::new(b'0')); +} + +fn generate_exact_digits( + mant: &mut Big, + scale: &Big, + scale2: &Big, + scale4: &Big, + scale8: &Big, + buf: &mut [MaybeUninit], +) -> bool { + let len = buf.len(); + #[cfg(kani)] + let proof_scale10 = scale.clone().mul_small(10).clone(); + #[cfg(kani)] + let proof_max_limbs = crate::cmp::max(mant.kani_size(), proof_scale10.kani_size()); + #[cfg(kani)] + let mut d = 0; + #[cfg(not(kani))] + let mut d; + #[cfg_attr(kani, kani::loop_modifies(&*mant, &buf[..], &d))] + #[cfg_attr(kani, kani::loop_invariant( + kani::index <= len + && len <= dragon_verify::PROOF_BUFLEN + && mant.kani_invariant_size() <= proof_max_limbs + && mant.kani_invariant_valid_storage() + && mant.kani_invariant_less_than(&proof_scale10) + && initialized_digit_prefix(buf, kani::index) + ))] + for i in 0..len { + if mant.is_zero() { + // following digits are all zeroes, we stop here + // do *not* try to perform rounding! rather, fill remaining digits. + fill_zeroes(&mut buf[i..len]); + return true; + } + + d = 0; + if *mant >= *scale8 { + mant.sub(scale8); + d += 8; + } + if *mant >= *scale4 { + mant.sub(scale4); + d += 4; + } + if *mant >= *scale2 { + mant.sub(scale2); + d += 2; + } + if *mant >= *scale { + mant.sub(scale); + d += 1; + } + debug_assert!(*mant < *scale); + debug_assert!(d < 10); + buf[i] = MaybeUninit::new(b'0' + d); + mant.mul_small(10); + } + false +} + +#[cfg(kani)] +fn initialized_digit_prefix(buf: &[MaybeUninit], len: usize) -> bool { + // Each byte keeps its own initialization guard. Combine the independent + // checks eagerly to avoid nested short-circuit paths during instrumentation. + macro_rules! check_digit { + ($($index:literal),+ $(,)?) => { + true $(& (buf.len() <= $index || len <= $index || ( + kani::mem::can_dereference(crate::ptr::addr_of!(buf[$index]) as *const u8) + && unsafe { + let digit = *(crate::ptr::addr_of!(buf[$index]) as *const u8); + digit >= b'0' && digit <= b'9' + } + )))+ + }; + } + len <= buf.len() + && len <= dragon_verify::PROOF_BUFLEN + && check_digit!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31 + ) +} + /// The exact and fixed mode implementation for Dragon. pub fn format_exact<'a>( d: &Decoded, @@ -349,7 +512,7 @@ pub fn format_exact<'a>( buf.len() }; - if len > 0 { + let exhausted = if len > 0 { // cache `(2, 4, 8) * scale` for digit generation. // (this can be expensive, so do not calculate them when the buffer is empty.) let mut scale2 = scale.clone(); @@ -359,65 +522,3312 @@ pub fn format_exact<'a>( let mut scale8 = scale.clone(); scale8.mul_pow2(3); - for i in 0..len { - if mant.is_zero() { - // following digits are all zeroes, we stop here - // do *not* try to perform rounding! rather, fill remaining digits. - for c in &mut buf[i..len] { - *c = MaybeUninit::new(b'0'); + generate_exact_digits(&mut mant, &scale, &scale2, &scale4, &scale8, &mut buf[..len]) + } else { + false + }; + + if exhausted { + // SAFETY: digit generation initialized the requested prefix. + (unsafe { buf[..len].assume_init_ref() }, k) + } else { + // rounding up if we stop in the middle of digits + // if the following digits are exactly 5000..., check the prior digit and try to + // round to even (i.e., avoid rounding up when the prior digit is even). + let order = mant.cmp(scale.mul_small(5)); + if order == Ordering::Greater + || (order == Ordering::Equal + // SAFETY: `buf[len-1]` is initialized. + && len > 0 && unsafe { buf[len - 1].assume_init() } & 1 == 1) + { + // if rounding up changes the length, the exponent should also change. + // but we've been requested a fixed number of digits, so do not alter the buffer... + // SAFETY: we initialized that memory above. + if let Some(c) = round_up(unsafe { buf[..len].assume_init_mut() }) { + // ...unless we've been requested the fixed precision instead. + // we also need to check that, if the original buffer was empty, + // the additional digit can only be added when `k == limit` (edge case). + k += 1; + if k > limit && len < buf.len() { + buf[len] = MaybeUninit::new(c); + len += 1; } - // SAFETY: we initialized that memory above. - return (unsafe { buf[..len].assume_init_ref() }, k); } + } + + // SAFETY: we initialized that memory above. + (unsafe { buf[..len].assume_init_ref() }, k) + } +} + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +pub mod dragon_verify { + use super::*; + use crate::kani; + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + use crate::num::imp::bignum::Big32x40 as Big; + #[cfg(target_pointer_width = "64")] + use crate::num::imp::bignum::Big64x20 as Big; + use crate::num::imp::bignum::KANI_LIMBS; + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + use crate::primitive::u32 as Digit; + #[cfg(target_pointer_width = "64")] + use crate::primitive::u64 as Digit; + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + type WideDigit = u64; + #[cfg(target_pointer_width = "64")] + type WideDigit = u128; + use crate::num::imp::flt2dec::flt2dec_verify::{ + arbitrary_finite_f32, arbitrary_finite_f64, arbitrary_finite_f64_exponent, + for_each_finite_partition, + }; + + // Keep exact comparison semantics and the generator's digit writes. The + // comparison models have a separate equivalence proof; exact mode also + // uses the division contract below for initial fixup. Neither termination + // nor the buffer index is assumed. Bigint unwind assertions remain enabled. + // Lengths above 32 require a separate proof; these harnesses are bounded. + pub(super) const PROOF_BUFLEN: usize = 32; + const _: () = assert!(PROOF_BUFLEN <= u8::MAX as usize); + + #[kani::proof] + #[kani::unwind(41)] + #[kani::stub(crate::num::imp::bignum::kani_loop_size, counter_frame_limb_count)] + #[kani::stub(Big::sub, Big::kani_sub_model)] + #[kani::stub(Big::mul_small, Big::kani_mul_small_model)] + #[kani::stub(::cmp, stub_cmp)] + #[kani::stub(Big::is_zero, stub_is_zero)] + #[kani::solver(kissat)] + fn check_exact_digit_loop_frame() { + let mut mant = Big::from_u64(5); + let scale = Big::from_small(1); + let scale2 = Big::from_small(2); + let scale4 = Big::from_small(4); + let scale8 = Big::from_small(8); + let mut buf = [MaybeUninit::uninit(); 1]; + let _ = generate_exact_digits(&mut mant, &scale, &scale2, &scale4, &scale8, &mut buf); + let digit = unsafe { buf[0].assume_init() }; + assert!(digit >= b'0' && digit <= b'9'); + kani::cover(digit == b'5', "the counter frame permits a generated digit"); + } + fn counter_frame_limb_count(size: usize) -> usize { + LimbCountMask(1).check(size) + } + + // The models assert the size bounds checked by the independent equivalence + // proof, then compute exact results with constant limb indices. + fn stub_cmp(left: &Big, right: &Big) -> Ordering { + left.kani_cmp_model(right) + } + + fn stub_is_zero(value: &Big) -> bool { + value.kani_is_zero_model() + } + + #[kani::proof] + #[kani::unwind(41)] + #[kani::solver(kissat)] + fn check_comparison_models_agree() { + // These methods only need size bounds. Inactive limbs may be arbitrary; + // unlike arithmetic contracts, this proof needs no zero-tail invariant. + // Six symbolic bits represent every permitted storage size, while + // keeping unused upper index bits concrete during symbolic execution. + let left = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + let right = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + // This is exactly the domain asserted by both models at their call sites. + kani::assume(left.kani_size() <= KANI_LIMBS && right.kani_size() <= KANI_LIMBS); + assert!(left.cmp(&right) == left.kani_cmp_model(&right)); + assert!(left.is_zero() == left.kani_is_zero_model()); + let ordering = left.kani_cmp_model(&right); + assert!(left.kani_invariant_size() == left.kani_size()); + assert!(left.kani_invariant_valid_storage() == left.kani_valid_storage()); + assert!(left.kani_invariant_less_than(&right) == (ordering == Ordering::Less)); + kani::cover(ordering == Ordering::Less, "comparison can be less"); + kani::cover(ordering == Ordering::Equal, "comparison can be equal"); + kani::cover(ordering == Ordering::Greater, "comparison can be greater"); + kani::cover(left.kani_is_zero_model(), "zero testing accepts zero"); + kani::cover(!left.kani_is_zero_model(), "zero testing accepts nonzero limbs"); + kani::cover(left.kani_size() == 0, "comparison accepts empty zero storage"); + kani::cover(left.kani_size() == KANI_LIMBS, "comparison accepts all bigint limbs"); + kani::cover( + left.kani_size() == 0 && !left.kani_valid_storage(), + "comparison models accept arbitrary inactive limbs", + ); + } + + #[kani::proof] + #[kani::unwind(41)] + #[kani::solver(kissat)] + fn check_add_model_agrees() { + let source = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + let other = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + kani::assume(source.kani_size() < KANI_LIMBS && other.kani_size() < KANI_LIMBS); + kani::cover(source.kani_size() == 0, "addition accepts empty storage"); + kani::cover( + source.kani_size() == KANI_LIMBS - 1, + "addition accepts the largest model input", + ); + let mut actual = source.clone(); + let mut modeled = source; + let actual_start = &mut actual as *mut Big; + let modeled_start = &mut modeled as *mut Big; + assert!(actual.add(&other) as *mut Big == actual_start); + assert!(modeled.kani_add_model(&other) as *mut Big == modeled_start); + assert!(actual.kani_same_storage(&modeled)); + kani::cover(actual.kani_size() == KANI_LIMBS, "addition can append a carry limb"); + } + + #[kani::proof] + #[kani::unwind(41)] + #[kani::solver(kissat)] + fn check_sub_model_agrees() { + let source = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + let other = Big::kani_with_arbitrary_limbs(usize::from(kani::any::() & 0x3f)); + kani::assume(source.kani_size() <= KANI_LIMBS && other.kani_size() <= KANI_LIMBS); + kani::assume(source.kani_cmp_model(&other) != Ordering::Less); + kani::cover(source.kani_size() == 0, "subtraction accepts empty storage"); + kani::cover(source.kani_size() == KANI_LIMBS, "subtraction accepts all limbs"); + let mut actual = source.clone(); + let mut modeled = source; + let actual_start = &mut actual as *mut Big; + let modeled_start = &mut modeled as *mut Big; + assert!(actual.sub(&other) as *mut Big == actual_start); + assert!(modeled.kani_sub_model(&other) as *mut Big == modeled_start); + assert!(actual.kani_same_storage(&modeled)); + } + + fn fill_zeroes_model(buf: &mut [MaybeUninit]) { + assert!(buf.len() <= PROOF_BUFLEN); + // SAFETY: MaybeUninit has byte size and alignment. The mutable + // slice covers exactly buf.len() writable bytes, including an empty slice. + unsafe { crate::ptr::write_bytes(buf.as_mut_ptr(), b'0', buf.len()) }; + } + + #[kani::proof] + #[kani::unwind(33)] + #[kani::solver(kissat)] + fn check_fill_zeroes_model_agrees() { + let initial = kani::any::<[u8; PROOF_BUFLEN]>(); + let mut actual = initial.map(MaybeUninit::new); + let mut modeled = initial.map(MaybeUninit::new); + let start = usize::from(kani::any::()); + let end = usize::from(kani::any::()); + kani::assume(start <= end && end <= PROOF_BUFLEN); + fill_zeroes(&mut actual[start..end]); + fill_zeroes_model(&mut modeled[start..end]); + macro_rules! compare_bytes { + ($($index:literal),+ $(,)?) => { + $( + // SAFETY: Both arrays began initialized, and zero filling + // preserves initialization of every byte. + assert_eq!(unsafe { actual[$index].assume_init() }, unsafe { + modeled[$index].assume_init() + }); + )+ + }; + } + compare_bytes!( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31 + ); + kani::cover(start == end, "zero filling accepts an empty slice"); + kani::cover(start == 0 && end == PROOF_BUFLEN, "zero filling reaches every byte"); + kani::cover( + start > 0 && end < PROOF_BUFLEN && start < end, + "zero filling preserves both edges", + ); + } + + fn check_mul_pow2_model_agrees() { + let mut value = Big::kani_valid_with_size(SIZE); + let shift = usize::from(kani::any::()) & (Digit::BITS as usize - 1); + let bits = (DIGITS * Digit::BITS as usize) | shift; + let digits = DIGITS; + let size = value.kani_size(); + // Use the real method's safe shift domain for valid bigint storage. + kani::assume(digits < KANI_LIMBS && size + digits <= KANI_LIMBS); + kani::assume(shift == 0 || size + digits > 0); + kani::assume( + shift == 0 + || (value.kani_limbs_mut()[KANI_LIMBS - digits - 1] + >> (Digit::BITS as usize - shift)) + == 0, + ); + let mut expected = value.clone(); + let mut actual = value; + expected.mul_pow2(bits); + actual.kani_mul_pow2_model(bits); + assert!(actual.kani_same_storage(&expected)); + kani::cover(shift == 0, "shift preserves whole limbs"); + kani::cover(shift == Digit::BITS as usize - 1, "shift uses the largest partial limb"); + kani::cover(digits == KANI_LIMBS - 1, "shift reaches the last stored limb"); + } + + // A case fixes the active size and whole-limb shift, leaving every + // partial-limb shift symbolic. Each real loop visits at most max(SIZE, + // DIGITS) limbs. Unwinding assertions verify the bound for every case. + macro_rules! check_shift_width { + ($module:ident, $($(#[$cfg:meta])* ($name:ident, $guide:ident, $size:literal, $digits:literal, $unwind:literal)),+ $(,)?) => { + mod $module { + use crate::kani; - let mut d = 0; - if mant >= scale8 { - mant.sub(&scale8); - d += 8; + $( + $(#[$cfg])* + fn $guide(size: usize) -> usize { + assert_eq!(size, $size); + $size + } + + $(#[$cfg])* + #[kani::proof] + #[kani::unwind($unwind)] + #[kani::solver(kissat)] + #[kani::stub(crate::num::imp::bignum::kani_loop_size, $guide)] + fn $name() { + super::check_mul_pow2_model_agrees::<$size, $digits>(); + } + )+ } - if mant >= scale4 { - mant.sub(&scale4); - d += 4; + }; + } + + check_shift_width!(shift_width_00, (size_00, guide_size_00, 0, 0, 1),); + check_shift_width!( + shift_width_01, + (size_00, guide_size_00, 0, 1, 2), + (size_01, guide_size_01, 1, 0, 2), + ); + check_shift_width!( + shift_width_02, + (size_00, guide_size_00, 0, 2, 3), + (size_01, guide_size_01, 1, 1, 2), + (size_02, guide_size_02, 2, 0, 3), + ); + check_shift_width!( + shift_width_03, + (size_00, guide_size_00, 0, 3, 4), + (size_01, guide_size_01, 1, 2, 3), + (size_02, guide_size_02, 2, 1, 3), + (size_03, guide_size_03, 3, 0, 4), + ); + check_shift_width!( + shift_width_04, + (size_00, guide_size_00, 0, 4, 5), + (size_01, guide_size_01, 1, 3, 4), + (size_02, guide_size_02, 2, 2, 3), + (size_03, guide_size_03, 3, 1, 4), + (size_04, guide_size_04, 4, 0, 5), + ); + check_shift_width!( + shift_width_05, + (size_00, guide_size_00, 0, 5, 6), + (size_01, guide_size_01, 1, 4, 5), + (size_02, guide_size_02, 2, 3, 4), + (size_03, guide_size_03, 3, 2, 4), + (size_04, guide_size_04, 4, 1, 5), + (size_05, guide_size_05, 5, 0, 6), + ); + check_shift_width!( + shift_width_06, + (size_00, guide_size_00, 0, 6, 7), + (size_01, guide_size_01, 1, 5, 6), + (size_02, guide_size_02, 2, 4, 5), + (size_03, guide_size_03, 3, 3, 4), + (size_04, guide_size_04, 4, 2, 5), + (size_05, guide_size_05, 5, 1, 6), + (size_06, guide_size_06, 6, 0, 7), + ); + check_shift_width!( + shift_width_07, + (size_00, guide_size_00, 0, 7, 8), + (size_01, guide_size_01, 1, 6, 7), + (size_02, guide_size_02, 2, 5, 6), + (size_03, guide_size_03, 3, 4, 5), + (size_04, guide_size_04, 4, 3, 5), + (size_05, guide_size_05, 5, 2, 6), + (size_06, guide_size_06, 6, 1, 7), + (size_07, guide_size_07, 7, 0, 8), + ); + check_shift_width!( + shift_width_08, + (size_00, guide_size_00, 0, 8, 9), + (size_01, guide_size_01, 1, 7, 8), + (size_02, guide_size_02, 2, 6, 7), + (size_03, guide_size_03, 3, 5, 6), + (size_04, guide_size_04, 4, 4, 5), + (size_05, guide_size_05, 5, 3, 6), + (size_06, guide_size_06, 6, 2, 7), + (size_07, guide_size_07, 7, 1, 8), + (size_08, guide_size_08, 8, 0, 9), + ); + check_shift_width!( + shift_width_09, + (size_00, guide_size_00, 0, 9, 10), + (size_01, guide_size_01, 1, 8, 9), + (size_02, guide_size_02, 2, 7, 8), + (size_03, guide_size_03, 3, 6, 7), + (size_04, guide_size_04, 4, 5, 6), + (size_05, guide_size_05, 5, 4, 6), + (size_06, guide_size_06, 6, 3, 7), + (size_07, guide_size_07, 7, 2, 8), + (size_08, guide_size_08, 8, 1, 9), + (size_09, guide_size_09, 9, 0, 10), + ); + check_shift_width!( + shift_width_10, + (size_00, guide_size_00, 0, 10, 11), + (size_01, guide_size_01, 1, 9, 10), + (size_02, guide_size_02, 2, 8, 9), + (size_03, guide_size_03, 3, 7, 8), + (size_04, guide_size_04, 4, 6, 7), + (size_05, guide_size_05, 5, 5, 6), + (size_06, guide_size_06, 6, 4, 7), + (size_07, guide_size_07, 7, 3, 8), + (size_08, guide_size_08, 8, 2, 9), + (size_09, guide_size_09, 9, 1, 10), + (size_10, guide_size_10, 10, 0, 11), + ); + check_shift_width!( + shift_width_11, + (size_00, guide_size_00, 0, 11, 12), + (size_01, guide_size_01, 1, 10, 11), + (size_02, guide_size_02, 2, 9, 10), + (size_03, guide_size_03, 3, 8, 9), + (size_04, guide_size_04, 4, 7, 8), + (size_05, guide_size_05, 5, 6, 7), + (size_06, guide_size_06, 6, 5, 7), + (size_07, guide_size_07, 7, 4, 8), + (size_08, guide_size_08, 8, 3, 9), + (size_09, guide_size_09, 9, 2, 10), + (size_10, guide_size_10, 10, 1, 11), + (size_11, guide_size_11, 11, 0, 12), + ); + check_shift_width!( + shift_width_12, + (size_00, guide_size_00, 0, 12, 13), + (size_01, guide_size_01, 1, 11, 12), + (size_02, guide_size_02, 2, 10, 11), + (size_03, guide_size_03, 3, 9, 10), + (size_04, guide_size_04, 4, 8, 9), + (size_05, guide_size_05, 5, 7, 8), + (size_06, guide_size_06, 6, 6, 7), + (size_07, guide_size_07, 7, 5, 8), + (size_08, guide_size_08, 8, 4, 9), + (size_09, guide_size_09, 9, 3, 10), + (size_10, guide_size_10, 10, 2, 11), + (size_11, guide_size_11, 11, 1, 12), + (size_12, guide_size_12, 12, 0, 13), + ); + check_shift_width!( + shift_width_13, + (size_00, guide_size_00, 0, 13, 14), + (size_01, guide_size_01, 1, 12, 13), + (size_02, guide_size_02, 2, 11, 12), + (size_03, guide_size_03, 3, 10, 11), + (size_04, guide_size_04, 4, 9, 10), + (size_05, guide_size_05, 5, 8, 9), + (size_06, guide_size_06, 6, 7, 8), + (size_07, guide_size_07, 7, 6, 8), + (size_08, guide_size_08, 8, 5, 9), + (size_09, guide_size_09, 9, 4, 10), + (size_10, guide_size_10, 10, 3, 11), + (size_11, guide_size_11, 11, 2, 12), + (size_12, guide_size_12, 12, 1, 13), + (size_13, guide_size_13, 13, 0, 14), + ); + check_shift_width!( + shift_width_14, + (size_00, guide_size_00, 0, 14, 15), + (size_01, guide_size_01, 1, 13, 14), + (size_02, guide_size_02, 2, 12, 13), + (size_03, guide_size_03, 3, 11, 12), + (size_04, guide_size_04, 4, 10, 11), + (size_05, guide_size_05, 5, 9, 10), + (size_06, guide_size_06, 6, 8, 9), + (size_07, guide_size_07, 7, 7, 8), + (size_08, guide_size_08, 8, 6, 9), + (size_09, guide_size_09, 9, 5, 10), + (size_10, guide_size_10, 10, 4, 11), + (size_11, guide_size_11, 11, 3, 12), + (size_12, guide_size_12, 12, 2, 13), + (size_13, guide_size_13, 13, 1, 14), + (size_14, guide_size_14, 14, 0, 15), + ); + check_shift_width!( + shift_width_15, + (size_00, guide_size_00, 0, 15, 16), + (size_01, guide_size_01, 1, 14, 15), + (size_02, guide_size_02, 2, 13, 14), + (size_03, guide_size_03, 3, 12, 13), + (size_04, guide_size_04, 4, 11, 12), + (size_05, guide_size_05, 5, 10, 11), + (size_06, guide_size_06, 6, 9, 10), + (size_07, guide_size_07, 7, 8, 9), + (size_08, guide_size_08, 8, 7, 9), + (size_09, guide_size_09, 9, 6, 10), + (size_10, guide_size_10, 10, 5, 11), + (size_11, guide_size_11, 11, 4, 12), + (size_12, guide_size_12, 12, 3, 13), + (size_13, guide_size_13, 13, 2, 14), + (size_14, guide_size_14, 14, 1, 15), + (size_15, guide_size_15, 15, 0, 16), + ); + check_shift_width!( + shift_width_16, + (size_00, guide_size_00, 0, 16, 17), + (size_01, guide_size_01, 1, 15, 16), + (size_02, guide_size_02, 2, 14, 15), + (size_03, guide_size_03, 3, 13, 14), + (size_04, guide_size_04, 4, 12, 13), + (size_05, guide_size_05, 5, 11, 12), + (size_06, guide_size_06, 6, 10, 11), + (size_07, guide_size_07, 7, 9, 10), + (size_08, guide_size_08, 8, 8, 9), + (size_09, guide_size_09, 9, 7, 10), + (size_10, guide_size_10, 10, 6, 11), + (size_11, guide_size_11, 11, 5, 12), + (size_12, guide_size_12, 12, 4, 13), + (size_13, guide_size_13, 13, 3, 14), + (size_14, guide_size_14, 14, 2, 15), + (size_15, guide_size_15, 15, 1, 16), + (size_16, guide_size_16, 16, 0, 17), + ); + check_shift_width!( + shift_width_17, + (size_00, guide_size_00, 0, 17, 18), + (size_01, guide_size_01, 1, 16, 17), + (size_02, guide_size_02, 2, 15, 16), + (size_03, guide_size_03, 3, 14, 15), + (size_04, guide_size_04, 4, 13, 14), + (size_05, guide_size_05, 5, 12, 13), + (size_06, guide_size_06, 6, 11, 12), + (size_07, guide_size_07, 7, 10, 11), + (size_08, guide_size_08, 8, 9, 10), + (size_09, guide_size_09, 9, 8, 10), + (size_10, guide_size_10, 10, 7, 11), + (size_11, guide_size_11, 11, 6, 12), + (size_12, guide_size_12, 12, 5, 13), + (size_13, guide_size_13, 13, 4, 14), + (size_14, guide_size_14, 14, 3, 15), + (size_15, guide_size_15, 15, 2, 16), + (size_16, guide_size_16, 16, 1, 17), + (size_17, guide_size_17, 17, 0, 18), + ); + check_shift_width!( + shift_width_18, + (size_00, guide_size_00, 0, 18, 19), + (size_01, guide_size_01, 1, 17, 18), + (size_02, guide_size_02, 2, 16, 17), + (size_03, guide_size_03, 3, 15, 16), + (size_04, guide_size_04, 4, 14, 15), + (size_05, guide_size_05, 5, 13, 14), + (size_06, guide_size_06, 6, 12, 13), + (size_07, guide_size_07, 7, 11, 12), + (size_08, guide_size_08, 8, 10, 11), + (size_09, guide_size_09, 9, 9, 10), + (size_10, guide_size_10, 10, 8, 11), + (size_11, guide_size_11, 11, 7, 12), + (size_12, guide_size_12, 12, 6, 13), + (size_13, guide_size_13, 13, 5, 14), + (size_14, guide_size_14, 14, 4, 15), + (size_15, guide_size_15, 15, 3, 16), + (size_16, guide_size_16, 16, 2, 17), + (size_17, guide_size_17, 17, 1, 18), + (size_18, guide_size_18, 18, 0, 19), + ); + check_shift_width!( + shift_width_19, + (size_00, guide_size_00, 0, 19, 20), + (size_01, guide_size_01, 1, 18, 19), + (size_02, guide_size_02, 2, 17, 18), + (size_03, guide_size_03, 3, 16, 17), + (size_04, guide_size_04, 4, 15, 16), + (size_05, guide_size_05, 5, 14, 15), + (size_06, guide_size_06, 6, 13, 14), + (size_07, guide_size_07, 7, 12, 13), + (size_08, guide_size_08, 8, 11, 12), + (size_09, guide_size_09, 9, 10, 11), + (size_10, guide_size_10, 10, 9, 11), + (size_11, guide_size_11, 11, 8, 12), + (size_12, guide_size_12, 12, 7, 13), + (size_13, guide_size_13, 13, 6, 14), + (size_14, guide_size_14, 14, 5, 15), + (size_15, guide_size_15, 15, 4, 16), + (size_16, guide_size_16, 16, 3, 17), + (size_17, guide_size_17, 17, 2, 18), + (size_18, guide_size_18, 18, 1, 19), + (size_19, guide_size_19, 19, 0, 20), + ); + check_shift_width!( + shift_width_20, + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + (size_00, guide_size_00, 0, 20, 21), + (size_01, guide_size_01, 1, 19, 20), + (size_02, guide_size_02, 2, 18, 19), + (size_03, guide_size_03, 3, 17, 18), + (size_04, guide_size_04, 4, 16, 17), + (size_05, guide_size_05, 5, 15, 16), + (size_06, guide_size_06, 6, 14, 15), + (size_07, guide_size_07, 7, 13, 14), + (size_08, guide_size_08, 8, 12, 13), + (size_09, guide_size_09, 9, 11, 12), + (size_10, guide_size_10, 10, 10, 11), + (size_11, guide_size_11, 11, 9, 12), + (size_12, guide_size_12, 12, 8, 13), + (size_13, guide_size_13, 13, 7, 14), + (size_14, guide_size_14, 14, 6, 15), + (size_15, guide_size_15, 15, 5, 16), + (size_16, guide_size_16, 16, 4, 17), + (size_17, guide_size_17, 17, 3, 18), + (size_18, guide_size_18, 18, 2, 19), + (size_19, guide_size_19, 19, 1, 20), + (size_20, guide_size_20, 20, 0, 21), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_21, + (size_00, guide_size_00, 0, 21, 22), + (size_01, guide_size_01, 1, 20, 21), + (size_02, guide_size_02, 2, 19, 20), + (size_03, guide_size_03, 3, 18, 19), + (size_04, guide_size_04, 4, 17, 18), + (size_05, guide_size_05, 5, 16, 17), + (size_06, guide_size_06, 6, 15, 16), + (size_07, guide_size_07, 7, 14, 15), + (size_08, guide_size_08, 8, 13, 14), + (size_09, guide_size_09, 9, 12, 13), + (size_10, guide_size_10, 10, 11, 12), + (size_11, guide_size_11, 11, 10, 12), + (size_12, guide_size_12, 12, 9, 13), + (size_13, guide_size_13, 13, 8, 14), + (size_14, guide_size_14, 14, 7, 15), + (size_15, guide_size_15, 15, 6, 16), + (size_16, guide_size_16, 16, 5, 17), + (size_17, guide_size_17, 17, 4, 18), + (size_18, guide_size_18, 18, 3, 19), + (size_19, guide_size_19, 19, 2, 20), + (size_20, guide_size_20, 20, 1, 21), + (size_21, guide_size_21, 21, 0, 22), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_22, + (size_00, guide_size_00, 0, 22, 23), + (size_01, guide_size_01, 1, 21, 22), + (size_02, guide_size_02, 2, 20, 21), + (size_03, guide_size_03, 3, 19, 20), + (size_04, guide_size_04, 4, 18, 19), + (size_05, guide_size_05, 5, 17, 18), + (size_06, guide_size_06, 6, 16, 17), + (size_07, guide_size_07, 7, 15, 16), + (size_08, guide_size_08, 8, 14, 15), + (size_09, guide_size_09, 9, 13, 14), + (size_10, guide_size_10, 10, 12, 13), + (size_11, guide_size_11, 11, 11, 12), + (size_12, guide_size_12, 12, 10, 13), + (size_13, guide_size_13, 13, 9, 14), + (size_14, guide_size_14, 14, 8, 15), + (size_15, guide_size_15, 15, 7, 16), + (size_16, guide_size_16, 16, 6, 17), + (size_17, guide_size_17, 17, 5, 18), + (size_18, guide_size_18, 18, 4, 19), + (size_19, guide_size_19, 19, 3, 20), + (size_20, guide_size_20, 20, 2, 21), + (size_21, guide_size_21, 21, 1, 22), + (size_22, guide_size_22, 22, 0, 23), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_23, + (size_00, guide_size_00, 0, 23, 24), + (size_01, guide_size_01, 1, 22, 23), + (size_02, guide_size_02, 2, 21, 22), + (size_03, guide_size_03, 3, 20, 21), + (size_04, guide_size_04, 4, 19, 20), + (size_05, guide_size_05, 5, 18, 19), + (size_06, guide_size_06, 6, 17, 18), + (size_07, guide_size_07, 7, 16, 17), + (size_08, guide_size_08, 8, 15, 16), + (size_09, guide_size_09, 9, 14, 15), + (size_10, guide_size_10, 10, 13, 14), + (size_11, guide_size_11, 11, 12, 13), + (size_12, guide_size_12, 12, 11, 13), + (size_13, guide_size_13, 13, 10, 14), + (size_14, guide_size_14, 14, 9, 15), + (size_15, guide_size_15, 15, 8, 16), + (size_16, guide_size_16, 16, 7, 17), + (size_17, guide_size_17, 17, 6, 18), + (size_18, guide_size_18, 18, 5, 19), + (size_19, guide_size_19, 19, 4, 20), + (size_20, guide_size_20, 20, 3, 21), + (size_21, guide_size_21, 21, 2, 22), + (size_22, guide_size_22, 22, 1, 23), + (size_23, guide_size_23, 23, 0, 24), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_24, + (size_00, guide_size_00, 0, 24, 25), + (size_01, guide_size_01, 1, 23, 24), + (size_02, guide_size_02, 2, 22, 23), + (size_03, guide_size_03, 3, 21, 22), + (size_04, guide_size_04, 4, 20, 21), + (size_05, guide_size_05, 5, 19, 20), + (size_06, guide_size_06, 6, 18, 19), + (size_07, guide_size_07, 7, 17, 18), + (size_08, guide_size_08, 8, 16, 17), + (size_09, guide_size_09, 9, 15, 16), + (size_10, guide_size_10, 10, 14, 15), + (size_11, guide_size_11, 11, 13, 14), + (size_12, guide_size_12, 12, 12, 13), + (size_13, guide_size_13, 13, 11, 14), + (size_14, guide_size_14, 14, 10, 15), + (size_15, guide_size_15, 15, 9, 16), + (size_16, guide_size_16, 16, 8, 17), + (size_17, guide_size_17, 17, 7, 18), + (size_18, guide_size_18, 18, 6, 19), + (size_19, guide_size_19, 19, 5, 20), + (size_20, guide_size_20, 20, 4, 21), + (size_21, guide_size_21, 21, 3, 22), + (size_22, guide_size_22, 22, 2, 23), + (size_23, guide_size_23, 23, 1, 24), + (size_24, guide_size_24, 24, 0, 25), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_25, + (size_00, guide_size_00, 0, 25, 26), + (size_01, guide_size_01, 1, 24, 25), + (size_02, guide_size_02, 2, 23, 24), + (size_03, guide_size_03, 3, 22, 23), + (size_04, guide_size_04, 4, 21, 22), + (size_05, guide_size_05, 5, 20, 21), + (size_06, guide_size_06, 6, 19, 20), + (size_07, guide_size_07, 7, 18, 19), + (size_08, guide_size_08, 8, 17, 18), + (size_09, guide_size_09, 9, 16, 17), + (size_10, guide_size_10, 10, 15, 16), + (size_11, guide_size_11, 11, 14, 15), + (size_12, guide_size_12, 12, 13, 14), + (size_13, guide_size_13, 13, 12, 14), + (size_14, guide_size_14, 14, 11, 15), + (size_15, guide_size_15, 15, 10, 16), + (size_16, guide_size_16, 16, 9, 17), + (size_17, guide_size_17, 17, 8, 18), + (size_18, guide_size_18, 18, 7, 19), + (size_19, guide_size_19, 19, 6, 20), + (size_20, guide_size_20, 20, 5, 21), + (size_21, guide_size_21, 21, 4, 22), + (size_22, guide_size_22, 22, 3, 23), + (size_23, guide_size_23, 23, 2, 24), + (size_24, guide_size_24, 24, 1, 25), + (size_25, guide_size_25, 25, 0, 26), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_26, + (size_00, guide_size_00, 0, 26, 27), + (size_01, guide_size_01, 1, 25, 26), + (size_02, guide_size_02, 2, 24, 25), + (size_03, guide_size_03, 3, 23, 24), + (size_04, guide_size_04, 4, 22, 23), + (size_05, guide_size_05, 5, 21, 22), + (size_06, guide_size_06, 6, 20, 21), + (size_07, guide_size_07, 7, 19, 20), + (size_08, guide_size_08, 8, 18, 19), + (size_09, guide_size_09, 9, 17, 18), + (size_10, guide_size_10, 10, 16, 17), + (size_11, guide_size_11, 11, 15, 16), + (size_12, guide_size_12, 12, 14, 15), + (size_13, guide_size_13, 13, 13, 14), + (size_14, guide_size_14, 14, 12, 15), + (size_15, guide_size_15, 15, 11, 16), + (size_16, guide_size_16, 16, 10, 17), + (size_17, guide_size_17, 17, 9, 18), + (size_18, guide_size_18, 18, 8, 19), + (size_19, guide_size_19, 19, 7, 20), + (size_20, guide_size_20, 20, 6, 21), + (size_21, guide_size_21, 21, 5, 22), + (size_22, guide_size_22, 22, 4, 23), + (size_23, guide_size_23, 23, 3, 24), + (size_24, guide_size_24, 24, 2, 25), + (size_25, guide_size_25, 25, 1, 26), + (size_26, guide_size_26, 26, 0, 27), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_27, + (size_00, guide_size_00, 0, 27, 28), + (size_01, guide_size_01, 1, 26, 27), + (size_02, guide_size_02, 2, 25, 26), + (size_03, guide_size_03, 3, 24, 25), + (size_04, guide_size_04, 4, 23, 24), + (size_05, guide_size_05, 5, 22, 23), + (size_06, guide_size_06, 6, 21, 22), + (size_07, guide_size_07, 7, 20, 21), + (size_08, guide_size_08, 8, 19, 20), + (size_09, guide_size_09, 9, 18, 19), + (size_10, guide_size_10, 10, 17, 18), + (size_11, guide_size_11, 11, 16, 17), + (size_12, guide_size_12, 12, 15, 16), + (size_13, guide_size_13, 13, 14, 15), + (size_14, guide_size_14, 14, 13, 15), + (size_15, guide_size_15, 15, 12, 16), + (size_16, guide_size_16, 16, 11, 17), + (size_17, guide_size_17, 17, 10, 18), + (size_18, guide_size_18, 18, 9, 19), + (size_19, guide_size_19, 19, 8, 20), + (size_20, guide_size_20, 20, 7, 21), + (size_21, guide_size_21, 21, 6, 22), + (size_22, guide_size_22, 22, 5, 23), + (size_23, guide_size_23, 23, 4, 24), + (size_24, guide_size_24, 24, 3, 25), + (size_25, guide_size_25, 25, 2, 26), + (size_26, guide_size_26, 26, 1, 27), + (size_27, guide_size_27, 27, 0, 28), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_28, + (size_00, guide_size_00, 0, 28, 29), + (size_01, guide_size_01, 1, 27, 28), + (size_02, guide_size_02, 2, 26, 27), + (size_03, guide_size_03, 3, 25, 26), + (size_04, guide_size_04, 4, 24, 25), + (size_05, guide_size_05, 5, 23, 24), + (size_06, guide_size_06, 6, 22, 23), + (size_07, guide_size_07, 7, 21, 22), + (size_08, guide_size_08, 8, 20, 21), + (size_09, guide_size_09, 9, 19, 20), + (size_10, guide_size_10, 10, 18, 19), + (size_11, guide_size_11, 11, 17, 18), + (size_12, guide_size_12, 12, 16, 17), + (size_13, guide_size_13, 13, 15, 16), + (size_14, guide_size_14, 14, 14, 15), + (size_15, guide_size_15, 15, 13, 16), + (size_16, guide_size_16, 16, 12, 17), + (size_17, guide_size_17, 17, 11, 18), + (size_18, guide_size_18, 18, 10, 19), + (size_19, guide_size_19, 19, 9, 20), + (size_20, guide_size_20, 20, 8, 21), + (size_21, guide_size_21, 21, 7, 22), + (size_22, guide_size_22, 22, 6, 23), + (size_23, guide_size_23, 23, 5, 24), + (size_24, guide_size_24, 24, 4, 25), + (size_25, guide_size_25, 25, 3, 26), + (size_26, guide_size_26, 26, 2, 27), + (size_27, guide_size_27, 27, 1, 28), + (size_28, guide_size_28, 28, 0, 29), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_29, + (size_00, guide_size_00, 0, 29, 30), + (size_01, guide_size_01, 1, 28, 29), + (size_02, guide_size_02, 2, 27, 28), + (size_03, guide_size_03, 3, 26, 27), + (size_04, guide_size_04, 4, 25, 26), + (size_05, guide_size_05, 5, 24, 25), + (size_06, guide_size_06, 6, 23, 24), + (size_07, guide_size_07, 7, 22, 23), + (size_08, guide_size_08, 8, 21, 22), + (size_09, guide_size_09, 9, 20, 21), + (size_10, guide_size_10, 10, 19, 20), + (size_11, guide_size_11, 11, 18, 19), + (size_12, guide_size_12, 12, 17, 18), + (size_13, guide_size_13, 13, 16, 17), + (size_14, guide_size_14, 14, 15, 16), + (size_15, guide_size_15, 15, 14, 16), + (size_16, guide_size_16, 16, 13, 17), + (size_17, guide_size_17, 17, 12, 18), + (size_18, guide_size_18, 18, 11, 19), + (size_19, guide_size_19, 19, 10, 20), + (size_20, guide_size_20, 20, 9, 21), + (size_21, guide_size_21, 21, 8, 22), + (size_22, guide_size_22, 22, 7, 23), + (size_23, guide_size_23, 23, 6, 24), + (size_24, guide_size_24, 24, 5, 25), + (size_25, guide_size_25, 25, 4, 26), + (size_26, guide_size_26, 26, 3, 27), + (size_27, guide_size_27, 27, 2, 28), + (size_28, guide_size_28, 28, 1, 29), + (size_29, guide_size_29, 29, 0, 30), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_30, + (size_00, guide_size_00, 0, 30, 31), + (size_01, guide_size_01, 1, 29, 30), + (size_02, guide_size_02, 2, 28, 29), + (size_03, guide_size_03, 3, 27, 28), + (size_04, guide_size_04, 4, 26, 27), + (size_05, guide_size_05, 5, 25, 26), + (size_06, guide_size_06, 6, 24, 25), + (size_07, guide_size_07, 7, 23, 24), + (size_08, guide_size_08, 8, 22, 23), + (size_09, guide_size_09, 9, 21, 22), + (size_10, guide_size_10, 10, 20, 21), + (size_11, guide_size_11, 11, 19, 20), + (size_12, guide_size_12, 12, 18, 19), + (size_13, guide_size_13, 13, 17, 18), + (size_14, guide_size_14, 14, 16, 17), + (size_15, guide_size_15, 15, 15, 16), + (size_16, guide_size_16, 16, 14, 17), + (size_17, guide_size_17, 17, 13, 18), + (size_18, guide_size_18, 18, 12, 19), + (size_19, guide_size_19, 19, 11, 20), + (size_20, guide_size_20, 20, 10, 21), + (size_21, guide_size_21, 21, 9, 22), + (size_22, guide_size_22, 22, 8, 23), + (size_23, guide_size_23, 23, 7, 24), + (size_24, guide_size_24, 24, 6, 25), + (size_25, guide_size_25, 25, 5, 26), + (size_26, guide_size_26, 26, 4, 27), + (size_27, guide_size_27, 27, 3, 28), + (size_28, guide_size_28, 28, 2, 29), + (size_29, guide_size_29, 29, 1, 30), + (size_30, guide_size_30, 30, 0, 31), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_31, + (size_00, guide_size_00, 0, 31, 32), + (size_01, guide_size_01, 1, 30, 31), + (size_02, guide_size_02, 2, 29, 30), + (size_03, guide_size_03, 3, 28, 29), + (size_04, guide_size_04, 4, 27, 28), + (size_05, guide_size_05, 5, 26, 27), + (size_06, guide_size_06, 6, 25, 26), + (size_07, guide_size_07, 7, 24, 25), + (size_08, guide_size_08, 8, 23, 24), + (size_09, guide_size_09, 9, 22, 23), + (size_10, guide_size_10, 10, 21, 22), + (size_11, guide_size_11, 11, 20, 21), + (size_12, guide_size_12, 12, 19, 20), + (size_13, guide_size_13, 13, 18, 19), + (size_14, guide_size_14, 14, 17, 18), + (size_15, guide_size_15, 15, 16, 17), + (size_16, guide_size_16, 16, 15, 17), + (size_17, guide_size_17, 17, 14, 18), + (size_18, guide_size_18, 18, 13, 19), + (size_19, guide_size_19, 19, 12, 20), + (size_20, guide_size_20, 20, 11, 21), + (size_21, guide_size_21, 21, 10, 22), + (size_22, guide_size_22, 22, 9, 23), + (size_23, guide_size_23, 23, 8, 24), + (size_24, guide_size_24, 24, 7, 25), + (size_25, guide_size_25, 25, 6, 26), + (size_26, guide_size_26, 26, 5, 27), + (size_27, guide_size_27, 27, 4, 28), + (size_28, guide_size_28, 28, 3, 29), + (size_29, guide_size_29, 29, 2, 30), + (size_30, guide_size_30, 30, 1, 31), + (size_31, guide_size_31, 31, 0, 32), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_32, + (size_00, guide_size_00, 0, 32, 33), + (size_01, guide_size_01, 1, 31, 32), + (size_02, guide_size_02, 2, 30, 31), + (size_03, guide_size_03, 3, 29, 30), + (size_04, guide_size_04, 4, 28, 29), + (size_05, guide_size_05, 5, 27, 28), + (size_06, guide_size_06, 6, 26, 27), + (size_07, guide_size_07, 7, 25, 26), + (size_08, guide_size_08, 8, 24, 25), + (size_09, guide_size_09, 9, 23, 24), + (size_10, guide_size_10, 10, 22, 23), + (size_11, guide_size_11, 11, 21, 22), + (size_12, guide_size_12, 12, 20, 21), + (size_13, guide_size_13, 13, 19, 20), + (size_14, guide_size_14, 14, 18, 19), + (size_15, guide_size_15, 15, 17, 18), + (size_16, guide_size_16, 16, 16, 17), + (size_17, guide_size_17, 17, 15, 18), + (size_18, guide_size_18, 18, 14, 19), + (size_19, guide_size_19, 19, 13, 20), + (size_20, guide_size_20, 20, 12, 21), + (size_21, guide_size_21, 21, 11, 22), + (size_22, guide_size_22, 22, 10, 23), + (size_23, guide_size_23, 23, 9, 24), + (size_24, guide_size_24, 24, 8, 25), + (size_25, guide_size_25, 25, 7, 26), + (size_26, guide_size_26, 26, 6, 27), + (size_27, guide_size_27, 27, 5, 28), + (size_28, guide_size_28, 28, 4, 29), + (size_29, guide_size_29, 29, 3, 30), + (size_30, guide_size_30, 30, 2, 31), + (size_31, guide_size_31, 31, 1, 32), + (size_32, guide_size_32, 32, 0, 33), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_33, + (size_00, guide_size_00, 0, 33, 34), + (size_01, guide_size_01, 1, 32, 33), + (size_02, guide_size_02, 2, 31, 32), + (size_03, guide_size_03, 3, 30, 31), + (size_04, guide_size_04, 4, 29, 30), + (size_05, guide_size_05, 5, 28, 29), + (size_06, guide_size_06, 6, 27, 28), + (size_07, guide_size_07, 7, 26, 27), + (size_08, guide_size_08, 8, 25, 26), + (size_09, guide_size_09, 9, 24, 25), + (size_10, guide_size_10, 10, 23, 24), + (size_11, guide_size_11, 11, 22, 23), + (size_12, guide_size_12, 12, 21, 22), + (size_13, guide_size_13, 13, 20, 21), + (size_14, guide_size_14, 14, 19, 20), + (size_15, guide_size_15, 15, 18, 19), + (size_16, guide_size_16, 16, 17, 18), + (size_17, guide_size_17, 17, 16, 18), + (size_18, guide_size_18, 18, 15, 19), + (size_19, guide_size_19, 19, 14, 20), + (size_20, guide_size_20, 20, 13, 21), + (size_21, guide_size_21, 21, 12, 22), + (size_22, guide_size_22, 22, 11, 23), + (size_23, guide_size_23, 23, 10, 24), + (size_24, guide_size_24, 24, 9, 25), + (size_25, guide_size_25, 25, 8, 26), + (size_26, guide_size_26, 26, 7, 27), + (size_27, guide_size_27, 27, 6, 28), + (size_28, guide_size_28, 28, 5, 29), + (size_29, guide_size_29, 29, 4, 30), + (size_30, guide_size_30, 30, 3, 31), + (size_31, guide_size_31, 31, 2, 32), + (size_32, guide_size_32, 32, 1, 33), + (size_33, guide_size_33, 33, 0, 34), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_34, + (size_00, guide_size_00, 0, 34, 35), + (size_01, guide_size_01, 1, 33, 34), + (size_02, guide_size_02, 2, 32, 33), + (size_03, guide_size_03, 3, 31, 32), + (size_04, guide_size_04, 4, 30, 31), + (size_05, guide_size_05, 5, 29, 30), + (size_06, guide_size_06, 6, 28, 29), + (size_07, guide_size_07, 7, 27, 28), + (size_08, guide_size_08, 8, 26, 27), + (size_09, guide_size_09, 9, 25, 26), + (size_10, guide_size_10, 10, 24, 25), + (size_11, guide_size_11, 11, 23, 24), + (size_12, guide_size_12, 12, 22, 23), + (size_13, guide_size_13, 13, 21, 22), + (size_14, guide_size_14, 14, 20, 21), + (size_15, guide_size_15, 15, 19, 20), + (size_16, guide_size_16, 16, 18, 19), + (size_17, guide_size_17, 17, 17, 18), + (size_18, guide_size_18, 18, 16, 19), + (size_19, guide_size_19, 19, 15, 20), + (size_20, guide_size_20, 20, 14, 21), + (size_21, guide_size_21, 21, 13, 22), + (size_22, guide_size_22, 22, 12, 23), + (size_23, guide_size_23, 23, 11, 24), + (size_24, guide_size_24, 24, 10, 25), + (size_25, guide_size_25, 25, 9, 26), + (size_26, guide_size_26, 26, 8, 27), + (size_27, guide_size_27, 27, 7, 28), + (size_28, guide_size_28, 28, 6, 29), + (size_29, guide_size_29, 29, 5, 30), + (size_30, guide_size_30, 30, 4, 31), + (size_31, guide_size_31, 31, 3, 32), + (size_32, guide_size_32, 32, 2, 33), + (size_33, guide_size_33, 33, 1, 34), + (size_34, guide_size_34, 34, 0, 35), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_35, + (size_00, guide_size_00, 0, 35, 36), + (size_01, guide_size_01, 1, 34, 35), + (size_02, guide_size_02, 2, 33, 34), + (size_03, guide_size_03, 3, 32, 33), + (size_04, guide_size_04, 4, 31, 32), + (size_05, guide_size_05, 5, 30, 31), + (size_06, guide_size_06, 6, 29, 30), + (size_07, guide_size_07, 7, 28, 29), + (size_08, guide_size_08, 8, 27, 28), + (size_09, guide_size_09, 9, 26, 27), + (size_10, guide_size_10, 10, 25, 26), + (size_11, guide_size_11, 11, 24, 25), + (size_12, guide_size_12, 12, 23, 24), + (size_13, guide_size_13, 13, 22, 23), + (size_14, guide_size_14, 14, 21, 22), + (size_15, guide_size_15, 15, 20, 21), + (size_16, guide_size_16, 16, 19, 20), + (size_17, guide_size_17, 17, 18, 19), + (size_18, guide_size_18, 18, 17, 19), + (size_19, guide_size_19, 19, 16, 20), + (size_20, guide_size_20, 20, 15, 21), + (size_21, guide_size_21, 21, 14, 22), + (size_22, guide_size_22, 22, 13, 23), + (size_23, guide_size_23, 23, 12, 24), + (size_24, guide_size_24, 24, 11, 25), + (size_25, guide_size_25, 25, 10, 26), + (size_26, guide_size_26, 26, 9, 27), + (size_27, guide_size_27, 27, 8, 28), + (size_28, guide_size_28, 28, 7, 29), + (size_29, guide_size_29, 29, 6, 30), + (size_30, guide_size_30, 30, 5, 31), + (size_31, guide_size_31, 31, 4, 32), + (size_32, guide_size_32, 32, 3, 33), + (size_33, guide_size_33, 33, 2, 34), + (size_34, guide_size_34, 34, 1, 35), + (size_35, guide_size_35, 35, 0, 36), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_36, + (size_00, guide_size_00, 0, 36, 37), + (size_01, guide_size_01, 1, 35, 36), + (size_02, guide_size_02, 2, 34, 35), + (size_03, guide_size_03, 3, 33, 34), + (size_04, guide_size_04, 4, 32, 33), + (size_05, guide_size_05, 5, 31, 32), + (size_06, guide_size_06, 6, 30, 31), + (size_07, guide_size_07, 7, 29, 30), + (size_08, guide_size_08, 8, 28, 29), + (size_09, guide_size_09, 9, 27, 28), + (size_10, guide_size_10, 10, 26, 27), + (size_11, guide_size_11, 11, 25, 26), + (size_12, guide_size_12, 12, 24, 25), + (size_13, guide_size_13, 13, 23, 24), + (size_14, guide_size_14, 14, 22, 23), + (size_15, guide_size_15, 15, 21, 22), + (size_16, guide_size_16, 16, 20, 21), + (size_17, guide_size_17, 17, 19, 20), + (size_18, guide_size_18, 18, 18, 19), + (size_19, guide_size_19, 19, 17, 20), + (size_20, guide_size_20, 20, 16, 21), + (size_21, guide_size_21, 21, 15, 22), + (size_22, guide_size_22, 22, 14, 23), + (size_23, guide_size_23, 23, 13, 24), + (size_24, guide_size_24, 24, 12, 25), + (size_25, guide_size_25, 25, 11, 26), + (size_26, guide_size_26, 26, 10, 27), + (size_27, guide_size_27, 27, 9, 28), + (size_28, guide_size_28, 28, 8, 29), + (size_29, guide_size_29, 29, 7, 30), + (size_30, guide_size_30, 30, 6, 31), + (size_31, guide_size_31, 31, 5, 32), + (size_32, guide_size_32, 32, 4, 33), + (size_33, guide_size_33, 33, 3, 34), + (size_34, guide_size_34, 34, 2, 35), + (size_35, guide_size_35, 35, 1, 36), + (size_36, guide_size_36, 36, 0, 37), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_37, + (size_00, guide_size_00, 0, 37, 38), + (size_01, guide_size_01, 1, 36, 37), + (size_02, guide_size_02, 2, 35, 36), + (size_03, guide_size_03, 3, 34, 35), + (size_04, guide_size_04, 4, 33, 34), + (size_05, guide_size_05, 5, 32, 33), + (size_06, guide_size_06, 6, 31, 32), + (size_07, guide_size_07, 7, 30, 31), + (size_08, guide_size_08, 8, 29, 30), + (size_09, guide_size_09, 9, 28, 29), + (size_10, guide_size_10, 10, 27, 28), + (size_11, guide_size_11, 11, 26, 27), + (size_12, guide_size_12, 12, 25, 26), + (size_13, guide_size_13, 13, 24, 25), + (size_14, guide_size_14, 14, 23, 24), + (size_15, guide_size_15, 15, 22, 23), + (size_16, guide_size_16, 16, 21, 22), + (size_17, guide_size_17, 17, 20, 21), + (size_18, guide_size_18, 18, 19, 20), + (size_19, guide_size_19, 19, 18, 20), + (size_20, guide_size_20, 20, 17, 21), + (size_21, guide_size_21, 21, 16, 22), + (size_22, guide_size_22, 22, 15, 23), + (size_23, guide_size_23, 23, 14, 24), + (size_24, guide_size_24, 24, 13, 25), + (size_25, guide_size_25, 25, 12, 26), + (size_26, guide_size_26, 26, 11, 27), + (size_27, guide_size_27, 27, 10, 28), + (size_28, guide_size_28, 28, 9, 29), + (size_29, guide_size_29, 29, 8, 30), + (size_30, guide_size_30, 30, 7, 31), + (size_31, guide_size_31, 31, 6, 32), + (size_32, guide_size_32, 32, 5, 33), + (size_33, guide_size_33, 33, 4, 34), + (size_34, guide_size_34, 34, 3, 35), + (size_35, guide_size_35, 35, 2, 36), + (size_36, guide_size_36, 36, 1, 37), + (size_37, guide_size_37, 37, 0, 38), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_38, + (size_00, guide_size_00, 0, 38, 39), + (size_01, guide_size_01, 1, 37, 38), + (size_02, guide_size_02, 2, 36, 37), + (size_03, guide_size_03, 3, 35, 36), + (size_04, guide_size_04, 4, 34, 35), + (size_05, guide_size_05, 5, 33, 34), + (size_06, guide_size_06, 6, 32, 33), + (size_07, guide_size_07, 7, 31, 32), + (size_08, guide_size_08, 8, 30, 31), + (size_09, guide_size_09, 9, 29, 30), + (size_10, guide_size_10, 10, 28, 29), + (size_11, guide_size_11, 11, 27, 28), + (size_12, guide_size_12, 12, 26, 27), + (size_13, guide_size_13, 13, 25, 26), + (size_14, guide_size_14, 14, 24, 25), + (size_15, guide_size_15, 15, 23, 24), + (size_16, guide_size_16, 16, 22, 23), + (size_17, guide_size_17, 17, 21, 22), + (size_18, guide_size_18, 18, 20, 21), + (size_19, guide_size_19, 19, 19, 20), + (size_20, guide_size_20, 20, 18, 21), + (size_21, guide_size_21, 21, 17, 22), + (size_22, guide_size_22, 22, 16, 23), + (size_23, guide_size_23, 23, 15, 24), + (size_24, guide_size_24, 24, 14, 25), + (size_25, guide_size_25, 25, 13, 26), + (size_26, guide_size_26, 26, 12, 27), + (size_27, guide_size_27, 27, 11, 28), + (size_28, guide_size_28, 28, 10, 29), + (size_29, guide_size_29, 29, 9, 30), + (size_30, guide_size_30, 30, 8, 31), + (size_31, guide_size_31, 31, 7, 32), + (size_32, guide_size_32, 32, 6, 33), + (size_33, guide_size_33, 33, 5, 34), + (size_34, guide_size_34, 34, 4, 35), + (size_35, guide_size_35, 35, 3, 36), + (size_36, guide_size_36, 36, 2, 37), + (size_37, guide_size_37, 37, 1, 38), + (size_38, guide_size_38, 38, 0, 39), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_39, + (size_00, guide_size_00, 0, 39, 40), + (size_01, guide_size_01, 1, 38, 39), + (size_02, guide_size_02, 2, 37, 38), + (size_03, guide_size_03, 3, 36, 37), + (size_04, guide_size_04, 4, 35, 36), + (size_05, guide_size_05, 5, 34, 35), + (size_06, guide_size_06, 6, 33, 34), + (size_07, guide_size_07, 7, 32, 33), + (size_08, guide_size_08, 8, 31, 32), + (size_09, guide_size_09, 9, 30, 31), + (size_10, guide_size_10, 10, 29, 30), + (size_11, guide_size_11, 11, 28, 29), + (size_12, guide_size_12, 12, 27, 28), + (size_13, guide_size_13, 13, 26, 27), + (size_14, guide_size_14, 14, 25, 26), + (size_15, guide_size_15, 15, 24, 25), + (size_16, guide_size_16, 16, 23, 24), + (size_17, guide_size_17, 17, 22, 23), + (size_18, guide_size_18, 18, 21, 22), + (size_19, guide_size_19, 19, 20, 21), + (size_20, guide_size_20, 20, 19, 21), + (size_21, guide_size_21, 21, 18, 22), + (size_22, guide_size_22, 22, 17, 23), + (size_23, guide_size_23, 23, 16, 24), + (size_24, guide_size_24, 24, 15, 25), + (size_25, guide_size_25, 25, 14, 26), + (size_26, guide_size_26, 26, 13, 27), + (size_27, guide_size_27, 27, 12, 28), + (size_28, guide_size_28, 28, 11, 29), + (size_29, guide_size_29, 29, 10, 30), + (size_30, guide_size_30, 30, 9, 31), + (size_31, guide_size_31, 31, 8, 32), + (size_32, guide_size_32, 32, 7, 33), + (size_33, guide_size_33, 33, 6, 34), + (size_34, guide_size_34, 34, 5, 35), + (size_35, guide_size_35, 35, 4, 36), + (size_36, guide_size_36, 36, 3, 37), + (size_37, guide_size_37, 37, 2, 38), + (size_38, guide_size_38, 38, 1, 39), + (size_39, guide_size_39, 39, 0, 40), + ); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_shift_width!( + shift_width_40, + (size_01, guide_size_01, 1, 39, 40), + (size_02, guide_size_02, 2, 38, 39), + (size_03, guide_size_03, 3, 37, 38), + (size_04, guide_size_04, 4, 36, 37), + (size_05, guide_size_05, 5, 35, 36), + (size_06, guide_size_06, 6, 34, 35), + (size_07, guide_size_07, 7, 33, 34), + (size_08, guide_size_08, 8, 32, 33), + (size_09, guide_size_09, 9, 31, 32), + (size_10, guide_size_10, 10, 30, 31), + (size_11, guide_size_11, 11, 29, 30), + (size_12, guide_size_12, 12, 28, 29), + (size_13, guide_size_13, 13, 27, 28), + (size_14, guide_size_14, 14, 26, 27), + (size_15, guide_size_15, 15, 25, 26), + (size_16, guide_size_16, 16, 24, 25), + (size_17, guide_size_17, 17, 23, 24), + (size_18, guide_size_18, 18, 22, 23), + (size_19, guide_size_19, 19, 21, 22), + (size_20, guide_size_20, 20, 20, 21), + (size_21, guide_size_21, 21, 19, 22), + (size_22, guide_size_22, 22, 18, 23), + (size_23, guide_size_23, 23, 17, 24), + (size_24, guide_size_24, 24, 16, 25), + (size_25, guide_size_25, 25, 15, 26), + (size_26, guide_size_26, 26, 14, 27), + (size_27, guide_size_27, 27, 13, 28), + (size_28, guide_size_28, 28, 12, 29), + (size_29, guide_size_29, 29, 11, 30), + (size_30, guide_size_30, 30, 10, 31), + (size_31, guide_size_31, 31, 9, 32), + (size_32, guide_size_32, 32, 8, 33), + (size_33, guide_size_33, 33, 7, 34), + (size_34, guide_size_34, 34, 6, 35), + (size_35, guide_size_35, 35, 5, 36), + (size_36, guide_size_36, 36, 4, 37), + (size_37, guide_size_37, 37, 3, 38), + (size_38, guide_size_38, 38, 2, 39), + (size_39, guide_size_39, 39, 1, 40), + (size_40, guide_size_40, 40, 0, 41), + ); + + // The cached products run the real multiplication and the model in + // lockstep at each `mul_digits_step`. A step multiplies digit `i` of the + // first factor by digit `j` of the second factor, and the log keeps one + // entry for each position `(i, j)`. The positions are constants in each + // unrolled loop, so no log index is symbolic, also when a zero digit + // skips a row. The real product runs first: its step at `(i, j)` records + // the arguments and returns arbitrary outputs. The model runs second: its + // step at `(i, j)` must pass the arguments of the real step at `(i, j)`, + // and it returns the outputs of that step. Thus the proof holds for every + // function in place of `mul_digits_step`, and the real one is one of them. + // Each position is recorded once and replayed once, and the model replays + // only recorded positions, so equal counts mean the same set of steps. + // The log replaces a comparison of two multiplier circuits. + struct Lockstep { + replay: bool, + recorded: usize, + replayed: usize, + recorded_at: [[bool; KANI_LIMBS]; KANI_LIMBS], + replayed_at: [[bool; KANI_LIMBS]; KANI_LIMBS], + arguments: [[(Digit, Digit, Digit, Digit); KANI_LIMBS]; KANI_LIMBS], + outputs: [[(Digit, Digit); KANI_LIMBS]; KANI_LIMBS], + } + + static mut LOCKSTEP: Lockstep = Lockstep { + replay: false, + recorded: 0, + replayed: 0, + recorded_at: [[false; KANI_LIMBS]; KANI_LIMBS], + replayed_at: [[false; KANI_LIMBS]; KANI_LIMBS], + arguments: [[(0, 0, 0, 0); KANI_LIMBS]; KANI_LIMBS], + outputs: [[(0, 0); KANI_LIMBS]; KANI_LIMBS], + }; + + fn lockstep() -> &'static mut Lockstep { + // SAFETY: a harness runs on one thread, and each caller stops using + // the reference before the next call to this function. + unsafe { &mut *(&raw mut LOCKSTEP) } + } + + fn mul_digits_step_lockstep( + i: usize, + j: usize, + a: Digit, + b: Digit, + c: Digit, + carry: Digit, + ) -> (Digit, Digit) { + assert!(i < KANI_LIMBS && j < KANI_LIMBS); + let state = lockstep(); + let arguments = (a, b, c, carry); + if state.replay { + assert!(state.recorded_at[i][j] && !state.replayed_at[i][j]); + assert!(state.arguments[i][j] == arguments); + state.replayed_at[i][j] = true; + state.replayed += 1; + state.outputs[i][j] + } else { + assert!(!state.recorded_at[i][j]); + let outputs: (Digit, Digit) = (kani::any(), kani::any()); + state.arguments[i][j] = arguments; + state.outputs[i][j] = outputs; + state.recorded_at[i][j] = true; + state.recorded += 1; + outputs + } + } + + fn check_cached_mul_digits_model_agrees(digits: &[Digit]) { + assert!(SIZE + digits.len() <= KANI_LIMBS); + let mut source = Big::kani_with_arbitrary_limbs(SIZE); + *source.kani_limbs_mut() = kani::any(); + let mut actual = source.clone(); + let mut modeled = source; + let actual_start = &mut actual as *mut Big; + let modeled_start = &mut modeled as *mut Big; + assert!(actual.mul_digits(digits) as *mut Big == actual_start); + lockstep().replay = true; + assert!(modeled.kani_mul_digits_model(digits) as *mut Big == modeled_start); + let state = lockstep(); + assert!(state.replayed == state.recorded); + assert!(actual.kani_same_storage(&modeled)); + if SIZE == 0 { + kani::cover(true, "cached multiplication accepts empty storage"); + } + if SIZE + digits.len() == KANI_LIMBS { + kani::cover(true, "cached multiplication accepts the largest model input"); + kani::cover(actual.kani_size() == KANI_LIMBS, "cached multiplication appends a carry"); + } + } + + macro_rules! check_cached_mul_sizes { + ($digits:ident; $($name:ident => $size:literal),+ $(,)?) => {$( + #[kani::proof] + #[kani::unwind(41)] + // The patched CBMC does not link CaDiCaL, and MiniSAT times out on these products. + #[kani::solver(kissat)] + #[kani::stub(Big::mul_digits_step, mul_digits_step_lockstep)] + fn $name() { + check_cached_mul_digits_model_agrees::<$size>(&$digits); } - if mant >= scale2 { - mant.sub(&scale2); - d += 2; + )+}; + } + + #[cfg(target_pointer_width = "64")] + macro_rules! check_cached_mul_extra_sizes { + (POW5TO16) => { check_cached_mul_sizes!(POW5TO16; size_11 => 11, size_12 => 12, size_13 => 13, size_14 => 14, size_15 => 15, size_16 => 16, size_17 => 17, size_18 => 18, size_19 => 19); }; + (POW5TO32) => { check_cached_mul_sizes!(POW5TO32; size_11 => 11, size_12 => 12, size_13 => 13, size_14 => 14, size_15 => 15, size_16 => 16, size_17 => 17, size_18 => 18); }; + (POW5TO64) => { check_cached_mul_sizes!(POW5TO64; size_11 => 11, size_12 => 12, size_13 => 13, size_14 => 14, size_15 => 15, size_16 => 16, size_17 => 17); }; + (POW5TO128) => { check_cached_mul_sizes!(POW5TO128; size_11 => 11, size_12 => 12, size_13 => 13, size_14 => 14, size_15 => 15); }; + (POW5TO256) => { }; + } + + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + macro_rules! check_cached_mul_extra_sizes { + (POW5TO16) => { check_cached_mul_sizes!(POW5TO16; size_22 => 22, size_23 => 23, size_24 => 24, size_25 => 25, size_26 => 26, size_27 => 27, size_28 => 28, size_29 => 29, size_30 => 30, size_31 => 31, size_32 => 32, size_33 => 33, size_34 => 34, size_35 => 35, size_36 => 36, size_37 => 37, size_38 => 38); }; + (POW5TO32) => { check_cached_mul_sizes!(POW5TO32; size_22 => 22, size_23 => 23, size_24 => 24, size_25 => 25, size_26 => 26, size_27 => 27, size_28 => 28, size_29 => 29, size_30 => 30, size_31 => 31, size_32 => 32, size_33 => 33, size_34 => 34, size_35 => 35, size_36 => 36, size_37 => 37); }; + (POW5TO64) => { check_cached_mul_sizes!(POW5TO64; size_22 => 22, size_23 => 23, size_24 => 24, size_25 => 25, size_26 => 26, size_27 => 27, size_28 => 28, size_29 => 29, size_30 => 30, size_31 => 31, size_32 => 32, size_33 => 33, size_34 => 34, size_35 => 35); }; + (POW5TO128) => { check_cached_mul_sizes!(POW5TO128; size_22 => 22, size_23 => 23, size_24 => 24, size_25 => 25, size_26 => 26, size_27 => 27, size_28 => 28, size_29 => 29, size_30 => 30); }; + (POW5TO256) => { }; + } + + macro_rules! check_cached_mul_digits { + ($($group:ident => $digits:ident),+ $(,)?) => {$( + mod $group { + use super::*; + + check_cached_mul_sizes!($digits; size_00 => 0, size_01 => 1, size_02 => 2, size_03 => 3, size_04 => 4, size_05 => 5, size_06 => 6, size_07 => 7, size_08 => 8, size_09 => 9, size_10 => 10); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_cached_mul_sizes!($digits; size_11 => 11, size_12 => 12, size_13 => 13, size_14 => 14, size_15 => 15, size_16 => 16, size_17 => 17, size_18 => 18, size_19 => 19, size_20 => 20, size_21 => 21); + check_cached_mul_extra_sizes!($digits); } - if mant >= scale { - mant.sub(&scale); - d += 1; + )+}; + } + + check_cached_mul_digits!( + cached_mul_pow5_16 => POW5TO16, + cached_mul_pow5_32 => POW5TO32, + cached_mul_pow5_64 => POW5TO64, + cached_mul_pow5_128 => POW5TO128, + cached_mul_pow5_256 => POW5TO256, + ); + + fn check_mul_small_model_agrees() { + assert!(SIZE < KANI_LIMBS); + let source = Big::kani_with_arbitrary_limbs(SIZE); + let multiplier: Digit = kani::any(); + kani::cover(source.kani_size() == 0, "multiplication accepts empty storage"); + kani::cover( + source.kani_size() == KANI_LIMBS - 1, + "multiplication accepts the largest model input", + ); + kani::cover(multiplier == 0, "multiplication accepts zero"); + kani::cover(multiplier == Digit::MAX, "multiplication accepts the largest digit"); + let mut actual = source.clone(); + let mut modeled = source; + let actual_start = &mut actual as *mut Big; + let modeled_start = &mut modeled as *mut Big; + assert!(actual.mul_small(multiplier) as *mut Big == actual_start); + assert!(modeled.kani_mul_small_model(multiplier) as *mut Big == modeled_start); + assert!(actual.kani_same_storage(&modeled)); + kani::cover(actual.kani_size() == KANI_LIMBS, "multiplication can append a carry limb"); + } + + macro_rules! check_mul_size { + ($name:ident, $size:literal) => { + #[kani::proof] + #[kani::unwind(41)] + #[cfg_attr( + target_pointer_width = "64", + kani::stub(u64::carrying_mul, carrying_mul_model) + )] + #[cfg_attr( + target_pointer_width = "64", + kani::stub(u64::carrying_mul_add, carrying_mul_add_model) + )] + #[cfg_attr( + any(target_pointer_width = "16", target_pointer_width = "32"), + kani::stub(u32::carrying_mul, carrying_mul_model) + )] + #[cfg_attr( + any(target_pointer_width = "16", target_pointer_width = "32"), + kani::stub(u32::carrying_mul_add, carrying_mul_add_model) + )] + #[kani::solver(z3)] + fn $name() { + check_mul_small_model_agrees::<$size>(); } - debug_assert!(mant < scale); - debug_assert!(d < 10); - buf[i] = MaybeUninit::new(b'0' + d); - mant.mul_small(10); + }; + } + + // Together these cases retain every storage size accepted by the model. + check_mul_size!(check_mul_small_model_agrees_00, 0); + check_mul_size!(check_mul_small_model_agrees_01, 1); + check_mul_size!(check_mul_small_model_agrees_02, 2); + check_mul_size!(check_mul_small_model_agrees_03, 3); + check_mul_size!(check_mul_small_model_agrees_04, 4); + check_mul_size!(check_mul_small_model_agrees_05, 5); + check_mul_size!(check_mul_small_model_agrees_06, 6); + check_mul_size!(check_mul_small_model_agrees_07, 7); + check_mul_size!(check_mul_small_model_agrees_08, 8); + check_mul_size!(check_mul_small_model_agrees_09, 9); + check_mul_size!(check_mul_small_model_agrees_10, 10); + check_mul_size!(check_mul_small_model_agrees_11, 11); + check_mul_size!(check_mul_small_model_agrees_12, 12); + check_mul_size!(check_mul_small_model_agrees_13, 13); + check_mul_size!(check_mul_small_model_agrees_14, 14); + check_mul_size!(check_mul_small_model_agrees_15, 15); + check_mul_size!(check_mul_small_model_agrees_16, 16); + check_mul_size!(check_mul_small_model_agrees_17, 17); + check_mul_size!(check_mul_small_model_agrees_18, 18); + check_mul_size!(check_mul_small_model_agrees_19, 19); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_20, 20); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_21, 21); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_22, 22); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_23, 23); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_24, 24); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_25, 25); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_26, 26); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_27, 27); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_28, 28); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_29, 29); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_30, 30); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_31, 31); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_32, 32); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_33, 33); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_34, 34); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_35, 35); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_36, 36); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_37, 37); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_38, 38); + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + check_mul_size!(check_mul_small_model_agrees_39, 39); + + // Prove the exact two-limb result once before composing longer carry chains. + #[kani::ensures(|result| { + (((result.1 as WideDigit) << Digit::BITS) | result.0 as WideDigit) + == (digit as WideDigit) + .wrapping_mul(multiplier as WideDigit) + .wrapping_add(carry as WideDigit) + .wrapping_add(addend as WideDigit) + })] + fn carrying_mul_add_contract( + digit: Digit, + multiplier: Digit, + carry: Digit, + addend: Digit, + ) -> (Digit, Digit) { + digit.carrying_mul_add(multiplier, carry, addend) + } + + // The standalone contract harness also proves this deterministic model + // equal to the real primitive for all four digit inputs. Sharing its + // expression on both sides preserves equal carry prefixes syntactically. + // The largest product plus two maximum digits fills exactly two limbs. + fn carrying_mul_add_model( + digit: Digit, + multiplier: Digit, + carry: Digit, + addend: Digit, + ) -> (Digit, Digit) { + let sum = (digit as WideDigit) + .wrapping_mul(multiplier as WideDigit) + .wrapping_add(carry as WideDigit) + .wrapping_add(addend as WideDigit); + (sum as Digit, (sum >> Digit::BITS) as Digit) + } + + pub(crate) fn carrying_mul_model( + digit: Digit, + multiplier: Digit, + carry: Digit, + ) -> (Digit, Digit) { + carrying_mul_add_model(digit, multiplier, carry, 0) + } + + #[kani::proof_for_contract(carrying_mul_add_contract)] + #[kani::solver(z3)] + fn check_carrying_mul_add_contract() { + let digit: Digit = kani::any(); + let multiplier: Digit = kani::any(); + let carry: Digit = kani::any(); + let addend: Digit = kani::any(); + let result = carrying_mul_add_contract(digit, multiplier, carry, addend); + let modeled = carrying_mul_add_model(digit, multiplier, carry, addend); + // First check the complete wide value. Packing two digit limbs is + // injective, so the subsequent limb comparison follows directly. + let result_bits = ((result.1 as WideDigit) << Digit::BITS) | result.0 as WideDigit; + let modeled_bits = ((modeled.1 as WideDigit) << Digit::BITS) | modeled.0 as WideDigit; + assert_eq!(result_bits, modeled_bits); + assert_eq!(result, modeled); + kani::cover( + digit == 0 && multiplier == 0 && addend == 0 && carry == 0, + "limb multiplication accepts all zero inputs", + ); + kani::cover( + digit == Digit::MAX + && multiplier == Digit::MAX + && addend == Digit::MAX + && carry == Digit::MAX, + "limb multiplication accepts all maximum inputs", + ); + kani::cover( + result == (Digit::MAX, Digit::MAX), + "limb multiplication reaches the full two-limb result", + ); + } + + // Bigint division needs the remainder bound to justify the next limb's + // division. Prove that scalar obligation separately; the storage contract + // below does not depend on the quotient's numeric value. + #[kani::requires(borrow < divisor)] + #[kani::ensures(|result| result.1 < divisor)] + fn div_rem_digit_contract(digit: Digit, divisor: Digit, borrow: Digit) -> (Digit, Digit) { + ::full_div_rem(digit, divisor, borrow) + } + + // Keep ordinary stub substitution separate from verified contract dispatch. + fn stub_div_rem_digit(digit: Digit, divisor: Digit, borrow: Digit) -> (Digit, Digit) { + div_rem_digit_contract(digit, divisor, borrow) + } + + #[kani::proof_for_contract(div_rem_digit_contract)] + #[kani::solver(kissat)] + fn check_div_rem_digit_contract() { + let digit: Digit = kani::any(); + let divisor: Digit = kani::any(); + let borrow: Digit = kani::any(); + let (_, remainder) = div_rem_digit_contract(digit, divisor, borrow); + kani::cover(divisor == 1, "limb division accepts the unit divisor"); + kani::cover(divisor == Digit::MAX, "limb division accepts the largest divisor"); + kani::cover(borrow == divisor - 1, "limb division accepts the largest borrow"); + kani::cover(remainder == 0, "limb division can have no remainder"); + } + + // Verify the limb loop separately from the outer power-of-ten loop. + // Only limbs are writable, so stubs retain the caller's size expression. + #[kani::requires(divisor > 0 && value.kani_valid_storage())] + #[kani::ensures(|result| { + value.kani_valid_storage() + && value.kani_size() == old(value.kani_size()) + && *result < divisor + })] + #[kani::modifies(value.kani_limbs_mut())] + fn div_rem_small_contract(value: &mut Big, divisor: Digit) -> Digit { + value.div_rem_small(divisor).1 + } + + fn stub_div_rem_small(value: &mut Big, divisor: Digit) -> (&mut Big, Digit) { + let remainder = div_rem_small_contract(value, divisor); + (value, remainder) + } + + #[kani::proof_for_contract(div_rem_small_contract)] + #[cfg_attr(target_pointer_width = "64", kani::stub( + ::full_div_rem, + stub_div_rem_digit + ))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub( + ::full_div_rem, + stub_div_rem_digit + ))] + #[kani::stub_verified(div_rem_digit_contract)] + #[kani::unwind(41)] + #[kani::solver(kissat)] + fn check_div_rem_small_contract() { + let mut value = Big::kani_any_valid(); + let divisor: Digit = kani::any(); + let _ = div_rem_small_contract(&mut value, divisor); + kani::cover(divisor == 1, "bigint division accepts the unit divisor"); + kani::cover(divisor == Digit::MAX, "bigint division accepts the largest divisor"); + kani::cover(value.kani_size() == 0, "bigint division accepts empty zero storage"); + kani::cover(value.kani_size() == KANI_LIMBS, "bigint division accepts all limbs"); + } + + // Division preserves the bigint's allocated prefix and unused zero limbs. + // Its numeric result is overapproximated; the generator still performs the + // real addition, exact comparison semantics, and subsequent digit extraction. + // Read size without constructing a slice: old expressions must not panic. + #[kani::requires(n <= PROOF_BUFLEN && value.kani_valid_storage())] + #[kani::ensures(|_| { + value.kani_valid_storage() && value.kani_size() == old(value.kani_size()) + })] + #[kani::modifies(value.kani_limbs_mut())] + fn div_2pow10_contract(value: &mut Big, n: usize) { + let _ = div_2pow10(value, n); + } + + fn stub_div_2pow10(value: &mut Big, n: usize) -> &mut Big { + div_2pow10_contract(value, n); + value + } + + #[kani::proof_for_contract(div_2pow10_contract)] + #[kani::stub(Big::div_rem_small, stub_div_rem_small)] + #[kani::stub_verified(div_rem_small_contract)] + #[kani::unwind(5)] + #[kani::solver(kissat)] + fn check_div_2pow10_contract() { + let mut value = Big::kani_any_valid(); + let n: usize = kani::any(); + div_2pow10_contract(&mut value, n); + kani::cover(n == 0, "division accepts the minimum power"); + kani::cover(n == PROOF_BUFLEN, "division accepts the maximum proof power"); + kani::cover(value.kani_size() == 0, "division accepts empty zero storage"); + kani::cover(value.kani_size() == KANI_LIMBS, "division accepts all bigint limbs"); + } + + // Dragon decodes every finite value with `-1076 <= exp <= 970`. This is the + // bound of `checked_f64_limb_count` for one exponent: the value bits, + // sixteen bits for decimal scaling, and one carry limb. + fn dragon_limb_bound(exp: i16) -> usize { + let exp = i32::from(exp); + let value_bits = (-exp).max(exp + 55).max(64) as usize; + (value_bits + 16).div_ceil(Digit::BITS as usize) + 1 + } + + // The size checks come before any limb access, so the limb predicates + // never read a limb at a havocked size. + fn dragon_scaled_storage_valid(value: &Big, limb_bound: usize) -> bool { + value.kani_contract_size() > 0 + && value.kani_contract_size() <= limb_bound + && value.kani_contract_valid_storage() + } + + // `value < scale * 10`. + fn dragon_scaled_below_scale10(value: &Big, scale: &Big, zero: &Big) -> bool { + value.kani_contract_cmp_scaled_sum(scale, zero, 1) == KaniInvariantOrdering::Less + } + + // `0 < value`. + fn dragon_scaled_nonzero(value: &Big, zero: &Big) -> bool { + zero.kani_contract_cmp_scaled_sum(value, zero, 0) == KaniInvariantOrdering::Less + } + + // The facts that the digit loop of `format_shortest` needs at `i == 0`, + // with each limb count bounded by `exp`. + fn dragon_shortest_scaling_valid(exp: i16, result: &(i16, Big, Big, Big, Big)) -> bool { + let (k, mant, minus, plus, scale) = result; + let limb_bound = dragon_limb_bound(exp); + let zero = Big::kani_contract_zero(); + -400 <= *k + && *k <= 400 + && dragon_scaled_storage_valid(mant, limb_bound) + && dragon_scaled_storage_valid(minus, limb_bound) + && dragon_scaled_storage_valid(plus, limb_bound) + && dragon_scaled_storage_valid(scale, limb_bound) + && (minus.kani_contract_size() <= mant.kani_contract_size() + || minus.kani_contract_size() <= scale.kani_contract_size()) + && (plus.kani_contract_size() <= mant.kani_contract_size() + || plus.kani_contract_size() <= scale.kani_contract_size()) + && dragon_scaled_below_scale10(mant, scale, &zero) + && dragon_scaled_below_scale10(minus, scale, &zero) + && dragon_scaled_below_scale10(plus, scale, &zero) + && dragon_scaled_nonzero(minus, &zero) + && dragon_scaled_nonzero(plus, &zero) + && scale.kani_contract_cmp_scaled_sum(minus, plus, MAX_SIG_DIGITS - 1) + == KaniInvariantOrdering::Less + } + + #[kani::requires( + mant >= 2 && minus == 1 + && (plus == 1 || (plus == 2 && (mant == 1 << 25 || mant == 1 << 54))) + && exp >= -1076 && exp <= 970 + && mant as u128 + plus as u128 <= ((minus as u128 + plus as u128) << 53) + )] + #[kani::ensures(|result| dragon_shortest_scaling_valid(exp, result))] + fn scale_shortest_contract( + mant: u64, + minus: u64, + plus: u64, + exp: i16, + inclusive: bool, + ) -> (i16, Big, Big, Big, Big) { + let d = Decoded { mant, minus, plus, exp, inclusive }; + let ShortestScaled { k, mant, minus, plus, scale } = scale_shortest(&d); + // Establish the size bounds in separate checked steps, so the value + // properties of the postcondition can use the bounds already established. + let limb_bound = dragon_limb_bound(exp); + assert!(-400 <= k && k <= 400); + assert!(mant.kani_size() > 0 && mant.kani_size() <= limb_bound); + assert!(minus.kani_size() > 0 && minus.kani_size() <= limb_bound); + assert!(plus.kani_size() > 0 && plus.kani_size() <= limb_bound); + assert!(scale.kani_size() > 0 && scale.kani_size() <= limb_bound); + (k, mant, minus, plus, scale) + } + + fn stub_scale_shortest(d: &Decoded) -> ShortestScaled { + let (k, mant, minus, plus, scale) = + scale_shortest_contract(d.mant, d.minus, d.plus, d.exp, d.inclusive); + ShortestScaled { k, mant, minus, plus, scale } + } + + // Each window of 16 exponents has its own contract proof. Together the + // windows cover every Dragon exponent, and each limb bound fits the storage. + #[kani::proof] + fn check_dragon_scale_partitioning() { + let exp = kani::any::(); + kani::assume((-1076..=970).contains(&exp)); + let index = (i32::from(exp) + 1076) / 16; + assert!((0..128).contains(&index)); + let first = -1076 + 16 * index; + let last = (first + 15).min(970); + assert!(first <= i32::from(exp) && i32::from(exp) <= last); + assert!(last - first < 16); + assert!(dragon_limb_bound(exp) < KANI_LIMBS); + kani::cover(exp == -1076, "partitions include the lowest Dragon exponent"); + kani::cover(exp == 970, "partitions include the highest Dragon exponent"); + } + + // The estimate of estimate_scaling_factor for a contract input with exponent `exp`, where + // `nbits` is the bit count of mant + plus - 1. + const fn fixed_k_estimate(nbits: i16, exp: i16) -> i16 { + (((nbits as i64 + exp as i64) * 1292913986) >> 32) as i16 + } + + const fn fixed_k_last(first: i16) -> i16 { + if first + 15 < 970 { first + 15 } else { 970 } + } + + // Every contract input has mant + plus in [3, 2^54 + 2], so nbits is in 2..=55, and the + // estimate of each input of the window that starts at `first` is in + // fixed_k_min(first)..=fixed_k_max(first) (check_dragon_scale_k_range). + const fn fixed_k_min(first: i16) -> i16 { + fixed_k_estimate(2, first) + } + + const fn fixed_k_max(first: i16) -> i16 { + fixed_k_estimate(55, fixed_k_last(first)) + } + + // True if `ks` lists each K of next..=last once, in increasing order. + const fn fixed_k_list_covers(ks: &[i16], next: i16, last: i16) -> bool { + match ks { + [] => next == last + 1, + [k, rest @ ..] => *k == next && fixed_k_list_covers(rest, next + 1, last), } } - // rounding up if we stop in the middle of digits - // if the following digits are exactly 5000..., check the prior digit and try to - // round to even (i.e., avoid rounding up when the prior digit is even). - let order = mant.cmp(scale.mul_small(5)); - if order == Ordering::Greater - || (order == Ordering::Equal - // SAFETY: `buf[len-1]` is initialized. - && len > 0 && unsafe { buf[len - 1].assume_init() } & 1 == 1) - { - // if rounding up changes the length, the exponent should also change. - // but we've been requested a fixed number of digits, so do not alter the buffer... - // SAFETY: we initialized that memory above. - if let Some(c) = round_up(unsafe { buf[..len].assume_init_mut() }) { - // ...unless we've been requested the fixed precision instead. - // we also need to check that, if the original buffer was empty, - // the additional digit can only be added when `k == limit` (edge case). - k += 1; - if k > limit && len < buf.len() { - buf[len] = MaybeUninit::new(c); - len += 1; + // True if `los` lists the piece offsets next, next + width, ..., 16 - width once, in + // increasing order: the pieces of a split pair, width exponents each, then cover the 16 + // exponents of its window. + const fn fixed_k_pieces_cover(los: &[u8], next: u8, width: u8) -> bool { + match los { + [] => next == 16, + [lo, rest @ ..] => *lo == next && fixed_k_pieces_cover(rest, next + width, width), + } + } + + // The exponents in each piece of a split pair: 16 less the last offset, so 4 for the + // offsets 0, 4, 8, 12 and 1 for the offsets 0 to 15. + const fn fixed_k_piece_width(los: &[u8]) -> u8 { + match los { + [.., last] => 16 - *last, + [] => 0, + } + } + + // True if `tops` lists the sub-piece indices next, next + 1, ..., n - 1 once, in + // increasing order, for n = 2, 4 or 8: the sub-pieces of a piece then take each value of + // the top mantissa bit (2 sub-pieces), the top 2 bits (4) or the top 3 bits (8) once. + const fn fixed_k_tops_cover(tops: &[u8], next: u8) -> bool { + match tops { + [] => next == 2 || next == 4 || next == 8, + [top, rest @ ..] => *top == next && fixed_k_tops_cover(rest, next + 1), + } + } + + // The shift of the top mantissa bits that pick the sub-piece of a piece at exponent + // `exp` (1, 2 or 3 bits for 2, 4 or 8 sub-pieces). The mantissa is below 2^bits, with bits + // as in fixed_k_limb_bound. The masked bits take each value once for any shift, so the + // sub-pieces cover the piece; this shift splits the largest mantissas. + const fn fixed_k_mant_shift(exp: i16, k: i16, pieces: u64) -> u32 { + let s_max = (k as i64 + 1) * (1 << 32) / 1292913986 - 1; + let window_bits = s_max - exp as i64; + let bits = if window_bits < 55 { window_bits } else { 55 }; + (bits - pieces.trailing_zeros() as i64) as u32 + } + + // The power-of-5 tables of mul_pow10 as (power, length in 64-bit limbs). + #[cfg(target_pointer_width = "64")] + const FIXED_K_POW5_TABLES: [(usize, usize); 5] = + [(16, 1), (32, 2), (64, 3), (128, 5), (256, 10)]; + + // Walks the power-of-5 tables that mul_pow10(x, n) multiplies in, in order. The operand + // is below 2^(millibits / 1000) before each table, and each power of 5 adds at most + // 2.322 bits (log2 5 < 2.322). + #[cfg(target_pointer_width = "64")] + const fn fixed_k_pow5_limbs( + tables: &[(usize, usize)], + n: usize, + millibits: usize, + other_len: usize, + ) -> usize { + match tables { + [] => KANI_LIMBS, + [(power, len), rest @ ..] => { + if n & *power == 0 { + fixed_k_pow5_limbs(rest, n, millibits, other_len) + } else if *len == other_len { + millibits.div_ceil(1000).div_ceil(64) + } else { + fixed_k_pow5_limbs(rest, n, millibits + 2322 * *power, other_len) + } } } } - // SAFETY: we initialized that memory above. - (unsafe { buf[..len].assume_init_ref() }, k) + // A bound on the limb count of the bigint operand of the cached product of mul_pow10 + // with the power table of `other_len` limbs, for each input of the window that starts at + // `first` with the estimate fixed to K. For K < 0, mant, minus and plus are below + // 2^bits before mul_pow10(-K), where bits = min(55, s_max - first) and s_max is the + // largest nbits + exp with an estimate of K. The stub of kani_mul_limb_bound asserts the + // actual count, so a wrong bound fails the proof; only the cost depends on it. + #[cfg(target_pointer_width = "64")] + const fn fixed_k_limb_bound(first: i16, k: i16, other_len: usize) -> usize { + if k >= 0 { + KANI_LIMBS + } else { + let n = k.unsigned_abs() as usize; + // Division truncates toward zero, so for K < 0 this is + // ceil((K + 1) * 2^32 / 1292913986) - 1. + let s_max = (k as i64 + 1) * (1 << 32) / 1292913986 - 1; + let window_bits = s_max - first as i64; + let bits = if window_bits < 55 { window_bits as usize } else { 55 }; + fixed_k_pow5_limbs(&FIXED_K_POW5_TABLES, n, 1000 * bits + 2322 * (n & 15), other_len) + } + } + + // 16- and 32-bit targets keep the storage bound. + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + const fn fixed_k_limb_bound(_first: i16, _k: i16, _other_len: usize) -> usize { + KANI_LIMBS + } + + // The proof of one fixed-K pair over the exponents FIRST + $exp_lo + (0..=$exp_mask) of its + // window: (0, 15) is the whole window, a piece of a split pair takes 4 exponents or 1. A + // sub-piece (the tail $mant_top, $mant_pieces) also fixes the top mantissa bits: the bits + // of mant from MANT_SHIFT up, masked to $mant_pieces - 1, equal $mant_top. + macro_rules! check_dragon_scale_fixed_k_harness { + // A piece split on the top mantissa bits: one module per sub-piece. The list must be + // 0..n - 1 in increasing order for n = 2, 4 or 8, or the build fails. + ( + $window:literal, + $k:expr, + $exp_lo:literal, + $exp_mask:expr, + [$($m:ident => $top:literal),* $(,)?] + ) => { + use super::*; + + const MANT_TOPS: &[u8] = &[$($top),*]; + const _: () = assert!(fixed_k_tops_cover(MANT_TOPS, 0)); + + $(mod $m { + check_dragon_scale_fixed_k_harness!( + $window, $k, $exp_lo, $exp_mask, $top, MANT_TOPS.len() as u64 + ); + })* + }; + ( + $window:literal, + $k:expr, + $exp_lo:literal, + $exp_mask:expr + $(, $mant_top:literal, $mant_pieces:expr)? + ) => { + use super::*; + + const FIRST: i16 = -1076 + 16 * $window; + const LAST: i16 = fixed_k_last(FIRST); + const K: i16 = $k; + + fn window_limb_count(size: usize) -> usize { + let limb_bound = dragon_limb_bound(FIRST).max(dragon_limb_bound(LAST)); + LimbCountMask((limb_bound + 1).next_power_of_two() - 1).check(size) + } + + fn stub_pow10_exponent(n: usize) -> usize { + let magnitude = usize::from(K.unsigned_abs()); + Pow10Prefix::for_range(magnitude..=magnitude).check(n) + } + + fn stub_pow2_bits(bits: usize) -> usize { + let encoded = bits & 2047; + assert_eq!(encoded, bits); + encoded + } + + fn stub_estimate(mant: u64, exp: i16) -> i16 { + assert_eq!( + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor(mant, exp), + K + ); + K + } + + fn stub_mul_limb_bound(size: usize, other_len: usize) -> usize { + let bound = fixed_k_limb_bound(FIRST, K, other_len); + assert!(size <= bound); + bound + } + + #[kani::proof_for_contract(scale_shortest_contract)] + #[kani::unwind(41)] + #[kani::stub(crate::num::imp::bignum::kani_loop_size, window_limb_count)] + #[kani::stub(Big::add, Big::kani_add_model)] + #[kani::stub(Big::sub, Big::kani_sub_model)] + #[kani::stub(Big::mul_small, Big::kani_mul_small_model)] + #[kani::stub(Big::mul_pow2, Big::kani_mul_pow2_model)] + #[kani::stub(Big::mul_digits, Big::kani_mul_digits_model)] + #[kani::stub(proof_pow10_exponent, stub_pow10_exponent)] + #[kani::stub(crate::num::imp::bignum::kani_pow2_bits, stub_pow2_bits)] + #[kani::stub(crate::num::imp::bignum::kani_mul_limb_bound, stub_mul_limb_bound)] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul, carrying_mul_model))] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul_add, carrying_mul_add_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul, carrying_mul_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul_add, carrying_mul_add_model))] + #[kani::stub(crate::num::imp::flt2dec::estimator::estimate_scaling_factor, stub_estimate)] + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::stub(::cmp, stub_cmp)] + #[kani::stub(Big::is_zero, stub_is_zero)] + #[kani::solver(cadical)] + fn check_scale_shortest() { + let mant = kani::any::() & ((1u64 << 55) - 1); + let minus = 1; + let plus = if kani::any::() { 1 } else { 2 }; + kani::assume(mant >= 2); + kani::assume(plus == 1 || mant == 1 << 25 || mant == 1 << 54); + kani::assume(mant as u128 + plus as u128 <= ((minus as u128 + plus as u128) << 53)); + let exp = FIRST + $exp_lo + i16::from(kani::any::() & $exp_mask); + kani::assume(exp <= LAST); + $({ + const MANT_SHIFT: u32 = fixed_k_mant_shift(FIRST + $exp_lo, K, $mant_pieces); + const _: () = assert!(MANT_SHIFT <= 53); + kani::assume((mant >> MANT_SHIFT) & ($mant_pieces - 1) == $mant_top); + })? + kani::assume( + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor(mant + plus, exp) + == K + ); + let inclusive = kani::any::(); + scale_shortest_contract(mant, minus, plus, exp, inclusive); + kani::cover(inclusive, "fixed-K scaling includes inclusive intervals"); + kani::cover(!inclusive, "fixed-K scaling includes exclusive intervals"); + } + }; + } + + // One proof per (window, K) pair, with the estimate fixed to K. The proof assumes that + // the estimate is K, and the estimator stub checks the same value. The stub of + // kani_mul_limb_bound asserts that each bigint operand of a cached product of mul_pow10 + // fits fixed_k_limb_bound. + macro_rules! check_dragon_scale_fixed_k_pair { + ($name:ident, $window:literal, $k:expr) => { + mod $name { + check_dragon_scale_fixed_k_harness!($window, $k, 0, 15); + } + }; + // A split pair: one module per piece, each piece a proof over PIECE_WIDTH exponents + // (4 pieces of 4 or 16 pieces of 1). The offsets must step by PIECE_WIDTH from 0 to + // 16 - PIECE_WIDTH in increasing order, or the build fails. + ( + $name:ident, + $window:literal, + $k:expr, + [$($piece:ident => $lo:literal $(=> [$($m:ident => $top:literal),* $(,)?])?),* $(,)?] + ) => { + mod $name { + use super::*; + + const PIECE_LOS: &[u8] = &[$($lo),*]; + const PIECE_WIDTH: u8 = fixed_k_piece_width(PIECE_LOS); + const PIECE_MASK: u8 = PIECE_WIDTH - 1; + const _: () = assert!(fixed_k_pieces_cover(PIECE_LOS, 0, PIECE_WIDTH)); + + $(mod $piece { + check_dragon_scale_fixed_k_harness!( + $window, $k, $lo, PIECE_MASK $(, [$($m => $top),*])? + ); + })* + } + }; + } + + // The fixed-K proofs of one window. The list must name each K of + // fixed_k_min(FIRST)..=fixed_k_max(FIRST) once, in increasing order, or the build fails. + // With check_dragon_scale_k_range the proofs cover every input of the window. + // An entry `kNNN => K => [eNN => offset, ...]` splits the proof of K into pieces. + // A piece `eNN => offset => [mN => top, ...]` splits again on the top mantissa bits. + macro_rules! check_dragon_scale_fixed_k_window { + ( + $name:ident, + $window:literal, + [$( + $k_name:ident => $k:expr $(=> [$( + $piece:ident => $lo:literal $(=> [$($m:ident => $top:literal),* $(,)?])? + ),* $(,)?])? + ),* $(,)?] + ) => { + mod $name { + use super::*; + + const _: () = { + let first: i16 = -1076 + 16 * $window; + assert!(fixed_k_list_covers(&[$($k),*], fixed_k_min(first), fixed_k_max(first))); + }; + + $(check_dragon_scale_fixed_k_pair!( + $k_name, $window, $k $(, [$($piece => $lo $(=> [$($m => $top),*])?),*])? + );)* + } + }; + } + + // Windows 000 to 127: one module per window, one fixed-K proof per K. Windows 000 to 062 + // have K < 0, windows 063 to 067 have both signs, windows 068 to 127 have K >= 0. A K < 0 + // name is k and |K|, a K >= 0 name is p and K. + mod dragon_scale_fixed_k { + use super::*; + + check_dragon_scale_fixed_k_window!(w000, 0, [ + k324 => -324, k323 => -323, k322 => -322, k321 => -321, k320 => -320, k319 => -319, + k318 => -318, k317 => -317 => [e00 => 0, e04 => 4, e08 => 8, e12 => 12], + k316 => -316, k315 => -315, k314 => -314, k313 => -313, + k312 => -312, k311 => -311, k310 => -310, k309 => -309, k308 => -308, k307 => -307, + k306 => -306, k305 => -305, k304 => -304, k303 => -303, + ]); + check_dragon_scale_fixed_k_window!(w001, 1, [ + k319 => -319, k318 => -318, k317 => -317, k316 => -316, k315 => -315, k314 => -314, + k313 => -313, k312 => -312, k311 => -311, k310 => -310, k309 => -309, k308 => -308, + k307 => -307, k306 => -306, k305 => -305, k304 => -304, k303 => -303, k302 => -302, + k301 => -301, k300 => -300, k299 => -299, + ]); + check_dragon_scale_fixed_k_window!(w002, 2, [ + k314 => -314, k313 => -313, k312 => -312, k311 => -311, k310 => -310, k309 => -309, + k308 => -308, k307 => -307, k306 => -306, k305 => -305, k304 => -304, k303 => -303, + k302 => -302, k301 => -301, k300 => -300, k299 => -299, k298 => -298, k297 => -297, + k296 => -296, k295 => -295, k294 => -294, + ]); + check_dragon_scale_fixed_k_window!(w003, 3, [ + k309 => -309, k308 => -308, k307 => -307, k306 => -306, k305 => -305, k304 => -304, + k303 => -303, k302 => -302, k301 => -301, k300 => -300, k299 => -299, k298 => -298, + k297 => -297, k296 => -296, k295 => -295, k294 => -294, k293 => -293, k292 => -292, + k291 => -291, k290 => -290, k289 => -289, + ]); + check_dragon_scale_fixed_k_window!(w004, 4, [ + k305 => -305, k304 => -304, k303 => -303, k302 => -302, k301 => -301, k300 => -300, + k299 => -299, k298 => -298, k297 => -297, k296 => -296, k295 => -295, k294 => -294, + k293 => -293, k292 => -292, k291 => -291, k290 => -290, k289 => -289, k288 => -288, + k287 => -287, k286 => -286, k285 => -285, k284 => -284, + ]); + check_dragon_scale_fixed_k_window!(w005, 5, [ + k300 => -300, k299 => -299, k298 => -298, k297 => -297, k296 => -296, k295 => -295, + k294 => -294, k293 => -293, k292 => -292, k291 => -291, k290 => -290, k289 => -289, + k288 => -288, k287 => -287, k286 => -286, k285 => -285, k284 => -284, k283 => -283, + k282 => -282, k281 => -281, k280 => -280, k279 => -279, + ]); + check_dragon_scale_fixed_k_window!(w006, 6, [ + k295 => -295, k294 => -294, k293 => -293, k292 => -292, k291 => -291, k290 => -290, + k289 => -289, k288 => -288, k287 => -287, k286 => -286, k285 => -285, k284 => -284, + k283 => -283, k282 => -282, k281 => -281, k280 => -280, k279 => -279, k278 => -278, + k277 => -277, k276 => -276, k275 => -275, k274 => -274, + ]); + check_dragon_scale_fixed_k_window!(w007, 7, [ + k290 => -290, k289 => -289, k288 => -288, k287 => -287, k286 => -286, k285 => -285, + k284 => -284, k283 => -283, k282 => -282, k281 => -281, k280 => -280, k279 => -279, + k278 => -278, k277 => -277, k276 => -276, k275 => -275, k274 => -274, k273 => -273, + k272 => -272, k271 => -271, k270 => -270, + ]); + check_dragon_scale_fixed_k_window!(w008, 8, [ + k285 => -285, k284 => -284, k283 => -283, k282 => -282, k281 => -281, k280 => -280, + k279 => -279, k278 => -278, k277 => -277, k276 => -276, k275 => -275, k274 => -274, + k273 => -273, k272 => -272, k271 => -271, k270 => -270, k269 => -269, k268 => -268, + k267 => -267, k266 => -266, k265 => -265, + ]); + check_dragon_scale_fixed_k_window!(w009, 9, [ + k280 => -280, k279 => -279, k278 => -278, k277 => -277, k276 => -276, k275 => -275, + k274 => -274, k273 => -273, k272 => -272, k271 => -271, k270 => -270, k269 => -269, + k268 => -268, k267 => -267, k266 => -266, k265 => -265, k264 => -264, k263 => -263, + k262 => -262, k261 => -261, k260 => -260, + ]); + check_dragon_scale_fixed_k_window!(w010, 10, [ + k276 => -276, k275 => -275, k274 => -274, k273 => -273, k272 => -272, k271 => -271, + k270 => -270, k269 => -269, k268 => -268, k267 => -267, k266 => -266, k265 => -265, + k264 => -264, k263 => -263, k262 => -262, k261 => -261, k260 => -260, k259 => -259, + k258 => -258, k257 => -257, k256 => -256, k255 => -255, + ]); + check_dragon_scale_fixed_k_window!(w011, 11, [ + k271 => -271, k270 => -270, k269 => -269, k268 => -268, k267 => -267, k266 => -266, + k265 => -265, k264 => -264, k263 => -263, k262 => -262, k261 => -261, k260 => -260, + k259 => -259, k258 => -258, k257 => -257, k256 => -256, k255 => -255, k254 => -254, + k253 => -253, k252 => -252, k251 => -251, k250 => -250, + ]); + check_dragon_scale_fixed_k_window!(w012, 12, [ + k266 => -266, k265 => -265, k264 => -264, k263 => -263, k262 => -262, k261 => -261, + k260 => -260, k259 => -259, k258 => -258, k257 => -257, k256 => -256, k255 => -255, + k254 => -254, k253 => -253, k252 => -252, k251 => -251, k250 => -250, k249 => -249, + k248 => -248, k247 => -247, k246 => -246, + ]); + check_dragon_scale_fixed_k_window!(w013, 13, [ + k261 => -261, k260 => -260, k259 => -259, k258 => -258, k257 => -257, k256 => -256, + k255 => -255, k254 => -254, k253 => -253, k252 => -252, k251 => -251, k250 => -250, + k249 => -249, k248 => -248, k247 => -247, k246 => -246, k245 => -245, k244 => -244, + k243 => -243, k242 => -242, k241 => -241, + ]); + check_dragon_scale_fixed_k_window!(w014, 14, [ + k256 => -256, k255 => -255, k254 => -254, k253 => -253, k252 => -252, k251 => -251, + k250 => -250, k249 => -249, k248 => -248, k247 => -247, k246 => -246, k245 => -245, + k244 => -244, k243 => -243, k242 => -242, k241 => -241, k240 => -240, k239 => -239, + k238 => -238, k237 => -237, k236 => -236, + ]); + check_dragon_scale_fixed_k_window!(w015, 15, [ + k252 => -252, k251 => -251, k250 => -250, k249 => -249, k248 => -248, k247 => -247, + k246 => -246, k245 => -245, k244 => -244, k243 => -243, k242 => -242, k241 => -241, + k240 => -240, k239 => -239, k238 => -238, k237 => -237, k236 => -236, k235 => -235, + k234 => -234, k233 => -233, k232 => -232, k231 => -231, + ]); + check_dragon_scale_fixed_k_window!(w016, 16, [ + k247 => -247, k246 => -246, k245 => -245, k244 => -244, k243 => -243, k242 => -242, + k241 => -241, k240 => -240, k239 => -239, k238 => -238, k237 => -237, k236 => -236, + k235 => -235, k234 => -234, k233 => -233, k232 => -232, k231 => -231, k230 => -230, + k229 => -229, k228 => -228, k227 => -227, k226 => -226, + ]); + check_dragon_scale_fixed_k_window!(w017, 17, [ + k242 => -242, k241 => -241, k240 => -240, k239 => -239, k238 => -238, k237 => -237, + k236 => -236, k235 => -235, k234 => -234, k233 => -233, k232 => -232, k231 => -231, + k230 => -230, k229 => -229, k228 => -228, k227 => -227, k226 => -226, k225 => -225, + k224 => -224, k223 => -223, k222 => -222, k221 => -221, + ]); + check_dragon_scale_fixed_k_window!(w018, 18, [ + k237 => -237, k236 => -236, k235 => -235, k234 => -234, k233 => -233, k232 => -232, + k231 => -231, k230 => -230, k229 => -229, k228 => -228, k227 => -227, k226 => -226, + k225 => -225, k224 => -224, k223 => -223, k222 => -222, k221 => -221, k220 => -220, + k219 => -219, k218 => -218, k217 => -217, + ]); + check_dragon_scale_fixed_k_window!(w019, 19, [ + k232 => -232, k231 => -231, k230 => -230, k229 => -229, k228 => -228, k227 => -227, + k226 => -226, k225 => -225, k224 => -224, k223 => -223, k222 => -222, k221 => -221, + k220 => -220, k219 => -219, k218 => -218, k217 => -217, k216 => -216, k215 => -215, + k214 => -214, k213 => -213, k212 => -212, + ]); + check_dragon_scale_fixed_k_window!(w020, 20, [ + k227 => -227, k226 => -226, k225 => -225, k224 => -224, k223 => -223, k222 => -222, + k221 => -221, k220 => -220, k219 => -219, k218 => -218, k217 => -217, k216 => -216, + k215 => -215, k214 => -214, k213 => -213, k212 => -212, k211 => -211, k210 => -210, + k209 => -209, k208 => -208, k207 => -207, + ]); + check_dragon_scale_fixed_k_window!(w021, 21, [ + k223 => -223, k222 => -222, k221 => -221, k220 => -220, k219 => -219, k218 => -218, + k217 => -217, k216 => -216, k215 => -215, k214 => -214, k213 => -213, k212 => -212, + k211 => -211, k210 => -210, k209 => -209, k208 => -208, k207 => -207, k206 => -206, + k205 => -205, k204 => -204, k203 => -203, k202 => -202, + ]); + check_dragon_scale_fixed_k_window!(w022, 22, [ + k218 => -218, k217 => -217, k216 => -216, k215 => -215, k214 => -214, k213 => -213, + k212 => -212, k211 => -211, k210 => -210, k209 => -209, k208 => -208, k207 => -207, + k206 => -206, k205 => -205, k204 => -204, k203 => -203, k202 => -202, k201 => -201, + k200 => -200, k199 => -199, k198 => -198, k197 => -197, + ]); + check_dragon_scale_fixed_k_window!(w023, 23, [ + k213 => -213, k212 => -212, k211 => -211, k210 => -210, k209 => -209, k208 => -208, + k207 => -207, k206 => -206, + k205 => -205 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k204 => -204 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k203 => -203 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k202 => -202 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k201 => -201 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k200 => -200 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k199 => -199, k198 => -198, k197 => -197, k196 => -196, k195 => -195, k194 => -194, + k193 => -193, + ]); + check_dragon_scale_fixed_k_window!(w024, 24, [ + k208 => -208, k207 => -207, k206 => -206, k205 => -205, k204 => -204, k203 => -203, + k202 => -202, k201 => -201, k200 => -200, k199 => -199, k198 => -198, k197 => -197, + k196 => -196, k195 => -195, k194 => -194, k193 => -193, k192 => -192, k191 => -191, + k190 => -190, k189 => -189, k188 => -188, + ]); + check_dragon_scale_fixed_k_window!(w025, 25, [ + k203 => -203, k202 => -202, k201 => -201, k200 => -200, k199 => -199, k198 => -198, + k197 => -197, k196 => -196, k195 => -195, k194 => -194, k193 => -193, k192 => -192, + k191 => -191, k190 => -190, k189 => -189, k188 => -188, k187 => -187, k186 => -186, + k185 => -185, k184 => -184, k183 => -183, + ]); + check_dragon_scale_fixed_k_window!(w026, 26, [ + k199 => -199, k198 => -198, k197 => -197, k196 => -196, k195 => -195, k194 => -194, + k193 => -193, k192 => -192, k191 => -191, k190 => -190, k189 => -189, k188 => -188, + k187 => -187, k186 => -186, k185 => -185, k184 => -184, k183 => -183, k182 => -182, + k181 => -181, k180 => -180, k179 => -179, k178 => -178, + ]); + check_dragon_scale_fixed_k_window!(w027, 27, [ + k194 => -194, k193 => -193, k192 => -192, k191 => -191, k190 => -190, k189 => -189, + k188 => -188, k187 => -187, k186 => -186, k185 => -185, k184 => -184, k183 => -183, + k182 => -182, k181 => -181, k180 => -180, k179 => -179, k178 => -178, k177 => -177, + k176 => -176, k175 => -175, k174 => -174, k173 => -173, + ]); + check_dragon_scale_fixed_k_window!(w028, 28, [ + k189 => -189, k188 => -188, k187 => -187, k186 => -186, k185 => -185, k184 => -184, + k183 => -183 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k182 => -182, k181 => -181, k180 => -180, k179 => -179, k178 => -178, k177 => -177, + k176 => -176, k175 => -175, k174 => -174, k173 => -173, k172 => -172, k171 => -171, + k170 => -170, k169 => -169, k168 => -168, + ]); + check_dragon_scale_fixed_k_window!(w029, 29, [ + k184 => -184, k183 => -183, k182 => -182, k181 => -181, k180 => -180, k179 => -179, + k178 => -178, k177 => -177, k176 => -176, k175 => -175, + k174 => -174 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k173 => -173 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k172 => -172 => [e00 => 0, e04 => 4, e08 => 8, e12 => 12], + k171 => -171, k170 => -170, k169 => -169, k168 => -168, k167 => -167, k166 => -166, + k165 => -165, k164 => -164, + ]); + check_dragon_scale_fixed_k_window!(w030, 30, [ + k179 => -179, k178 => -178, k177 => -177, k176 => -176, k175 => -175, k174 => -174, + k173 => -173, k172 => -172, k171 => -171, k170 => -170, k169 => -169, k168 => -168, + k167 => -167, k166 => -166, k165 => -165, k164 => -164, k163 => -163, k162 => -162, + k161 => -161, k160 => -160, k159 => -159, + ]); + check_dragon_scale_fixed_k_window!(w031, 31, [ + k174 => -174, k173 => -173, k172 => -172, k171 => -171, k170 => -170, k169 => -169, + k168 => -168, k167 => -167, k166 => -166, k165 => -165, k164 => -164, k163 => -163, + k162 => -162, k161 => -161, k160 => -160, k159 => -159, k158 => -158, k157 => -157, + k156 => -156, k155 => -155, k154 => -154, + ]); + check_dragon_scale_fixed_k_window!(w032, 32, [ + k170 => -170, k169 => -169, k168 => -168, k167 => -167, k166 => -166, k165 => -165, + k164 => -164, k163 => -163, k162 => -162, k161 => -161, k160 => -160, k159 => -159, + k158 => -158, k157 => -157, k156 => -156, k155 => -155, k154 => -154, k153 => -153, + k152 => -152, k151 => -151, k150 => -150, k149 => -149, + ]); + check_dragon_scale_fixed_k_window!(w033, 33, [ + k165 => -165, k164 => -164, k163 => -163, k162 => -162, k161 => -161, k160 => -160, + k159 => -159, k158 => -158, k157 => -157, k156 => -156, k155 => -155, k154 => -154, + k153 => -153, k152 => -152, k151 => -151, k150 => -150, k149 => -149, k148 => -148, + k147 => -147, k146 => -146, k145 => -145, k144 => -144, + ]); + check_dragon_scale_fixed_k_window!(w034, 34, [ + k160 => -160, k159 => -159, k158 => -158, k157 => -157, k156 => -156, k155 => -155, + k154 => -154, + k153 => -153 => [ + e00 => 0, e01 => 1, + e02 => 2 => [ + m0 => 0, m1 => 1, m2 => 2, m3 => 3, m4 => 4, m5 => 5, m6 => 6, m7 => 7, + ], + e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k152 => -152 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k151 => -151 => [ + e00 => 0, e01 => 1, e02 => 2, e03 => 3, + e04 => 4, e05 => 5, e06 => 6, e07 => 7, + e08 => 8, e09 => 9, e10 => 10, e11 => 11, + e12 => 12, e13 => 13, e14 => 14, e15 => 15, + ], + k150 => -150, k149 => -149, k148 => -148, k147 => -147, k146 => -146, k145 => -145, + k144 => -144, k143 => -143, k142 => -142, k141 => -141, k140 => -140, + ]); + check_dragon_scale_fixed_k_window!(w035, 35, [ + k155 => -155, k154 => -154, k153 => -153, k152 => -152, k151 => -151, k150 => -150, + k149 => -149, k148 => -148, k147 => -147, k146 => -146, k145 => -145, k144 => -144, + k143 => -143, k142 => -142, k141 => -141, k140 => -140, k139 => -139, k138 => -138, + k137 => -137, k136 => -136, k135 => -135, + ]); + check_dragon_scale_fixed_k_window!(w036, 36, [ + k150 => -150, k149 => -149, k148 => -148, k147 => -147, k146 => -146, k145 => -145, + k144 => -144, k143 => -143, k142 => -142, k141 => -141, k140 => -140, k139 => -139, + k138 => -138, k137 => -137, k136 => -136, k135 => -135, k134 => -134, k133 => -133, + k132 => -132, k131 => -131, k130 => -130, + ]); + check_dragon_scale_fixed_k_window!(w037, 37, [ + k146 => -146, k145 => -145, k144 => -144, k143 => -143, k142 => -142, k141 => -141, + k140 => -140, k139 => -139, k138 => -138, k137 => -137, k136 => -136, k135 => -135, + k134 => -134, k133 => -133, k132 => -132, k131 => -131, k130 => -130, k129 => -129, + k128 => -128, k127 => -127, k126 => -126, k125 => -125, + ]); + check_dragon_scale_fixed_k_window!(w038, 38, [ + k141 => -141, k140 => -140, k139 => -139, k138 => -138, k137 => -137, k136 => -136, + k135 => -135, k134 => -134, k133 => -133, k132 => -132, k131 => -131, k130 => -130, + k129 => -129, k128 => -128, k127 => -127, k126 => -126, k125 => -125, k124 => -124, + k123 => -123, k122 => -122, k121 => -121, k120 => -120, + ]); + check_dragon_scale_fixed_k_window!(w039, 39, [ + k136 => -136, k135 => -135, k134 => -134, k133 => -133, k132 => -132, k131 => -131, + k130 => -130, k129 => -129, k128 => -128, k127 => -127, k126 => -126, k125 => -125, + k124 => -124, k123 => -123, k122 => -122, k121 => -121, k120 => -120, k119 => -119, + k118 => -118, k117 => -117, k116 => -116, k115 => -115, + ]); + check_dragon_scale_fixed_k_window!(w040, 40, [ + k131 => -131, k130 => -130, k129 => -129, k128 => -128, k127 => -127, k126 => -126, + k125 => -125, k124 => -124, k123 => -123, k122 => -122, k121 => -121, k120 => -120, + k119 => -119, k118 => -118, k117 => -117, k116 => -116, k115 => -115, k114 => -114, + k113 => -113, k112 => -112, k111 => -111, + ]); + check_dragon_scale_fixed_k_window!(w041, 41, [ + k126 => -126, k125 => -125, k124 => -124, k123 => -123, k122 => -122, k121 => -121, + k120 => -120, k119 => -119, k118 => -118, k117 => -117, k116 => -116, k115 => -115, + k114 => -114, k113 => -113, k112 => -112, k111 => -111, k110 => -110, k109 => -109, + k108 => -108, k107 => -107, k106 => -106, + ]); + check_dragon_scale_fixed_k_window!(w042, 42, [ + k122 => -122, k121 => -121, k120 => -120, k119 => -119, k118 => -118, k117 => -117, + k116 => -116, k115 => -115, k114 => -114, k113 => -113, k112 => -112, k111 => -111, + k110 => -110, k109 => -109, k108 => -108, k107 => -107, k106 => -106, k105 => -105, + k104 => -104, k103 => -103, k102 => -102, k101 => -101, + ]); + check_dragon_scale_fixed_k_window!(w043, 43, [ + k117 => -117, k116 => -116, k115 => -115, k114 => -114, k113 => -113, k112 => -112, + k111 => -111, k110 => -110, k109 => -109, k108 => -108, k107 => -107, k106 => -106, + k105 => -105, k104 => -104, k103 => -103, k102 => -102, k101 => -101, k100 => -100, + k099 => -99, k098 => -98, k097 => -97, k096 => -96, + ]); + check_dragon_scale_fixed_k_window!(w044, 44, [ + k112 => -112, k111 => -111, k110 => -110, k109 => -109, k108 => -108, k107 => -107, + k106 => -106, k105 => -105, k104 => -104, k103 => -103, k102 => -102, k101 => -101, + k100 => -100, k099 => -99, k098 => -98, k097 => -97, k096 => -96, k095 => -95, + k094 => -94, k093 => -93, k092 => -92, k091 => -91, + ]); + check_dragon_scale_fixed_k_window!(w045, 45, [ + k107 => -107, k106 => -106, k105 => -105, k104 => -104, k103 => -103, k102 => -102, + k101 => -101, k100 => -100, k099 => -99, k098 => -98, k097 => -97, k096 => -96, + k095 => -95, k094 => -94, k093 => -93, k092 => -92, k091 => -91, k090 => -90, + k089 => -89, k088 => -88, k087 => -87, + ]); + check_dragon_scale_fixed_k_window!(w046, 46, [ + k102 => -102, k101 => -101, k100 => -100, k099 => -99, k098 => -98, k097 => -97, + k096 => -96, k095 => -95, k094 => -94, k093 => -93, k092 => -92, k091 => -91, + k090 => -90, k089 => -89, k088 => -88, k087 => -87, k086 => -86, k085 => -85, + k084 => -84, k083 => -83, k082 => -82, + ]); + check_dragon_scale_fixed_k_window!(w047, 47, [ + k097 => -97, k096 => -96, k095 => -95, k094 => -94, k093 => -93, k092 => -92, + k091 => -91 => [e00 => 0, e04 => 4, e08 => 8, e12 => 12], + k090 => -90, k089 => -89, k088 => -88, k087 => -87, k086 => -86, k085 => -85, + k084 => -84, k083 => -83, k082 => -82, k081 => -81, k080 => -80, k079 => -79, + k078 => -78, k077 => -77, + ]); + check_dragon_scale_fixed_k_window!(w048, 48, [ + k093 => -93, k092 => -92, k091 => -91, k090 => -90, k089 => -89, k088 => -88, + k087 => -87, k086 => -86, k085 => -85, k084 => -84, k083 => -83, k082 => -82, + k081 => -81, k080 => -80, k079 => -79, k078 => -78, k077 => -77, k076 => -76, + k075 => -75, k074 => -74, k073 => -73, k072 => -72, + ]); + check_dragon_scale_fixed_k_window!(w049, 49, [ + k088 => -88, k087 => -87, k086 => -86, k085 => -85, k084 => -84, k083 => -83, + k082 => -82, k081 => -81, k080 => -80, k079 => -79, k078 => -78, k077 => -77, + k076 => -76, k075 => -75, k074 => -74, k073 => -73, k072 => -72, k071 => -71, + k070 => -70, k069 => -69, k068 => -68, k067 => -67, + ]); + check_dragon_scale_fixed_k_window!(w050, 50, [ + k083 => -83, k082 => -82, k081 => -81, k080 => -80, k079 => -79, k078 => -78, + k077 => -77, k076 => -76, k075 => -75, k074 => -74, k073 => -73, k072 => -72, + k071 => -71, k070 => -70, k069 => -69, k068 => -68, k067 => -67, k066 => -66, + k065 => -65, k064 => -64, k063 => -63, + ]); + check_dragon_scale_fixed_k_window!(w051, 51, [ + k078 => -78, k077 => -77, k076 => -76, k075 => -75, k074 => -74, k073 => -73, + k072 => -72, k071 => -71, k070 => -70, k069 => -69, k068 => -68, k067 => -67, + k066 => -66, k065 => -65, k064 => -64, k063 => -63, k062 => -62, k061 => -61, + k060 => -60, k059 => -59, k058 => -58, + ]); + check_dragon_scale_fixed_k_window!(w052, 52, [ + k073 => -73, k072 => -72, k071 => -71, k070 => -70, k069 => -69, k068 => -68, + k067 => -67, k066 => -66, k065 => -65, k064 => -64, k063 => -63, k062 => -62, + k061 => -61, k060 => -60, k059 => -59, k058 => -58, k057 => -57, k056 => -56, + k055 => -55, k054 => -54, k053 => -53, + ]); + check_dragon_scale_fixed_k_window!(w053, 53, [ + k069 => -69, k068 => -68, k067 => -67, k066 => -66, k065 => -65, k064 => -64, + k063 => -63, k062 => -62, k061 => -61, k060 => -60, k059 => -59, k058 => -58, + k057 => -57, k056 => -56, k055 => -55, k054 => -54, k053 => -53, k052 => -52, + k051 => -51, k050 => -50, k049 => -49, k048 => -48, + ]); + check_dragon_scale_fixed_k_window!(w054, 54, [ + k064 => -64, k063 => -63, k062 => -62, k061 => -61, k060 => -60, k059 => -59, + k058 => -58, k057 => -57, k056 => -56, k055 => -55, k054 => -54, k053 => -53, + k052 => -52, k051 => -51, k050 => -50, k049 => -49, k048 => -48, k047 => -47, + k046 => -46, k045 => -45, k044 => -44, k043 => -43, + ]); + check_dragon_scale_fixed_k_window!(w055, 55, [ + k059 => -59, k058 => -58, k057 => -57, k056 => -56, k055 => -55, k054 => -54, + k053 => -53, k052 => -52, k051 => -51, k050 => -50, k049 => -49, k048 => -48, + k047 => -47, k046 => -46, k045 => -45, k044 => -44, k043 => -43, k042 => -42, + k041 => -41, k040 => -40, k039 => -39, k038 => -38, + ]); + check_dragon_scale_fixed_k_window!(w056, 56, [ + k054 => -54, k053 => -53, k052 => -52, k051 => -51, k050 => -50, k049 => -49, + k048 => -48, k047 => -47, k046 => -46, k045 => -45, k044 => -44, k043 => -43, + k042 => -42, k041 => -41, k040 => -40, k039 => -39, k038 => -38, k037 => -37, + k036 => -36, k035 => -35, k034 => -34, + ]); + check_dragon_scale_fixed_k_window!(w057, 57, [ + k049 => -49, k048 => -48, k047 => -47, k046 => -46, k045 => -45, k044 => -44, + k043 => -43, k042 => -42, k041 => -41, k040 => -40, k039 => -39, k038 => -38, + k037 => -37, k036 => -36, k035 => -35, k034 => -34, k033 => -33, k032 => -32, + k031 => -31, k030 => -30, k029 => -29, + ]); + check_dragon_scale_fixed_k_window!(w058, 58, [ + k044 => -44, k043 => -43, k042 => -42, k041 => -41, k040 => -40, k039 => -39, + k038 => -38, k037 => -37, k036 => -36, k035 => -35, k034 => -34, k033 => -33, + k032 => -32, k031 => -31, k030 => -30, k029 => -29, k028 => -28, k027 => -27, + k026 => -26, k025 => -25, k024 => -24, + ]); + check_dragon_scale_fixed_k_window!(w059, 59, [ + k040 => -40, k039 => -39, k038 => -38, k037 => -37, k036 => -36, k035 => -35, + k034 => -34, k033 => -33, k032 => -32, k031 => -31, k030 => -30, k029 => -29, + k028 => -28, k027 => -27, k026 => -26, k025 => -25, k024 => -24, k023 => -23, + k022 => -22, k021 => -21, k020 => -20, k019 => -19, + ]); + check_dragon_scale_fixed_k_window!(w060, 60, [ + k035 => -35, k034 => -34, k033 => -33, k032 => -32, k031 => -31, k030 => -30, + k029 => -29, k028 => -28, k027 => -27, k026 => -26, k025 => -25, k024 => -24, + k023 => -23, k022 => -22, k021 => -21, k020 => -20, k019 => -19, k018 => -18, + k017 => -17, k016 => -16, k015 => -15, k014 => -14, + ]); + check_dragon_scale_fixed_k_window!(w061, 61, [ + k030 => -30, k029 => -29, k028 => -28, k027 => -27, k026 => -26, k025 => -25, + k024 => -24, k023 => -23, k022 => -22, k021 => -21, k020 => -20, k019 => -19, + k018 => -18, k017 => -17, k016 => -16, k015 => -15, k014 => -14, k013 => -13, + k012 => -12, k011 => -11, k010 => -10, + ]); + check_dragon_scale_fixed_k_window!(w062, 62, [ + k025 => -25, k024 => -24, k023 => -23, k022 => -22, k021 => -21, k020 => -20, + k019 => -19, k018 => -18, k017 => -17, k016 => -16, k015 => -15, k014 => -14, + k013 => -13, k012 => -12, k011 => -11, k010 => -10, k009 => -9, k008 => -8, + k007 => -7, k006 => -6, k005 => -5, + ]); + check_dragon_scale_fixed_k_window!(w063, 63, [ + k020 => -20, k019 => -19, k018 => -18, k017 => -17, k016 => -16, k015 => -15, + k014 => -14, k013 => -13, k012 => -12, k011 => -11, k010 => -10, k009 => -9, + k008 => -8, k007 => -7, k006 => -6, k005 => -5, k004 => -4, k003 => -3, + k002 => -2, k001 => -1, p000 => 0, + ]); + check_dragon_scale_fixed_k_window!(w064, 64, [ + k016 => -16, k015 => -15, k014 => -14, k013 => -13, k012 => -12, k011 => -11, + k010 => -10, k009 => -9, k008 => -8, k007 => -7, k006 => -6, k005 => -5, + k004 => -4, k003 => -3, k002 => -2, k001 => -1, p000 => 0, p001 => 1, + p002 => 2, p003 => 3, p004 => 4, p005 => 5, + ]); + check_dragon_scale_fixed_k_window!(w065, 65, [ + k011 => -11, k010 => -10, k009 => -9, k008 => -8, k007 => -7, k006 => -6, + k005 => -5, k004 => -4, k003 => -3, k002 => -2, k001 => -1, p000 => 0, + p001 => 1, p002 => 2, p003 => 3, p004 => 4, p005 => 5, p006 => 6, + p007 => 7, p008 => 8, p009 => 9, p010 => 10, + ]); + check_dragon_scale_fixed_k_window!(w066, 66, [ + k006 => -6, k005 => -5, k004 => -4, k003 => -3, k002 => -2, k001 => -1, + p000 => 0, p001 => 1, p002 => 2, p003 => 3, p004 => 4, p005 => 5, + p006 => 6, p007 => 7, p008 => 8, p009 => 9, p010 => 10, p011 => 11, + p012 => 12, p013 => 13, p014 => 14, p015 => 15, + ]); + check_dragon_scale_fixed_k_window!(w067, 67, [ + k001 => -1, p000 => 0, p001 => 1, p002 => 2, p003 => 3, p004 => 4, + p005 => 5, p006 => 6, p007 => 7, p008 => 8, p009 => 9, p010 => 10, + p011 => 11, p012 => 12, p013 => 13, p014 => 14, p015 => 15, p016 => 16, + p017 => 17, p018 => 18, p019 => 19, + ]); + check_dragon_scale_fixed_k_window!(w068, 68, [ + p004 => 4, p005 => 5, p006 => 6, p007 => 7, p008 => 8, p009 => 9, + p010 => 10, p011 => 11, p012 => 12, p013 => 13, p014 => 14, p015 => 15, + p016 => 16, p017 => 17, p018 => 18, p019 => 19, p020 => 20, p021 => 21, + p022 => 22, p023 => 23, p024 => 24, + ]); + check_dragon_scale_fixed_k_window!(w069, 69, [ + p009 => 9, p010 => 10, p011 => 11, p012 => 12, p013 => 13, p014 => 14, + p015 => 15, p016 => 16, p017 => 17, p018 => 18, p019 => 19, p020 => 20, + p021 => 21, p022 => 22, p023 => 23, p024 => 24, p025 => 25, p026 => 26, + p027 => 27, p028 => 28, p029 => 29, + ]); + check_dragon_scale_fixed_k_window!(w070, 70, [ + p013 => 13, p014 => 14, p015 => 15, p016 => 16, p017 => 17, p018 => 18, + p019 => 19, p020 => 20, p021 => 21, p022 => 22, p023 => 23, p024 => 24, + p025 => 25, p026 => 26, p027 => 27, p028 => 28, p029 => 29, p030 => 30, + p031 => 31, p032 => 32, p033 => 33, p034 => 34, + ]); + check_dragon_scale_fixed_k_window!(w071, 71, [ + p018 => 18, p019 => 19, p020 => 20, p021 => 21, p022 => 22, p023 => 23, + p024 => 24, p025 => 25, p026 => 26, p027 => 27, p028 => 28, p029 => 29, + p030 => 30, p031 => 31, p032 => 32, p033 => 33, p034 => 34, p035 => 35, + p036 => 36, p037 => 37, p038 => 38, p039 => 39, + ]); + check_dragon_scale_fixed_k_window!(w072, 72, [ + p023 => 23, p024 => 24, p025 => 25, p026 => 26, p027 => 27, p028 => 28, + p029 => 29, p030 => 30, p031 => 31, p032 => 32, p033 => 33, p034 => 34, + p035 => 35, p036 => 36, p037 => 37, p038 => 38, p039 => 39, p040 => 40, + p041 => 41, p042 => 42, p043 => 43, + ]); + check_dragon_scale_fixed_k_window!(w073, 73, [ + p028 => 28, p029 => 29, p030 => 30, p031 => 31, p032 => 32, p033 => 33, + p034 => 34, p035 => 35, p036 => 36, p037 => 37, p038 => 38, p039 => 39, + p040 => 40, p041 => 41, p042 => 42, p043 => 43, p044 => 44, p045 => 45, + p046 => 46, p047 => 47, p048 => 48, + ]); + check_dragon_scale_fixed_k_window!(w074, 74, [ + p033 => 33, p034 => 34, p035 => 35, p036 => 36, p037 => 37, p038 => 38, + p039 => 39, p040 => 40, p041 => 41, p042 => 42, p043 => 43, p044 => 44, + p045 => 45, p046 => 46, p047 => 47, p048 => 48, p049 => 49, p050 => 50, + p051 => 51, p052 => 52, p053 => 53, + ]); + check_dragon_scale_fixed_k_window!(w075, 75, [ + p037 => 37, p038 => 38, p039 => 39, p040 => 40, p041 => 41, p042 => 42, + p043 => 43, p044 => 44, p045 => 45, p046 => 46, p047 => 47, p048 => 48, + p049 => 49, p050 => 50, p051 => 51, p052 => 52, p053 => 53, p054 => 54, + p055 => 55, p056 => 56, p057 => 57, p058 => 58, + ]); + check_dragon_scale_fixed_k_window!(w076, 76, [ + p042 => 42, p043 => 43, p044 => 44, p045 => 45, p046 => 46, p047 => 47, + p048 => 48, p049 => 49, p050 => 50, p051 => 51, p052 => 52, p053 => 53, + p054 => 54, p055 => 55, p056 => 56, p057 => 57, p058 => 58, p059 => 59, + p060 => 60, p061 => 61, p062 => 62, p063 => 63, + ]); + check_dragon_scale_fixed_k_window!(w077, 77, [ + p047 => 47, p048 => 48, p049 => 49, p050 => 50, p051 => 51, p052 => 52, + p053 => 53, p054 => 54, p055 => 55, p056 => 56, p057 => 57, p058 => 58, + p059 => 59, p060 => 60, p061 => 61, p062 => 62, p063 => 63, p064 => 64, + p065 => 65, p066 => 66, p067 => 67, p068 => 68, + ]); + check_dragon_scale_fixed_k_window!(w078, 78, [ + p052 => 52, p053 => 53, p054 => 54, p055 => 55, p056 => 56, p057 => 57, + p058 => 58, p059 => 59, p060 => 60, p061 => 61, p062 => 62, p063 => 63, + p064 => 64, p065 => 65, p066 => 66, p067 => 67, p068 => 68, p069 => 69, + p070 => 70, p071 => 71, p072 => 72, + ]); + check_dragon_scale_fixed_k_window!(w079, 79, [ + p057 => 57, p058 => 58, p059 => 59, p060 => 60, p061 => 61, p062 => 62, + p063 => 63, p064 => 64, p065 => 65, p066 => 66, p067 => 67, p068 => 68, + p069 => 69, p070 => 70, p071 => 71, p072 => 72, p073 => 73, p074 => 74, + p075 => 75, p076 => 76, p077 => 77, + ]); + check_dragon_scale_fixed_k_window!(w080, 80, [ + p062 => 62, p063 => 63, p064 => 64, p065 => 65, p066 => 66, p067 => 67, + p068 => 68, p069 => 69, p070 => 70, p071 => 71, p072 => 72, p073 => 73, + p074 => 74, p075 => 75, p076 => 76, p077 => 77, p078 => 78, p079 => 79, + p080 => 80, p081 => 81, p082 => 82, + ]); + check_dragon_scale_fixed_k_window!(w081, 81, [ + p066 => 66, p067 => 67, p068 => 68, p069 => 69, p070 => 70, p071 => 71, + p072 => 72, p073 => 73, p074 => 74, p075 => 75, p076 => 76, p077 => 77, + p078 => 78, p079 => 79, p080 => 80, p081 => 81, p082 => 82, p083 => 83, + p084 => 84, p085 => 85, p086 => 86, p087 => 87, + ]); + check_dragon_scale_fixed_k_window!(w082, 82, [ + p071 => 71, p072 => 72, p073 => 73, p074 => 74, p075 => 75, p076 => 76, + p077 => 77, p078 => 78, p079 => 79, p080 => 80, p081 => 81, p082 => 82, + p083 => 83, p084 => 84, p085 => 85, p086 => 86, p087 => 87, p088 => 88, + p089 => 89, p090 => 90, p091 => 91, p092 => 92, + ]); + check_dragon_scale_fixed_k_window!(w083, 83, [ + p076 => 76, p077 => 77, p078 => 78, p079 => 79, p080 => 80, p081 => 81, + p082 => 82, p083 => 83, p084 => 84, p085 => 85, p086 => 86, p087 => 87, + p088 => 88, p089 => 89, p090 => 90, p091 => 91, p092 => 92, p093 => 93, + p094 => 94, p095 => 95, p096 => 96, + ]); + check_dragon_scale_fixed_k_window!(w084, 84, [ + p081 => 81, p082 => 82, p083 => 83, p084 => 84, p085 => 85, p086 => 86, + p087 => 87, p088 => 88, p089 => 89, p090 => 90, p091 => 91, p092 => 92, + p093 => 93, p094 => 94, p095 => 95, p096 => 96, p097 => 97, p098 => 98, + p099 => 99, p100 => 100, p101 => 101, + ]); + check_dragon_scale_fixed_k_window!(w085, 85, [ + p086 => 86, p087 => 87, p088 => 88, p089 => 89, p090 => 90, p091 => 91, + p092 => 92, p093 => 93, p094 => 94, p095 => 95, p096 => 96, p097 => 97, + p098 => 98, p099 => 99, p100 => 100, p101 => 101, p102 => 102, p103 => 103, + p104 => 104, p105 => 105, p106 => 106, + ]); + check_dragon_scale_fixed_k_window!(w086, 86, [ + p090 => 90, p091 => 91, p092 => 92, p093 => 93, p094 => 94, p095 => 95, + p096 => 96, p097 => 97, p098 => 98, p099 => 99, p100 => 100, p101 => 101, + p102 => 102, p103 => 103, p104 => 104, p105 => 105, p106 => 106, p107 => 107, + p108 => 108, p109 => 109, p110 => 110, p111 => 111, + ]); + check_dragon_scale_fixed_k_window!(w087, 87, [ + p095 => 95, p096 => 96, p097 => 97, p098 => 98, p099 => 99, p100 => 100, + p101 => 101, p102 => 102, p103 => 103, p104 => 104, p105 => 105, p106 => 106, + p107 => 107, p108 => 108, p109 => 109, p110 => 110, p111 => 111, p112 => 112, + p113 => 113, p114 => 114, p115 => 115, p116 => 116, + ]); + check_dragon_scale_fixed_k_window!(w088, 88, [ + p100 => 100, p101 => 101, p102 => 102, p103 => 103, p104 => 104, p105 => 105, + p106 => 106, p107 => 107, p108 => 108, p109 => 109, p110 => 110, p111 => 111, + p112 => 112, p113 => 113, p114 => 114, p115 => 115, p116 => 116, p117 => 117, + p118 => 118, p119 => 119, p120 => 120, p121 => 121, + ]); + check_dragon_scale_fixed_k_window!(w089, 89, [ + p105 => 105, p106 => 106, p107 => 107, p108 => 108, p109 => 109, p110 => 110, + p111 => 111, p112 => 112, p113 => 113, p114 => 114, p115 => 115, p116 => 116, + p117 => 117, p118 => 118, p119 => 119, p120 => 120, p121 => 121, p122 => 122, + p123 => 123, p124 => 124, p125 => 125, + ]); + check_dragon_scale_fixed_k_window!(w090, 90, [ + p110 => 110, p111 => 111, p112 => 112, p113 => 113, p114 => 114, p115 => 115, + p116 => 116, p117 => 117, p118 => 118, p119 => 119, p120 => 120, p121 => 121, + p122 => 122, p123 => 123, p124 => 124, p125 => 125, p126 => 126, p127 => 127, + p128 => 128, p129 => 129, p130 => 130, + ]); + check_dragon_scale_fixed_k_window!(w091, 91, [ + p114 => 114, p115 => 115, p116 => 116, p117 => 117, p118 => 118, p119 => 119, + p120 => 120, p121 => 121, p122 => 122, p123 => 123, p124 => 124, p125 => 125, + p126 => 126, p127 => 127, p128 => 128, p129 => 129, p130 => 130, p131 => 131, + p132 => 132, p133 => 133, p134 => 134, p135 => 135, + ]); + check_dragon_scale_fixed_k_window!(w092, 92, [ + p119 => 119, p120 => 120, p121 => 121, p122 => 122, p123 => 123, p124 => 124, + p125 => 125, p126 => 126, p127 => 127, p128 => 128, p129 => 129, p130 => 130, + p131 => 131, p132 => 132, p133 => 133, p134 => 134, p135 => 135, p136 => 136, + p137 => 137, p138 => 138, p139 => 139, p140 => 140, + ]); + check_dragon_scale_fixed_k_window!(w093, 93, [ + p124 => 124, p125 => 125, p126 => 126, p127 => 127, p128 => 128, p129 => 129, + p130 => 130, p131 => 131, p132 => 132, p133 => 133, p134 => 134, p135 => 135, + p136 => 136, p137 => 137, p138 => 138, p139 => 139, p140 => 140, p141 => 141, + p142 => 142, p143 => 143, p144 => 144, p145 => 145, + ]); + check_dragon_scale_fixed_k_window!(w094, 94, [ + p129 => 129, p130 => 130, p131 => 131, p132 => 132, p133 => 133, p134 => 134, + p135 => 135, p136 => 136, p137 => 137, p138 => 138, p139 => 139, p140 => 140, + p141 => 141, p142 => 142, p143 => 143, p144 => 144, p145 => 145, p146 => 146, + p147 => 147, p148 => 148, p149 => 149, + ]); + check_dragon_scale_fixed_k_window!(w095, 95, [ + p134 => 134, p135 => 135, p136 => 136, p137 => 137, p138 => 138, p139 => 139, + p140 => 140, p141 => 141, p142 => 142, p143 => 143, p144 => 144, p145 => 145, + p146 => 146, p147 => 147, p148 => 148, p149 => 149, p150 => 150, p151 => 151, + p152 => 152, p153 => 153, p154 => 154, + ]); + check_dragon_scale_fixed_k_window!(w096, 96, [ + p139 => 139, p140 => 140, p141 => 141, p142 => 142, p143 => 143, p144 => 144, + p145 => 145, p146 => 146, p147 => 147, p148 => 148, p149 => 149, p150 => 150, + p151 => 151, p152 => 152, p153 => 153, p154 => 154, p155 => 155, p156 => 156, + p157 => 157, p158 => 158, p159 => 159, + ]); + check_dragon_scale_fixed_k_window!(w097, 97, [ + p143 => 143, p144 => 144, p145 => 145, p146 => 146, p147 => 147, p148 => 148, + p149 => 149, p150 => 150, p151 => 151, p152 => 152, p153 => 153, p154 => 154, + p155 => 155, p156 => 156, p157 => 157, p158 => 158, p159 => 159, p160 => 160, + p161 => 161, p162 => 162, p163 => 163, p164 => 164, + ]); + check_dragon_scale_fixed_k_window!(w098, 98, [ + p148 => 148, p149 => 149, p150 => 150, p151 => 151, p152 => 152, p153 => 153, + p154 => 154, p155 => 155, p156 => 156, p157 => 157, p158 => 158, p159 => 159, + p160 => 160, p161 => 161, p162 => 162, p163 => 163, p164 => 164, p165 => 165, + p166 => 166, p167 => 167, p168 => 168, p169 => 169, + ]); + check_dragon_scale_fixed_k_window!(w099, 99, [ + p153 => 153, p154 => 154, p155 => 155, p156 => 156, p157 => 157, p158 => 158, + p159 => 159, p160 => 160, p161 => 161, p162 => 162, p163 => 163, p164 => 164, + p165 => 165, p166 => 166, p167 => 167, p168 => 168, p169 => 169, p170 => 170, + p171 => 171, p172 => 172, p173 => 173, + ]); + check_dragon_scale_fixed_k_window!(w100, 100, [ + p158 => 158, p159 => 159, p160 => 160, p161 => 161, p162 => 162, p163 => 163, + p164 => 164, p165 => 165, p166 => 166, p167 => 167, p168 => 168, p169 => 169, + p170 => 170, p171 => 171, p172 => 172, p173 => 173, p174 => 174, p175 => 175, + p176 => 176, p177 => 177, p178 => 178, + ]); + check_dragon_scale_fixed_k_window!(w101, 101, [ + p163 => 163, p164 => 164, p165 => 165, p166 => 166, p167 => 167, p168 => 168, + p169 => 169, p170 => 170, p171 => 171, p172 => 172, p173 => 173, p174 => 174, + p175 => 175, p176 => 176, p177 => 177, p178 => 178, p179 => 179, p180 => 180, + p181 => 181, p182 => 182, p183 => 183, + ]); + check_dragon_scale_fixed_k_window!(w102, 102, [ + p167 => 167, p168 => 168, p169 => 169, p170 => 170, p171 => 171, p172 => 172, + p173 => 173, p174 => 174, p175 => 175, p176 => 176, p177 => 177, p178 => 178, + p179 => 179, p180 => 180, p181 => 181, p182 => 182, p183 => 183, p184 => 184, + p185 => 185, p186 => 186, p187 => 187, p188 => 188, + ]); + check_dragon_scale_fixed_k_window!(w103, 103, [ + p172 => 172, p173 => 173, p174 => 174, p175 => 175, p176 => 176, p177 => 177, + p178 => 178, p179 => 179, p180 => 180, p181 => 181, p182 => 182, p183 => 183, + p184 => 184, p185 => 185, p186 => 186, p187 => 187, p188 => 188, p189 => 189, + p190 => 190, p191 => 191, p192 => 192, p193 => 193, + ]); + check_dragon_scale_fixed_k_window!(w104, 104, [ + p177 => 177, p178 => 178, p179 => 179, p180 => 180, p181 => 181, p182 => 182, + p183 => 183, p184 => 184, p185 => 185, p186 => 186, p187 => 187, p188 => 188, + p189 => 189, p190 => 190, p191 => 191, p192 => 192, p193 => 193, p194 => 194, + p195 => 195, p196 => 196, p197 => 197, p198 => 198, + ]); + check_dragon_scale_fixed_k_window!(w105, 105, [ + p182 => 182, p183 => 183, p184 => 184, p185 => 185, p186 => 186, p187 => 187, + p188 => 188, p189 => 189, p190 => 190, p191 => 191, p192 => 192, p193 => 193, + p194 => 194, p195 => 195, p196 => 196, p197 => 197, p198 => 198, p199 => 199, + p200 => 200, p201 => 201, p202 => 202, + ]); + check_dragon_scale_fixed_k_window!(w106, 106, [ + p187 => 187, p188 => 188, p189 => 189, p190 => 190, p191 => 191, p192 => 192, + p193 => 193, p194 => 194, p195 => 195, p196 => 196, p197 => 197, p198 => 198, + p199 => 199, p200 => 200, p201 => 201, p202 => 202, p203 => 203, p204 => 204, + p205 => 205, p206 => 206, p207 => 207, + ]); + check_dragon_scale_fixed_k_window!(w107, 107, [ + p192 => 192, p193 => 193, p194 => 194, p195 => 195, p196 => 196, p197 => 197, + p198 => 198, p199 => 199, p200 => 200, p201 => 201, p202 => 202, p203 => 203, + p204 => 204, p205 => 205, p206 => 206, p207 => 207, p208 => 208, p209 => 209, + p210 => 210, p211 => 211, p212 => 212, + ]); + check_dragon_scale_fixed_k_window!(w108, 108, [ + p196 => 196, p197 => 197, p198 => 198, p199 => 199, p200 => 200, p201 => 201, + p202 => 202, p203 => 203, p204 => 204, p205 => 205, p206 => 206, p207 => 207, + p208 => 208, p209 => 209, p210 => 210, p211 => 211, p212 => 212, p213 => 213, + p214 => 214, p215 => 215, p216 => 216, p217 => 217, + ]); + check_dragon_scale_fixed_k_window!(w109, 109, [ + p201 => 201, p202 => 202, p203 => 203, p204 => 204, p205 => 205, p206 => 206, + p207 => 207, p208 => 208, p209 => 209, p210 => 210, p211 => 211, p212 => 212, + p213 => 213, p214 => 214, p215 => 215, p216 => 216, p217 => 217, p218 => 218, + p219 => 219, p220 => 220, p221 => 221, p222 => 222, + ]); + check_dragon_scale_fixed_k_window!(w110, 110, [ + p206 => 206, p207 => 207, p208 => 208, p209 => 209, p210 => 210, p211 => 211, + p212 => 212, p213 => 213, p214 => 214, p215 => 215, p216 => 216, p217 => 217, + p218 => 218, p219 => 219, p220 => 220, p221 => 221, p222 => 222, p223 => 223, + p224 => 224, p225 => 225, p226 => 226, + ]); + check_dragon_scale_fixed_k_window!(w111, 111, [ + p211 => 211, p212 => 212, p213 => 213, p214 => 214, p215 => 215, p216 => 216, + p217 => 217, p218 => 218, p219 => 219, p220 => 220, p221 => 221, p222 => 222, + p223 => 223, p224 => 224, p225 => 225, p226 => 226, p227 => 227, p228 => 228, + p229 => 229, p230 => 230, p231 => 231, + ]); + check_dragon_scale_fixed_k_window!(w112, 112, [ + p216 => 216, p217 => 217, p218 => 218, p219 => 219, p220 => 220, p221 => 221, + p222 => 222, p223 => 223, p224 => 224, p225 => 225, p226 => 226, p227 => 227, + p228 => 228, p229 => 229, p230 => 230, p231 => 231, p232 => 232, p233 => 233, + p234 => 234, p235 => 235, p236 => 236, + ]); + check_dragon_scale_fixed_k_window!(w113, 113, [ + p220 => 220, p221 => 221, p222 => 222, p223 => 223, p224 => 224, p225 => 225, + p226 => 226, p227 => 227, p228 => 228, p229 => 229, p230 => 230, p231 => 231, + p232 => 232, p233 => 233, p234 => 234, p235 => 235, p236 => 236, p237 => 237, + p238 => 238, p239 => 239, p240 => 240, p241 => 241, + ]); + check_dragon_scale_fixed_k_window!(w114, 114, [ + p225 => 225, p226 => 226, p227 => 227, p228 => 228, p229 => 229, p230 => 230, + p231 => 231, p232 => 232, p233 => 233, p234 => 234, p235 => 235, p236 => 236, + p237 => 237, p238 => 238, p239 => 239, p240 => 240, p241 => 241, p242 => 242, + p243 => 243, p244 => 244, p245 => 245, p246 => 246, + ]); + check_dragon_scale_fixed_k_window!(w115, 115, [ + p230 => 230, p231 => 231, p232 => 232, p233 => 233, p234 => 234, p235 => 235, + p236 => 236, p237 => 237, p238 => 238, p239 => 239, p240 => 240, p241 => 241, + p242 => 242, p243 => 243, p244 => 244, p245 => 245, p246 => 246, p247 => 247, + p248 => 248, p249 => 249, p250 => 250, p251 => 251, + ]); + check_dragon_scale_fixed_k_window!(w116, 116, [ + p235 => 235, p236 => 236, p237 => 237, p238 => 238, p239 => 239, p240 => 240, + p241 => 241, p242 => 242, p243 => 243, p244 => 244, p245 => 245, p246 => 246, + p247 => 247, p248 => 248, p249 => 249, p250 => 250, p251 => 251, p252 => 252, + p253 => 253, p254 => 254, p255 => 255, + ]); + check_dragon_scale_fixed_k_window!(w117, 117, [ + p240 => 240, p241 => 241, p242 => 242, p243 => 243, p244 => 244, p245 => 245, + p246 => 246, p247 => 247, p248 => 248, p249 => 249, p250 => 250, p251 => 251, + p252 => 252, p253 => 253, p254 => 254, p255 => 255, p256 => 256, p257 => 257, + p258 => 258, p259 => 259, p260 => 260, + ]); + check_dragon_scale_fixed_k_window!(w118, 118, [ + p245 => 245, p246 => 246, p247 => 247, p248 => 248, p249 => 249, p250 => 250, + p251 => 251, p252 => 252, p253 => 253, p254 => 254, p255 => 255, p256 => 256, + p257 => 257, p258 => 258, p259 => 259, p260 => 260, p261 => 261, p262 => 262, + p263 => 263, p264 => 264, p265 => 265, + ]); + check_dragon_scale_fixed_k_window!(w119, 119, [ + p249 => 249, p250 => 250, p251 => 251, p252 => 252, p253 => 253, p254 => 254, + p255 => 255, p256 => 256, p257 => 257, p258 => 258, p259 => 259, p260 => 260, + p261 => 261, p262 => 262, p263 => 263, p264 => 264, p265 => 265, p266 => 266, + p267 => 267, p268 => 268, p269 => 269, p270 => 270, + ]); + check_dragon_scale_fixed_k_window!(w120, 120, [ + p254 => 254, p255 => 255, p256 => 256, p257 => 257, p258 => 258, p259 => 259, + p260 => 260, p261 => 261, p262 => 262, p263 => 263, p264 => 264, p265 => 265, + p266 => 266, p267 => 267, p268 => 268, p269 => 269, p270 => 270, p271 => 271, + p272 => 272, p273 => 273, p274 => 274, p275 => 275, + ]); + check_dragon_scale_fixed_k_window!(w121, 121, [ + p259 => 259, p260 => 260, p261 => 261, p262 => 262, p263 => 263, p264 => 264, + p265 => 265, p266 => 266, p267 => 267, p268 => 268, p269 => 269, p270 => 270, + p271 => 271, p272 => 272, p273 => 273, p274 => 274, p275 => 275, p276 => 276, + p277 => 277, p278 => 278, p279 => 279, + ]); + check_dragon_scale_fixed_k_window!(w122, 122, [ + p264 => 264, p265 => 265, p266 => 266, p267 => 267, p268 => 268, p269 => 269, + p270 => 270, p271 => 271, p272 => 272, p273 => 273, p274 => 274, p275 => 275, + p276 => 276, p277 => 277, p278 => 278, p279 => 279, p280 => 280, p281 => 281, + p282 => 282, p283 => 283, p284 => 284, + ]); + check_dragon_scale_fixed_k_window!(w123, 123, [ + p269 => 269, p270 => 270, p271 => 271, p272 => 272, p273 => 273, p274 => 274, + p275 => 275, p276 => 276, p277 => 277, p278 => 278, p279 => 279, p280 => 280, + p281 => 281, p282 => 282, p283 => 283, p284 => 284, p285 => 285, p286 => 286, + p287 => 287, p288 => 288, p289 => 289, + ]); + check_dragon_scale_fixed_k_window!(w124, 124, [ + p273 => 273, p274 => 274, p275 => 275, p276 => 276, p277 => 277, p278 => 278, + p279 => 279, p280 => 280, p281 => 281, p282 => 282, p283 => 283, p284 => 284, + p285 => 285, p286 => 286, p287 => 287, p288 => 288, p289 => 289, p290 => 290, + p291 => 291, p292 => 292, p293 => 293, p294 => 294, + ]); + check_dragon_scale_fixed_k_window!(w125, 125, [ + p278 => 278, p279 => 279, p280 => 280, p281 => 281, p282 => 282, p283 => 283, + p284 => 284, p285 => 285, p286 => 286, p287 => 287, p288 => 288, p289 => 289, + p290 => 290, p291 => 291, p292 => 292, p293 => 293, p294 => 294, p295 => 295, + p296 => 296, p297 => 297, p298 => 298, p299 => 299, + ]); + check_dragon_scale_fixed_k_window!(w126, 126, [ + p283 => 283, p284 => 284, p285 => 285, p286 => 286, p287 => 287, p288 => 288, + p289 => 289, p290 => 290, p291 => 291, p292 => 292, p293 => 293, p294 => 294, + p295 => 295, p296 => 296, p297 => 297, p298 => 298, p299 => 299, p300 => 300, + p301 => 301, p302 => 302, p303 => 303, p304 => 304, + ]); + check_dragon_scale_fixed_k_window!(w127, 127, [ + p288 => 288, p289 => 289, p290 => 290, p291 => 291, p292 => 292, p293 => 293, + p294 => 294, p295 => 295, p296 => 296, p297 => 297, p298 => 298, p299 => 299, + p300 => 300, p301 => 301, p302 => 302, p303 => 303, p304 => 304, p305 => 305, + p306 => 306, p307 => 307, p308 => 308, + ]); + } + + // Every contract input of windows 000 to 127 has its estimate in the window range, so + // the fixed-K proofs of each K in that range cover the window. + #[kani::proof] + fn check_dragon_scale_k_range() { + let mant = kani::any::() & ((1u64 << 55) - 1); + let plus: u64 = if kani::any::() { 1 } else { 2 }; + kani::assume(mant >= 2); + kani::assume(plus == 1 || mant == 1 << 25 || mant == 1 << 54); + kani::assume(mant as u128 + plus as u128 <= ((1 + plus as u128) << 53)); + let window = kani::any::(); + kani::assume(window <= 127); + let first = -1076 + 16 * i16::from(window); + let exp = first + i16::from(kani::any::() & 15); + kani::assume(exp <= fixed_k_last(first)); + let k = + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor(mant + plus, exp); + assert!(fixed_k_min(first) <= k && k <= fixed_k_max(first)); + kani::cover(window == 0, "the K range includes the lowest window"); + kani::cover(window == 127, "the K range includes the last fixed-K window"); + } + + // Only the additional [1, 2) probes use this adapter. Check the scaling + // value against the independently proved model for the actual arguments, + // then expose the literal zero before expanding bigint multiplication. + // A nonzero estimate fails the proof; it is never assumed away. + fn estimate_unit_scale(mant: u64, exp: i16) -> i16 { + assert_eq!( + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor(mant, exp), + 0 + ); + 0 + } + + // Identity instrumentation lets each proof expose constant exponent bits + // before expanding the original multiplication body. The replacement + // checks equality on every call, so an invalid prefix fails verification. + pub(super) fn proof_pow10_exponent(n: usize) -> usize { + n + } + + struct FixedExponentBits(usize); + struct VariableExponentBits(usize); + + struct Pow10Prefix { + fixed: FixedExponentBits, + variable: VariableExponentBits, + } + + impl Pow10Prefix { + const fn for_range(range: crate::ops::RangeInclusive) -> Self { + let lower = *range.start(); + let upper = *range.end(); + assert!(lower <= upper); + let different = lower ^ upper; + let variable = if different == 0 { 0 } else { usize::MAX >> different.leading_zeros() }; + Self { + fixed: FixedExponentBits(lower & !variable), + variable: VariableExponentBits(variable), + } + } + + fn check(&self, n: usize) -> usize { + let encoded = self.fixed.0 | (n & self.variable.0); + assert_eq!(encoded, n); + encoded + } + } + + // For normal inputs with exponent field e, the estimator uses a binary + // exponent sum between e - bias and e - bias + 1. Group zero also includes + // subnormals, down to -149 for f32 and -1074 for f64. Applying the original + // integer scaling formula to those endpoints gives these magnitude ranges. + // They only choose an encoding; check() proves it equals the actual value. + const F32_POW10_PREFIXES: [Pow10Prefix; 4] = [ + Pow10Prefix::for_range(19..=45), + Pow10Prefix::for_range(0..=19), + Pow10Prefix::for_range(0..=19), + Pow10Prefix::for_range(19..=38), + ]; + + const F64_POW10_PREFIXES: [Pow10Prefix; 32] = [ + Pow10Prefix::for_range(289..=324), + Pow10Prefix::for_range(270..=289), + Pow10Prefix::for_range(251..=270), + Pow10Prefix::for_range(231..=251), + Pow10Prefix::for_range(212..=231), + Pow10Prefix::for_range(193..=212), + Pow10Prefix::for_range(174..=193), + Pow10Prefix::for_range(154..=174), + Pow10Prefix::for_range(135..=154), + Pow10Prefix::for_range(116..=135), + Pow10Prefix::for_range(97..=116), + Pow10Prefix::for_range(77..=97), + Pow10Prefix::for_range(58..=77), + Pow10Prefix::for_range(39..=58), + Pow10Prefix::for_range(19..=39), + Pow10Prefix::for_range(0..=19), + Pow10Prefix::for_range(0..=19), + Pow10Prefix::for_range(19..=38), + Pow10Prefix::for_range(38..=58), + Pow10Prefix::for_range(58..=77), + Pow10Prefix::for_range(77..=96), + Pow10Prefix::for_range(96..=115), + Pow10Prefix::for_range(115..=135), + Pow10Prefix::for_range(135..=154), + Pow10Prefix::for_range(154..=173), + Pow10Prefix::for_range(173..=192), + Pow10Prefix::for_range(192..=212), + Pow10Prefix::for_range(212..=231), + Pow10Prefix::for_range(231..=250), + Pow10Prefix::for_range(250..=270), + Pow10Prefix::for_range(270..=289), + Pow10Prefix::for_range(289..=308), + ]; + + fn checked_pow10_f32(n: usize) -> usize { + F32_POW10_PREFIXES[GROUP].check(n) + } + + fn checked_pow10_f64(n: usize) -> usize { + F64_POW10_PREFIXES[GROUP].check(n) + } + + fn checked_pow10_unit(n: usize) -> usize { + assert_eq!(EXPONENT, 1023); + Pow10Prefix::for_range(0..=0).check(n) + } + + struct LimbCountMask(usize); + + impl LimbCountMask { + fn check(&self, size: usize) -> usize { + let encoded = size & self.0; + assert_eq!(encoded, size); + encoded + } + } + + fn checked_f32_limb_count(size: usize) -> usize { + // Scaled f32 values fit in five 32-bit limbs. The division abstraction + // can also add a carry limb when its result is added to the mantissa. + LimbCountMask(7).check(size) + } + + fn checked_f64_limb_count(size: usize) -> usize { + let first_exponent = 64 * GROUP as i32; + let last_exponent = first_exponent + 63; + let value_bits = (1076 - first_exponent).max(last_exponent - 1021).max(64) as usize; + // Reserve sixteen bits for decimal scaling and digit extraction, + // and a full carry limb for the division abstraction's addition. + let limb_bound = (value_bits + 16).div_ceil(Digit::BITS as usize) + 1; + LimbCountMask((limb_bound + 1).next_power_of_two() - 1).check(size) + } + + // Only the two unit-exponent diagnostics use these checked count encodings. + // A count outside the mask fails an assertion, without excluding any input. + fn unit_shortest_limb_count(size: usize) -> usize { + LimbCountMask(if Digit::BITS == 64 { 1 } else { 3 }).check(size) + } + + fn unit_exact_limb_count(size: usize) -> usize { + LimbCountMask(if Digit::BITS == 64 { 3 } else { 7 }).check(size) + } + + macro_rules! check_partition { + ($name:ident, arbitrary_finite_f32, $group:literal, $cover_fallback:literal) => { + check_partition!( + $name, + arbitrary_finite_f32, + $group, + $cover_fallback, + 19, + 33, + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor, + checked_pow10_f32, + 255, + checked_f32_limb_count, + checked_f32_limb_count + ); + }; + ($name:ident, $decode:ident, $group:literal, $cover_fallback:literal) => { + check_partition!($name, $decode, $group, $cover_fallback, 41, 41); + }; + ( + $name:ident, $decode:ident, $group:literal, $cover_fallback:literal, + $shortest_unwind:literal, $exact_unwind:literal + ) => { + check_partition!( + $name, + $decode, + $group, + $cover_fallback, + $shortest_unwind, + $exact_unwind, + crate::num::imp::flt2dec::estimator_verify::estimate_scaling_factor, + checked_pow10_f64, + 2047, + partition_limb_count, + partition_limb_count + ); + }; + ( + $name:ident, $decode:ident, $group:literal, $cover_fallback:literal, + $shortest_unwind:literal, $exact_unwind:literal, $estimate:path, $pow10:ident, + $pow2_mask:literal + $(, $shortest_limbs:path, $exact_limbs:path)? + ) => { + mod $name { + use super::*; + + fn partition_limb_count(size: usize) -> usize { + checked_f64_limb_count::<$group>(size) + } + + fn stub_pow10_exponent(n: usize) -> usize { + $pow10::<$group>(n) + } + + fn stub_pow2_bits(bits: usize) -> usize { + let encoded = bits & $pow2_mask; + assert_eq!(encoded, bits); + encoded + } + + #[kani::proof] + #[kani::unwind($shortest_unwind)] + $( + #[kani::stub(crate::num::imp::bignum::kani_loop_size, $shortest_limbs)] + #[kani::stub(Big::add, Big::kani_add_model)] + #[kani::stub(Big::sub, Big::kani_sub_model)] + #[kani::stub(Big::mul_small, Big::kani_mul_small_model)] + #[kani::stub(Big::mul_pow2, Big::kani_mul_pow2_model)] + )? + #[kani::stub(proof_pow10_exponent, stub_pow10_exponent)] + #[kani::stub(crate::num::imp::bignum::kani_pow2_bits, stub_pow2_bits)] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul, carrying_mul_model))] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul_add, carrying_mul_add_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul, carrying_mul_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul_add, carrying_mul_add_model))] + #[kani::stub(crate::num::imp::flt2dec::estimator::estimate_scaling_factor, $estimate)] + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::stub(::cmp, stub_cmp)] + #[kani::stub(Big::is_zero, stub_is_zero)] + #[kani::stub( + crate::num::imp::flt2dec::round_up, + crate::num::imp::flt2dec::rounding_verify::stub_round_up + )] + #[kani::stub_verified(crate::num::imp::flt2dec::rounding_verify::round_up_contract)] + #[kani::solver(kissat)] + fn check_format_shortest() { + let d = $decode::<$group>(); + let len = usize::from(kani::any::()); + kani::assume(len >= MAX_SIG_DIGITS && len <= PROOF_BUFLEN); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_ptr().cast::(); + kani::cover(len == MAX_SIG_DIGITS, "shortest uses the minimum buffer"); + kani::cover(len == PROOF_BUFLEN, "shortest uses the largest proof buffer"); + let (digits, _) = format_shortest(&d, &mut buf[..len]); + kani::cover(digits.len() > 1, "shortest produces multiple digits"); + assert!(!digits.is_empty()); + assert!(digits.len() <= len); + assert_eq!(digits.as_ptr(), start); + } + + #[kani::proof] + #[kani::unwind($exact_unwind)] + $( + #[kani::stub(crate::num::imp::bignum::kani_loop_size, $exact_limbs)] + #[kani::stub(Big::add, Big::kani_add_model)] + #[kani::stub(Big::sub, Big::kani_sub_model)] + #[kani::stub(Big::mul_small, Big::kani_mul_small_model)] + #[kani::stub(Big::mul_pow2, Big::kani_mul_pow2_model)] + )? + #[kani::stub(proof_pow10_exponent, stub_pow10_exponent)] + #[kani::stub(crate::num::imp::bignum::kani_pow2_bits, stub_pow2_bits)] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul, carrying_mul_model))] + #[cfg_attr(target_pointer_width = "64", kani::stub(u64::carrying_mul_add, carrying_mul_add_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul, carrying_mul_model))] + #[cfg_attr(any(target_pointer_width = "16", target_pointer_width = "32"), kani::stub(u32::carrying_mul_add, carrying_mul_add_model))] + #[kani::stub(crate::num::imp::flt2dec::estimator::estimate_scaling_factor, $estimate)] + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::stub(::cmp, stub_cmp)] + #[kani::stub(Big::is_zero, stub_is_zero)] + #[kani::stub( + crate::num::imp::flt2dec::round_up, + crate::num::imp::flt2dec::rounding_verify::stub_round_up + )] + #[kani::stub_verified(crate::num::imp::flt2dec::rounding_verify::round_up_contract)] + #[kani::stub(fill_zeroes, fill_zeroes_model)] + #[kani::stub(div_2pow10, stub_div_2pow10)] + #[kani::stub_verified(div_2pow10_contract)] + #[kani::solver(kissat)] + fn check_format_exact() { + let d = $decode::<$group>(); + let limit: i16 = kani::any(); + let len = usize::from(kani::any::()); + kani::assume(len <= PROOF_BUFLEN); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_ptr().cast::(); + kani::cover(len == 0, "exact accepts an empty buffer"); + kani::cover(len == PROOF_BUFLEN, "exact uses the largest proof buffer"); + let (digits, _) = format_exact(&d, &mut buf[..len], limit); + kani::cover(digits.is_empty(), "exact can return an empty prefix"); + kani::cover(digits.len() > 1, "exact produces multiple digits"); + assert!(digits.len() <= len); + assert_eq!(digits.as_ptr(), start); + } + } + }; + } + + for_each_finite_partition!(check_partition); + // These inputs need fewer bigint limbs. Keep enough iterations for the + // digit and rounding loops; unwinding assertions check every loop bound. + check_partition!( + f64_exp_1023, + arbitrary_finite_f64_exponent, + 1023, + false, + 19, + 33, + estimate_unit_scale, + checked_pow10_unit, + 63, + unit_shortest_limb_count, + unit_exact_limb_count + ); } diff --git a/library/core/src/num/imp/flt2dec/strategy/grisu.rs b/library/core/src/num/imp/flt2dec/strategy/grisu.rs index f7ee465829340..f9aa01f8b0609 100644 --- a/library/core/src/num/imp/flt2dec/strategy/grisu.rs +++ b/library/core/src/num/imp/flt2dec/strategy/grisu.rs @@ -7,6 +7,8 @@ use flt2dec::{Decoded, MAX_SIG_DIGITS, round_up}; +#[cfg(kani)] +use crate::kani; use crate::mem::MaybeUninit; use crate::num::imp::diy_float::Fp; use crate::num::imp::flt2dec; @@ -120,13 +122,28 @@ pub const CACHED_POW10_LAST_E: i16 = 1039; #[doc(hidden)] pub fn cached_power(alpha: i16, gamma: i16) -> (i16, Fp) { + let (f, e, k) = CACHED_POW10[cached_power_index(alpha, gamma)]; + (k, Fp { f, e }) +} + +#[inline] +fn cached_power_index(alpha: i16, gamma: i16) -> usize { let offset = CACHED_POW10_FIRST_E as i32; let range = (CACHED_POW10.len() as i32) - 1; let domain = (CACHED_POW10_LAST_E - CACHED_POW10_FIRST_E) as i32; let idx = ((gamma as i32) - offset) * range / domain; - let (f, e, k) = CACHED_POW10[idx as usize]; + #[cfg(kani)] + let idx = proof_cached_power_index(idx); + let (_, e, _) = CACHED_POW10[idx as usize]; debug_assert!(alpha <= e && e <= gamma); - (k, Fp { f, e }) + idx as usize +} + +// Diagnostics can expose a constant table entry after asserting its index. +// The original index calculation and table lookup still execute. +#[cfg(kani)] +fn proof_cached_power_index(index: i32) -> i32 { + index } /// Given `x > 0`, returns `(k, 10^k)` such that `10^k <= x < 10^(k+1)`. @@ -168,18 +185,64 @@ pub fn format_shortest_opt<'a>( d: &Decoded, buf: &'a mut [MaybeUninit], ) -> Option<(/*digits*/ &'a [u8], /*exp*/ i16)> { - assert!(d.mant > 0); - assert!(d.minus > 0); - assert!(d.plus > 0); - assert!(d.mant.checked_add(d.plus).is_some()); - assert!(d.mant.checked_sub(d.minus).is_some()); - assert!(buf.len() >= MAX_SIG_DIGITS); - assert!(d.mant + d.plus < (1 << 61)); // we need at least three bits of additional precision + let rounding = generate_shortest(d, buf); + round_and_weed( + rounding.digits, + rounding.exp, + rounding.remainder, + rounding.threshold, + rounding.plus1v, + rounding.ten_kappa, + rounding.ulp, + ) +} + +// Keep the initialized prefix and numeric state together for one final call. +// Generation keeps its existing stopping conditions and argument order. +struct ShortestRounding<'a> { + digits: &'a mut [u8], + exp: i16, + remainder: u64, + threshold: u64, + plus1v: u64, + ten_kappa: u64, + ulp: u64, +} + +#[inline] +fn div_rem_pow10(value: u32, divisor: u32) -> (u32, u32) { + (value / divisor, value % divisor) +} +struct ShortestScaled { + plus: Fp, + minus: Fp, + v: Fp, + minusk: i16, +} + +#[inline] +fn normalize_shortest(d: &Decoded) -> (Fp, Fp, Fp) { // start with the normalized values with the shared exponent let plus = Fp { f: d.mant + d.plus, e: d.exp }.normalize(); let minus = Fp { f: d.mant - d.minus, e: d.exp }.normalize_to(plus.e); let v = Fp { f: d.mant, e: d.exp }.normalize_to(plus.e); + (plus, minus, v) +} + +#[inline] +fn multiply_shortest(plus: Fp, minus: Fp, v: Fp, index: usize) -> (Fp, Fp, Fp) { + let (f, e, _) = CACHED_POW10[index]; + let cached = Fp { f, e }; + (plus.mul(cached), minus.mul(cached), v.mul(cached)) +} + +#[inline] +fn scale_shortest(d: &Decoded) -> ShortestScaled { + #[cfg(not(kani))] + let (plus, minus, v) = normalize_shortest(d); + #[cfg(kani)] + let (plus, minus, v) = grisu_verify::normalize_shortest_for_proof(d); // find any `cached = 10^minusk` such that `ALPHA <= minusk + plus.e + 64 <= GAMMA`. // since `plus` is normalized, this means `2^(62 + ALPHA) <= plus * cached < 2^(64 + GAMMA)`; @@ -195,12 +258,74 @@ pub fn format_shortest_opt<'a>( // // the first gives `64 + GAMMA <= 32`, while the second gives `10 * 2^-ALPHA <= 2^64`; // -60 and -32 is the maximal range with this constraint, and V8 also uses them. - let (minusk, cached) = cached_power(ALPHA - plus.e - 64, GAMMA - plus.e - 64); + let index = cached_power_index(ALPHA - plus.e - 64, GAMMA - plus.e - 64); + let (_, _, minusk) = CACHED_POW10[index]; // scale fps. this gives the maximal error of 1 ulp (proved from Theorem 5.1). - let plus = plus.mul(cached); - let minus = minus.mul(cached); - let v = v.mul(cached); + let (plus, minus, v) = multiply_shortest(plus, minus, v, index); + ShortestScaled { plus, minus, v, minusk } +} + +fn generate_integral_digits( + plus1int: u32, + plus1frac: u64, + delta1: u64, + e: usize, + max_kappa: usize, + max_ten_kappa: u32, + buf: &mut [MaybeUninit], +) -> (usize, Option<(u64, u64)>) { + let mut i = 0; + let mut ten_kappa = max_ten_kappa; + let mut remainder = plus1int; + // The loop contract takes this local's address before the first digit. + #[cfg(kani)] + let mut plus1rem = 0; + #[cfg(not(kani))] + let mut plus1rem; + #[cfg_attr(kani, kani::loop_invariant( + i <= max_kappa as usize + && ten_kappa as u64 == grisu_verify::proof_power10(max_kappa as usize - i) + && ten_kappa <= max_ten_kappa + && (remainder as u64) < 10 * ten_kappa as u64 + && remainder <= plus1int + && ((remainder as u128) << e) + plus1frac as u128 >= delta1 as u128 + && grisu_verify::initialized_digits(buf, i) + ))] + #[cfg_attr(kani, kani::loop_modifies( + &i, &ten_kappa, &remainder, &plus1rem, + crate::ptr::slice_from_raw_parts(buf.as_ptr(), MAX_SIG_DIGITS) + ))] + loop { + let (q, r) = div_rem_pow10(remainder, ten_kappa); + debug_assert!(q < 10); + buf[i] = MaybeUninit::new(b'0' + q as u8); + i += 1; + plus1rem = ((r as u64) << e) + plus1frac; + if plus1rem < delta1 || i > max_kappa as usize { + let rounding = if plus1rem < delta1 { + Some((plus1rem, (ten_kappa as u64) << e)) + } else { + debug_assert_eq!(ten_kappa, 1); + None + }; + break (i, rounding); + } + ten_kappa /= 10; + remainder = r; + } +} + +fn generate_shortest<'a>(d: &Decoded, buf: &'a mut [MaybeUninit]) -> ShortestRounding<'a> { + assert!(d.mant > 0); + assert!(d.minus > 0); + assert!(d.plus > 0); + assert!(d.mant.checked_add(d.plus).is_some()); + assert!(d.mant.checked_sub(d.minus).is_some()); + assert!(buf.len() >= MAX_SIG_DIGITS); + assert!(d.mant + d.plus < (1 << 61)); // we need at least three bits of additional precision + + let ShortestScaled { plus, minus, v, minusk } = scale_shortest(d); debug_assert_eq!(plus.e, minus.e); debug_assert_eq!(plus.e, v.e); @@ -239,7 +364,6 @@ pub fn format_shortest_opt<'a>( // this is an upper bound of `kappa` below. let (max_kappa, max_ten_kappa) = max_pow10_no_more_than(plus1int); - let mut i = 0; let exp = max_kappa as i16 - minusk + 1; // Theorem 6.2: if `k` is the greatest integer s.t. `0 <= y mod 10^k <= y - x`, @@ -255,48 +379,26 @@ pub fn format_shortest_opt<'a>( let delta1frac = delta1 & ((1 << e) - 1); // render integral parts, while checking for the accuracy at each step. - let mut ten_kappa = max_ten_kappa; // 10^kappa - let mut remainder = plus1int; // digits yet to be rendered - loop { - // we always have at least one digit to render, as `plus1 >= 10^kappa` - // invariants: - // - `delta1int <= remainder < 10^(kappa+1)` - // - `plus1int = d[0..n-1] * 10^(kappa+1) + remainder` - // (it follows that `remainder = plus1int % 10^(kappa+1)`) - - // divide `remainder` by `10^kappa`. both are scaled by `2^-e`. - let q = remainder / ten_kappa; - let r = remainder % ten_kappa; - debug_assert!(q < 10); - buf[i] = MaybeUninit::new(b'0' + q as u8); - i += 1; - - let plus1rem = ((r as u64) << e) + plus1frac; // == (plus1 % 10^kappa) * 2^e - if plus1rem < delta1 { - // `plus1 % 10^kappa < delta1 = plus1 - minus1`; we've found the correct `kappa`. - let ten_kappa = (ten_kappa as u64) << e; // scale 10^kappa back to the shared exponent - return round_and_weed( - // SAFETY: we initialized that memory above. - unsafe { buf[..i].assume_init_mut() }, - exp, - plus1rem, - delta1, - plus1 - v.f, - ten_kappa, - 1, - ); - } - - // break the loop when we have rendered all integral digits. - // the exact number of digits is `max_kappa + 1` as `plus1 < 10^(max_kappa+1)`. - if i > max_kappa as usize { - debug_assert_eq!(ten_kappa, 1); - break; - } - - // restore invariants - ten_kappa /= 10; - remainder = r; + let (mut i, integral_rounding) = generate_integral_digits( + plus1int, + plus1frac, + delta1, + e, + max_kappa as usize, + max_ten_kappa, + buf, + ); + if let Some((remainder, ten_kappa)) = integral_rounding { + return ShortestRounding { + // SAFETY: the integral generator initialized this prefix. + digits: unsafe { buf[..i].assume_init_mut() }, + exp, + remainder, + threshold: delta1, + plus1v: plus1 - v.f, + ten_kappa, + ulp: 1, + }; } // render fractional parts, while checking for the accuracy at each step. @@ -304,6 +406,29 @@ pub fn format_shortest_opt<'a>( let mut remainder = plus1frac; let mut threshold = delta1frac; let mut ulp = 1; + #[cfg(kani)] + let mut fractional_digits = 0usize; + // The scaling contract preserves a nonzero suffix in the lower e - 16 + // bits for an asymmetric interval. Each decimal step adds only one factor + // of two, so the suffix stays nonzero below bit e through 16 more digits. + #[cfg_attr(kani, kani::loop_invariant( + i >= max_kappa as usize + 1 && i <= 16 + && fractional_digits <= 15 + && i == max_kappa as usize + 1 + fractional_digits + && remainder < 1u64 << e + && threshold <= remainder + && ulp < 1u64 << 50 + && threshold as u128 == grisu_verify::proof_threshold(delta1frac, fractional_digits) + && ulp == grisu_verify::proof_power10(fractional_digits) + && (2 * ((plus.f - v.f) as u128) < delta1 as u128 + || remainder.wrapping_sub(threshold) + << ((64 - (e - 16 + fractional_digits)) as u8) != 0) + && grisu_verify::initialized_digits(buf, i) + ))] + #[cfg_attr(kani, kani::loop_modifies( + &i, &remainder, &threshold, &ulp, &fractional_digits, + crate::ptr::slice_from_raw_parts(buf.as_ptr(), MAX_SIG_DIGITS) + ))] loop { // the next digit should be significant as we've tested that before breaking out // invariants, where `m = max_kappa + 1` (# of digits in the integral part): @@ -311,143 +436,150 @@ pub fn format_shortest_opt<'a>( // - `plus1frac * 10^(n-m) = d[m..n-1] * 2^e + remainder` remainder *= 10; // won't overflow, `2^e * 10 < 2^64` - threshold *= 10; + #[cfg(kani)] + { + (threshold, fractional_digits) = + grisu_verify::advance_fraction_threshold(delta1frac, fractional_digits, threshold); + } + #[cfg(not(kani))] + { + threshold *= 10; + } ulp *= 10; // divide `remainder` by `10^kappa`. // both are scaled by `2^e / 10^kappa`, so the latter is implicit here. let q = remainder >> e; - let r = remainder & ((1 << e) - 1); + remainder &= (1 << e) - 1; debug_assert!(q < 10); buf[i] = MaybeUninit::new(b'0' + q as u8); i += 1; - if r < threshold { + if remainder < threshold { let ten_kappa = 1 << e; // implicit divisor - return round_and_weed( + return ShortestRounding { // SAFETY: we initialized that memory above. - unsafe { buf[..i].assume_init_mut() }, + digits: unsafe { buf[..i].assume_init_mut() }, exp, - r, + remainder, threshold, - (plus1 - v.f) * ulp, + plus1v: (plus1 - v.f) * ulp, ten_kappa, ulp, - ); + }; } // restore invariants - remainder = r; } +} - // we've generated all significant digits of `plus1`, but not sure if it's the optimal one. - // for example, if `minus1` is 3.14153... and `plus1` is 3.14158..., there are 5 different - // shortest representation from 3.14154 to 3.14158 but we only have the greatest one. - // we have to successively decrease the last digit and check if this is the optimal repr. - // there are at most 9 candidates (..1 to ..9), so this is fairly quick. ("rounding" phase) - // - // the function checks if this "optimal" repr is actually within the ulp ranges, - // and also, it is possible that the "second-to-optimal" repr can actually be optimal - // due to the rounding error. in either cases this returns `None`. ("weeding" phase) - // - // all arguments here are scaled by the common (but implicit) value `k`, so that: - // - `remainder = (plus1 % 10^kappa) * k` - // - `threshold = (plus1 - minus1) * k` (and also, `remainder < threshold`) - // - `plus1v = (plus1 - v) * k` (and also, `threshold > plus1v` from prior invariants) - // - `ten_kappa = 10^kappa * k` - // - `ulp = 2^-e * k` - fn round_and_weed( - buf: &mut [u8], - exp: i16, - remainder: u64, - threshold: u64, - plus1v: u64, - ten_kappa: u64, - ulp: u64, - ) -> Option<(&[u8], i16)> { - assert!(!buf.is_empty()); +// we've generated all significant digits of `plus1`, but not sure if it's the optimal one. +// for example, if `minus1` is 3.14153... and `plus1` is 3.14158..., there are 5 different +// shortest representation from 3.14154 to 3.14158 but we only have the greatest one. +// we have to successively decrease the last digit and check if this is the optimal repr. +// there are at most 9 candidates (..1 to ..9), so this is fairly quick. ("rounding" phase) +// +// the function checks if this "optimal" repr is actually within the ulp ranges, +// and also, it is possible that the "second-to-optimal" repr can actually be optimal +// due to the rounding error. in either cases this returns `None`. ("weeding" phase) +// +// all arguments here are scaled by the common (but implicit) value `k`, so that: +// - `remainder = (plus1 % 10^kappa) * k` +// - `threshold = (plus1 - minus1) * k` (and also, `remainder < threshold`) +// - `plus1v = (plus1 - v) * k` (and also, `threshold > plus1v` from prior invariants) +// - `ten_kappa = 10^kappa * k` +// - `ulp = 2^-e * k` +fn round_and_weed( + buf: &mut [u8], + exp: i16, + remainder: u64, + threshold: u64, + plus1v: u64, + ten_kappa: u64, + ulp: u64, +) -> Option<(&[u8], i16)> { + assert!(!buf.is_empty()); - // produce two approximations to `v` (actually `plus1 - v`) within 1.5 ulps. - // the resulting representation should be the closest representation to both. + // produce two approximations to `v` (actually `plus1 - v`) within 1.5 ulps. + // the resulting representation should be the closest representation to both. + // + // here `plus1 - v` is used since calculations are done with respect to `plus1` + // in order to avoid overflow/underflow (hence the seemingly swapped names). + let plus1v_down = plus1v + ulp; // plus1 - (v - 1 ulp) + let plus1v_up = plus1v - ulp; // plus1 - (v + 1 ulp) + + // decrease the last digit and stop at the closest representation to `v + 1 ulp`. + let mut plus1w = remainder; // plus1w(n) = plus1 - w(n) + { + let last = buf.last_mut().unwrap(); + + // we work with the approximated digits `w(n)`, which is initially equal to `plus1 - + // plus1 % 10^kappa`. after running the loop body `n` times, `w(n) = plus1 - + // plus1 % 10^kappa - n * 10^kappa`. we set `plus1w(n) = plus1 - w(n) = + // plus1 % 10^kappa + n * 10^kappa` (thus `remainder = plus1w(0)`) to simplify checks. + // note that `plus1w(n)` is always increasing. // - // here `plus1 - v` is used since calculations are done with respect to `plus1` - // in order to avoid overflow/underflow (hence the seemingly swapped names). - let plus1v_down = plus1v + ulp; // plus1 - (v - 1 ulp) - let plus1v_up = plus1v - ulp; // plus1 - (v + 1 ulp) - - // decrease the last digit and stop at the closest representation to `v + 1 ulp`. - let mut plus1w = remainder; // plus1w(n) = plus1 - w(n) - { - let last = buf.last_mut().unwrap(); - - // we work with the approximated digits `w(n)`, which is initially equal to `plus1 - - // plus1 % 10^kappa`. after running the loop body `n` times, `w(n) = plus1 - - // plus1 % 10^kappa - n * 10^kappa`. we set `plus1w(n) = plus1 - w(n) = - // plus1 % 10^kappa + n * 10^kappa` (thus `remainder = plus1w(0)`) to simplify checks. - // note that `plus1w(n)` is always increasing. - // - // we have three conditions to terminate. any of them will make the loop unable to - // proceed, but we then have at least one valid representation known to be closest to - // `v + 1 ulp` anyway. we will denote them as TC1 through TC3 for brevity. - // - // TC1: `w(n) <= v + 1 ulp`, i.e., this is the last repr that can be the closest one. - // this is equivalent to `plus1 - w(n) = plus1w(n) >= plus1 - (v + 1 ulp) = plus1v_up`. - // combined with TC2 (which checks if `w(n+1)` is valid), this prevents the possible - // overflow on the calculation of `plus1w(n)`. - // - // TC2: `w(n+1) < minus1`, i.e., the next repr definitely does not round to `v`. - // this is equivalent to `plus1 - w(n) + 10^kappa = plus1w(n) + 10^kappa > - // plus1 - minus1 = threshold`. the left hand side can overflow, but we know - // `threshold > plus1v`, so if TC1 is false, `threshold - plus1w(n) > - // threshold - (plus1v - 1 ulp) > 1 ulp` and we can safely test if - // `threshold - plus1w(n) < 10^kappa` instead. - // - // TC3: `abs(w(n) - (v + 1 ulp)) <= abs(w(n+1) - (v + 1 ulp))`, i.e., the next repr is - // no closer to `v + 1 ulp` than the current repr. given `z(n) = plus1v_up - plus1w(n)`, - // this becomes `abs(z(n)) <= abs(z(n+1))`. again assuming that TC1 is false, we have - // `z(n) > 0`. we have two cases to consider: - // - // - when `z(n+1) >= 0`: TC3 becomes `z(n) <= z(n+1)`. as `plus1w(n)` is increasing, - // `z(n)` should be decreasing and this is clearly false. - // - when `z(n+1) < 0`: - // - TC3a: the precondition is `plus1v_up < plus1w(n) + 10^kappa`. assuming TC2 is - // false, `threshold >= plus1w(n) + 10^kappa` so it cannot overflow. - // - TC3b: TC3 becomes `z(n) <= -z(n+1)`, i.e., `plus1v_up - plus1w(n) >= - // plus1w(n+1) - plus1v_up = plus1w(n) + 10^kappa - plus1v_up`. the negated TC1 - // gives `plus1v_up > plus1w(n)`, so it cannot overflow or underflow when - // combined with TC3a. - // - // consequently, we should stop when `TC1 || TC2 || (TC3a && TC3b)`. the following is - // equal to its inverse, `!TC1 && !TC2 && (!TC3a || !TC3b)`. - while plus1w < plus1v_up - && threshold - plus1w >= ten_kappa - && (plus1w + ten_kappa < plus1v_up - || plus1v_up - plus1w >= plus1w + ten_kappa - plus1v_up) - { - *last -= 1; - debug_assert!(*last > b'0'); // the shortest repr cannot end with `0` - plus1w += ten_kappa; - } - } - - // check if this representation is also the closest representation to `v - 1 ulp`. + // we have three conditions to terminate. any of them will make the loop unable to + // proceed, but we then have at least one valid representation known to be closest to + // `v + 1 ulp` anyway. we will denote them as TC1 through TC3 for brevity. + // + // TC1: `w(n) <= v + 1 ulp`, i.e., this is the last repr that can be the closest one. + // this is equivalent to `plus1 - w(n) = plus1w(n) >= plus1 - (v + 1 ulp) = plus1v_up`. + // combined with TC2 (which checks if `w(n+1)` is valid), this prevents the possible + // overflow on the calculation of `plus1w(n)`. + // + // TC2: `w(n+1) < minus1`, i.e., the next repr definitely does not round to `v`. + // this is equivalent to `plus1 - w(n) + 10^kappa = plus1w(n) + 10^kappa > + // plus1 - minus1 = threshold`. the left hand side can overflow, but we know + // `threshold > plus1v`, so if TC1 is false, `threshold - plus1w(n) > + // threshold - (plus1v - 1 ulp) > 1 ulp` and we can safely test if + // `threshold - plus1w(n) < 10^kappa` instead. + // + // TC3: `abs(w(n) - (v + 1 ulp)) <= abs(w(n+1) - (v + 1 ulp))`, i.e., the next repr is + // no closer to `v + 1 ulp` than the current repr. given `z(n) = plus1v_up - plus1w(n)`, + // this becomes `abs(z(n)) <= abs(z(n+1))`. again assuming that TC1 is false, we have + // `z(n) > 0`. we have two cases to consider: // - // this is simply same to the terminating conditions for `v + 1 ulp`, with all `plus1v_up` - // replaced by `plus1v_down` instead. overflow analysis equally holds. - if plus1w < plus1v_down + // - when `z(n+1) >= 0`: TC3 becomes `z(n) <= z(n+1)`. as `plus1w(n)` is increasing, + // `z(n)` should be decreasing and this is clearly false. + // - when `z(n+1) < 0`: + // - TC3a: the precondition is `plus1v_up < plus1w(n) + 10^kappa`. assuming TC2 is + // false, `threshold >= plus1w(n) + 10^kappa` so it cannot overflow. + // - TC3b: TC3 becomes `z(n) <= -z(n+1)`, i.e., `plus1v_up - plus1w(n) >= + // plus1w(n+1) - plus1v_up = plus1w(n) + 10^kappa - plus1v_up`. the negated TC1 + // gives `plus1v_up > plus1w(n)`, so it cannot overflow or underflow when + // combined with TC3a. + // + // consequently, we should stop when `TC1 || TC2 || (TC3a && TC3b)`. the following is + // equal to its inverse, `!TC1 && !TC2 && (!TC3a || !TC3b)`. + while plus1w < plus1v_up && threshold - plus1w >= ten_kappa - && (plus1w + ten_kappa < plus1v_down - || plus1v_down - plus1w >= plus1w + ten_kappa - plus1v_down) + && (plus1w + ten_kappa < plus1v_up + || plus1v_up - plus1w >= plus1w + ten_kappa - plus1v_up) { - return None; + *last -= 1; + debug_assert!(*last > b'0'); // the shortest repr cannot end with `0` + plus1w += ten_kappa; } + } - // now we have the closest representation to `v` between `plus1` and `minus1`. - // this is too liberal, though, so we reject any `w(n)` not between `plus0` and `minus0`, - // i.e., `plus1 - plus1w(n) <= minus0` or `plus1 - plus1w(n) >= plus0`. we utilize the facts - // that `threshold = plus1 - minus1` and `plus1 - plus0 = minus0 - minus1 = 2 ulp`. - if 2 * ulp <= plus1w && plus1w <= threshold - 4 * ulp { Some((buf, exp)) } else { None } + // check if this representation is also the closest representation to `v - 1 ulp`. + // + // this is simply same to the terminating conditions for `v + 1 ulp`, with all `plus1v_up` + // replaced by `plus1v_down` instead. overflow analysis equally holds. + if plus1w < plus1v_down + && threshold - plus1w >= ten_kappa + && (plus1w + ten_kappa < plus1v_down + || plus1v_down - plus1w >= plus1w + ten_kappa - plus1v_down) + { + return None; } + + // now we have the closest representation to `v` between `plus1` and `minus1`. + // this is too liberal, though, so we reject any `w(n)` not between `plus0` and `minus0`, + // i.e., `plus1 - plus1w(n) <= minus0` or `plus1 - plus1w(n) >= plus0`. we utilize the facts + // that `threshold = plus1 - minus1` and `plus1 - plus0 = minus0 - minus1 = 2 ulp`. + if 2 * ulp <= plus1w && plus1w <= threshold - 4 * ulp { Some((buf, exp)) } else { None } } /// The shortest mode implementation for Grisu with Dragon fallback. @@ -475,6 +607,34 @@ pub fn format_exact_opt<'a>( buf: &'a mut [MaybeUninit], limit: i16, ) -> Option<(/*digits*/ &'a [u8], /*exp*/ i16)> { + generate_exact(d, buf, limit).and_then(|rounding| { + // SAFETY: generation initialized the recorded prefix, or selected an + // empty prefix. No buffer writes occur between generation and rounding. + unsafe { + possibly_round( + buf, + rounding.len, + rounding.exp, + limit, + rounding.remainder, + rounding.ten_kappa, + rounding.ulp, + ) + } + }) +} + +// Keep the rounding state separate from generation so every exit shares one +// final call. This also avoids repeating its proof contract at each loop step. +struct ExactRounding { + len: usize, + exp: i16, + remainder: u64, + ten_kappa: u64, + ulp: u64, +} + +fn generate_exact(d: &Decoded, buf: &mut [MaybeUninit], limit: i16) -> Option { assert!(d.mant > 0); assert!(d.mant < (1 << 61)); // we need at least three bits of additional precision assert!(!buf.is_empty()); @@ -538,10 +698,14 @@ pub fn format_exact_opt<'a>( // this will increase the false negative rate, but only very, *very* slightly; // it can only matter noticeably when the mantissa is bigger than 60 bits. // - // SAFETY: `len=0`, so the obligation of having initialized this memory is trivial. - return unsafe { - possibly_round(buf, 0, exp, limit, v.f / 10, (max_ten_kappa as u64) << e, err << e) - }; + // The empty prefix needs no initialization. + return Some(ExactRounding { + len: 0, + exp, + remainder: v.f / 10, + ten_kappa: (max_ten_kappa as u64) << e, + ulp: err << e, + }); } else if ((exp as i32 - limit as i32) as usize) < buf.len() { (exp - limit) as usize } else { @@ -571,10 +735,14 @@ pub fn format_exact_opt<'a>( // is the buffer full? run the rounding pass with the remainder. if i == len { let vrem = ((r as u64) << e) + vfrac; // == (v % 10^kappa) * 2^e - // SAFETY: we have initialized `len` many bytes. - return unsafe { - possibly_round(buf, len, exp, limit, vrem, (ten_kappa as u64) << e, err << e) - }; + // We have initialized `len` many bytes. + return Some(ExactRounding { + len, + exp, + remainder: vrem, + ten_kappa: (ten_kappa as u64) << e, + ulp: err << e, + }); } // break the loop when we have rendered all integral digits. @@ -624,8 +792,8 @@ pub fn format_exact_opt<'a>( // is the buffer full? run the rounding pass with the remainder. if i == len { - // SAFETY: we have initialized `len` many bytes. - return unsafe { possibly_round(buf, len, exp, limit, r, 1 << e, err) }; + // We have initialized `len` many bytes. + return Some(ExactRounding { len, exp, remainder: r, ten_kappa: 1 << e, ulp: err }); } // restore invariants @@ -633,130 +801,130 @@ pub fn format_exact_opt<'a>( } // further calculation is useless (`possibly_round` definitely fails), so we give up. - return None; + None +} - // we've generated all requested digits of `v`, which should be also same to corresponding - // digits of `v - 1 ulp`. now we check if there is a unique representation shared by - // both `v - 1 ulp` and `v + 1 ulp`; this can be either same to generated digits, or - // to the rounded-up version of those digits. if the range contains multiple representations - // of the same length, we cannot be sure and should return `None` instead. +// we've generated all requested digits of `v`, which should be also same to corresponding +// digits of `v - 1 ulp`. now we check if there is a unique representation shared by +// both `v - 1 ulp` and `v + 1 ulp`; this can be either same to generated digits, or +// to the rounded-up version of those digits. if the range contains multiple representations +// of the same length, we cannot be sure and should return `None` instead. +// +// all arguments here are scaled by the common (but implicit) value `k`, so that: +// - `remainder = (v % 10^kappa) * k` +// - `ten_kappa = 10^kappa * k` +// - `ulp = 2^-e * k` +// +// SAFETY: the first `len` bytes of `buf` must be initialized. +unsafe fn possibly_round( + buf: &mut [MaybeUninit], + mut len: usize, + mut exp: i16, + limit: i16, + remainder: u64, + ten_kappa: u64, + ulp: u64, +) -> Option<(&[u8], i16)> { + debug_assert!(remainder < ten_kappa); + + // 10^kappa + // : : :<->: : + // : : : : : + // :|1 ulp|1 ulp| : + // :|<--->|<--->| : + // ----|-----|-----|---- + // | v | + // v - 1 ulp v + 1 ulp // - // all arguments here are scaled by the common (but implicit) value `k`, so that: - // - `remainder = (v % 10^kappa) * k` - // - `ten_kappa = 10^kappa * k` - // - `ulp = 2^-e * k` + // (for the reference, the dotted line indicates the exact value for + // possible representations in given number of digits.) // - // SAFETY: the first `len` bytes of `buf` must be initialized. - unsafe fn possibly_round( - buf: &mut [MaybeUninit], - mut len: usize, - mut exp: i16, - limit: i16, - remainder: u64, - ten_kappa: u64, - ulp: u64, - ) -> Option<(&[u8], i16)> { - debug_assert!(remainder < ten_kappa); - - // 10^kappa - // : : :<->: : - // : : : : : - // :|1 ulp|1 ulp| : - // :|<--->|<--->| : - // ----|-----|-----|---- - // | v | - // v - 1 ulp v + 1 ulp - // - // (for the reference, the dotted line indicates the exact value for - // possible representations in given number of digits.) - // - // error is too large that there are at least three possible representations - // between `v - 1 ulp` and `v + 1 ulp`. we cannot determine which one is correct. - if ulp >= ten_kappa { - return None; - } + // error is too large that there are at least three possible representations + // between `v - 1 ulp` and `v + 1 ulp`. we cannot determine which one is correct. + if ulp >= ten_kappa { + return None; + } - // 10^kappa - // :<------->: - // : : - // : |1 ulp|1 ulp| - // : |<--->|<--->| - // ----|-----|-----|---- - // | v | - // v - 1 ulp v + 1 ulp - // - // in fact, 1/2 ulp is enough to introduce two possible representations. - // (remember that we need a unique representation for both `v - 1 ulp` and `v + 1 ulp`.) - // this won't overflow, as `ulp < ten_kappa` from the first check. - if ten_kappa - ulp <= ulp { - return None; - } + // 10^kappa + // :<------->: + // : : + // : |1 ulp|1 ulp| + // : |<--->|<--->| + // ----|-----|-----|---- + // | v | + // v - 1 ulp v + 1 ulp + // + // in fact, 1/2 ulp is enough to introduce two possible representations. + // (remember that we need a unique representation for both `v - 1 ulp` and `v + 1 ulp`.) + // this won't overflow, as `ulp < ten_kappa` from the first check. + if ten_kappa - ulp <= ulp { + return None; + } - // remainder - // :<->| : - // : | : - // :<--------- 10^kappa ---------->: - // | : | : - // |1 ulp|1 ulp| : - // |<--->|<--->| : - // ----|-----|-----|------------------------ - // | v | - // v - 1 ulp v + 1 ulp - // - // if `v + 1 ulp` is closer to the rounded-down representation (which is already in `buf`), - // then we can safely return. note that `v - 1 ulp` *can* be less than the current - // representation, but as `1 ulp < 10^kappa / 2`, this condition is enough: - // the distance between `v - 1 ulp` and the current representation - // cannot exceed `10^kappa / 2`. - // - // the condition equals to `remainder + ulp < 10^kappa / 2`. - // since this can easily overflow, first check if `remainder < 10^kappa / 2`. - // we've already verified that `ulp < 10^kappa / 2`, so as long as - // `10^kappa` did not overflow after all, the second check is fine. - if ten_kappa - remainder > remainder && ten_kappa - 2 * remainder >= 2 * ulp { - // SAFETY: our caller initialized that memory. - return Some((unsafe { buf[..len].assume_init_ref() }, exp)); - } + // remainder + // :<->| : + // : | : + // :<--------- 10^kappa ---------->: + // | : | : + // |1 ulp|1 ulp| : + // |<--->|<--->| : + // ----|-----|-----|------------------------ + // | v | + // v - 1 ulp v + 1 ulp + // + // if `v + 1 ulp` is closer to the rounded-down representation (which is already in `buf`), + // then we can safely return. note that `v - 1 ulp` *can* be less than the current + // representation, but as `1 ulp < 10^kappa / 2`, this condition is enough: + // the distance between `v - 1 ulp` and the current representation + // cannot exceed `10^kappa / 2`. + // + // the condition equals to `remainder + ulp < 10^kappa / 2`. + // since this can easily overflow, first check if `remainder < 10^kappa / 2`. + // we've already verified that `ulp < 10^kappa / 2`, so as long as + // `10^kappa` did not overflow after all, the second check is fine. + if ten_kappa - remainder > remainder && ten_kappa - 2 * remainder >= 2 * ulp { + // SAFETY: our caller initialized that memory. + return Some((unsafe { buf[..len].assume_init_ref() }, exp)); + } - // :<------- remainder ------>| : - // : | : - // :<--------- 10^kappa --------->: - // : | | : | - // : |1 ulp|1 ulp| - // : |<--->|<--->| - // -----------------------|-----|-----|----- - // | v | - // v - 1 ulp v + 1 ulp - // - // on the other hands, if `v - 1 ulp` is closer to the rounded-up representation, - // we should round up and return. for the same reason we don't need to check `v + 1 ulp`. - // - // the condition equals to `remainder - ulp >= 10^kappa / 2`. - // again we first check if `remainder > ulp` (note that this is not `remainder >= ulp`, - // as `10^kappa` is never zero). also note that `remainder - ulp <= 10^kappa`, - // so the second check does not overflow. - if remainder > ulp && ten_kappa - (remainder - ulp) <= remainder - ulp { - if let Some(c) = - // SAFETY: our caller must have initialized that memory. - round_up(unsafe { buf[..len].assume_init_mut() }) - { - // only add an additional digit when we've been requested the fixed precision. - // we also need to check that, if the original buffer was empty, - // the additional digit can only be added when `exp == limit` (edge case). - exp += 1; - if exp > limit && len < buf.len() { - buf[len] = MaybeUninit::new(c); - len += 1; - } + // :<------- remainder ------>| : + // : | : + // :<--------- 10^kappa --------->: + // : | | : | + // : |1 ulp|1 ulp| + // : |<--->|<--->| + // -----------------------|-----|-----|----- + // | v | + // v - 1 ulp v + 1 ulp + // + // on the other hands, if `v - 1 ulp` is closer to the rounded-up representation, + // we should round up and return. for the same reason we don't need to check `v + 1 ulp`. + // + // the condition equals to `remainder - ulp >= 10^kappa / 2`. + // again we first check if `remainder > ulp` (note that this is not `remainder >= ulp`, + // as `10^kappa` is never zero). also note that `remainder - ulp <= 10^kappa`, + // so the second check does not overflow. + if remainder > ulp && ten_kappa - (remainder - ulp) <= remainder - ulp { + if let Some(c) = + // SAFETY: our caller must have initialized that memory. + round_up(unsafe { buf[..len].assume_init_mut() }) + { + // only add an additional digit when we've been requested the fixed precision. + // we also need to check that, if the original buffer was empty, + // the additional digit can only be added when `exp == limit` (edge case). + exp += 1; + if exp > limit && len < buf.len() { + buf[len] = MaybeUninit::new(c); + len += 1; } - // SAFETY: we and our caller initialized that memory. - return Some((unsafe { buf[..len].assume_init_ref() }, exp)); } - - // otherwise we are doomed (i.e., some values between `v - 1 ulp` and `v + 1 ulp` are - // rounding down and others are rounding up) and give up. - None + // SAFETY: we and our caller initialized that memory. + return Some((unsafe { buf[..len].assume_init_ref() }, exp)); } + + // otherwise we are doomed (i.e., some values between `v - 1 ulp` and `v + 1 ulp` are + // rounding down and others are rounding up) and give up. + None } /// The exact and fixed mode implementation for Grisu with Dragon fallback. @@ -776,3 +944,1542 @@ pub fn format_exact<'a>( None => fallback(d, buf, limit), } } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +pub mod grisu_verify { + use super::*; + use crate::kani; + use crate::num::imp::flt2dec::flt2dec_verify::{ + arbitrary_finite_f32, arbitrary_finite_f64, arbitrary_finite_f64_exponent, + arbitrary_finite_f64_range, for_each_finite_partition, + }; + + // The direct strategy harnesses execute the real generator bodies. Exact + // and shortest modes compose separately proved final-rounding contracts. + // Buffer lengths are symbolic: shortest mode includes the minimum legal + // buffer, and exact mode includes both one-byte and multi-digit buffers. + // These are bounded harnesses; a successful run covers lengths up to 32, + // not arbitrary slice lengths. Unwinding assertions remain enabled. + const PROOF_BUFLEN: usize = 32; + const _: () = assert!(PROOF_BUFLEN <= u8::MAX as usize); + + pub(super) fn initialized_digits(buf: &[MaybeUninit], len: usize) -> bool { + // Keep each guarded byte check independent to avoid deep conditionals. + macro_rules! check_digit { + ($($index:literal),+ $(,)?) => { + true $(& (buf.len() <= $index || len <= $index || ( + kani::mem::can_dereference(crate::ptr::addr_of!(buf[$index]) as *const u8) + && unsafe { + let digit = *(crate::ptr::addr_of!(buf[$index]) as *const u8); + digit >= b'0' && digit <= b'9' + } + )))+ + }; + } + len <= MAX_SIG_DIGITS + && buf.len() >= MAX_SIG_DIGITS + && check_digit!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + } + + pub(super) fn proof_power10(exponent: usize) -> u64 { + match exponent { + 0 => 1, + 1 => 10, + 2 => 100, + 3 => 1000, + 4 => 10000, + 5 => 100000, + 6 => 1000000, + 7 => 10000000, + 8 => 100000000, + 9 => 1000000000, + 10 => 10000000000, + 11 => 100000000000, + 12 => 1000000000000, + 13 => 10000000000000, + 14 => 100000000000000, + 15 => 1000000000000000, + 16 => 10000000000000000, + _ => 0, + } + } + + #[kani::requires(exponent <= 15)] + #[kani::requires(threshold <= u64::MAX / 10)] + #[kani::requires(threshold as u128 == proof_threshold(delta, exponent))] + #[kani::ensures(|result| result.0 as u128 == proof_threshold(delta, result.1))] + #[kani::ensures(|result| result.0 == threshold * 10 && result.1 == exponent + 1)] + pub(super) fn advance_fraction_threshold( + delta: u64, + exponent: usize, + threshold: u64, + ) -> (u64, usize) { + let _ = delta; + (threshold * 10, exponent + 1) + } + + #[kani::proof_for_contract(advance_fraction_threshold)] + fn check_advance_fraction_threshold() { + let delta: u64 = kani::any(); + let exponent = usize::from(kani::any::() & 15); + let threshold: u64 = kani::any(); + let result = advance_fraction_threshold(delta, exponent, threshold); + kani::cover(exponent == 15, "threshold advancement checks the last fractional step"); + kani::cover(delta == 0 && result.0 == 0, "threshold advancement includes zero"); + kani::cover( + exponent == 0 && threshold == u64::MAX / 10, + "threshold advancement checks the maximum safe product", + ); + } + + #[rustc_force_inline] + pub(super) fn proof_threshold(delta: u64, exponent: usize) -> u128 { + // Keep multiplication inside each arm so every factor is constant. + match exponent { + 0 => delta as u128, + 1 => delta as u128 * 10, + 2 => delta as u128 * 100, + 3 => delta as u128 * 1_000, + 4 => delta as u128 * 10_000, + 5 => delta as u128 * 100_000, + 6 => delta as u128 * 1_000_000, + 7 => delta as u128 * 10_000_000, + 8 => delta as u128 * 100_000_000, + 9 => delta as u128 * 1_000_000_000, + 10 => delta as u128 * 10_000_000_000, + 11 => delta as u128 * 100_000_000_000, + 12 => delta as u128 * 1_000_000_000_000, + 13 => delta as u128 * 10_000_000_000_000, + 14 => delta as u128 * 100_000_000_000_000, + 15 => delta as u128 * 1_000_000_000_000_000, + 16 => delta as u128 * 10_000_000_000_000_000, + _ => 0, + } + } + + macro_rules! check_fractional_encoding_power { + ($($name:ident => $exponent:literal),+ $(,)?) => {$( + #[kani::proof] + fn $name() { + check_fractional_encoding_for_exponent($exponent); + } + )+}; + } + + check_fractional_encoding_power!( + check_fractional_encoding_00 => 0, + check_fractional_encoding_01 => 1, + check_fractional_encoding_02 => 2, + check_fractional_encoding_03 => 3, + check_fractional_encoding_04 => 4, + check_fractional_encoding_05 => 5, + check_fractional_encoding_06 => 6, + check_fractional_encoding_07 => 7, + check_fractional_encoding_08 => 8, + check_fractional_encoding_09 => 9, + check_fractional_encoding_10 => 10, + check_fractional_encoding_11 => 11, + check_fractional_encoding_12 => 12, + check_fractional_encoding_13 => 13, + check_fractional_encoding_14 => 14, + check_fractional_encoding_15 => 15, + ); + + fn check_fractional_encoding_for_exponent(exponent: usize) { + assert!(exponent <= 15); + let power = proof_power10(exponent); + assert!(power < 1u64 << 50); + let ulp: u64 = kani::any(); + let delta: u64 = kani::any(); + let threshold: u64 = kani::any(); + assert_eq!(proof_threshold(delta, exponent), delta as u128 * power as u128); + let original = threshold as u128 == delta as u128 * ulp as u128 && ulp == power; + if ulp == power { + assert_eq!(ulp, ulp & ((1u64 << 50) - 1)); + } + let narrowed = ulp < 1u64 << 50 + && threshold as u128 == proof_threshold(delta, exponent) + && ulp == power; + assert_eq!(original, narrowed); + + let shift: usize = kani::any(); + let value: u64 = kani::any(); + kani::assume(shift < 64); + assert_eq!(value << shift, value << (shift as u8)); + kani::cover(ulp == power, "the selected fractional decimal power is checked"); + kani::cover(shift == 63, "the maximum defined shift is checked"); + } + + // Decode gives minus = 1. Only normal powers of two have plus = 2, + // with the f32 or f64 significand shifted by two. Callers prove these + // conditions from the real decoder, without restricting finite float bits. + fn normalized_shortest_valid( + state: (u64, u64, u64, i16), + mant: u64, + input_plus: u64, + input_exp: i16, + ) -> bool { + let (plus, minus, v, exp) = state; + let shift = mant.wrapping_add(input_plus).leading_zeros() as i16; + exp == input_exp.wrapping_sub(shift) + && normalized_interval_valid(plus, minus, v) + && plus as u128 - minus as u128 == (input_plus as u128 + 1) << shift + && plus >= 1 << 63 + && minus >= 1 << 62 + && minus <= v + && v <= plus + && if input_plus == 1 { + let gap = plus - v; + gap >= 512 && gap == v - minus && (gap as u128) << 54 >= plus as u128 + } else { + let gap = if mant == 1 << 25 { 1u64 << 38 } else { 1u64 << 9 }; + v == 1 << 63 && plus == (1 << 63) + 2 * gap && minus == (1 << 63) - gap + } + } + + #[kani::requires( + mant >= 2 && minus == 1 + && (plus == 1 || (plus == 2 && (mant == 1 << 25 || mant == 1 << 54))) + && exp >= -1076 && exp <= 970 + && mant as u128 + plus as u128 <= ((minus as u128 + plus as u128) << 53) + )] + #[kani::ensures(|result| normalized_shortest_valid(*result, mant, plus, exp))] + fn normalize_shortest_contract( + mant: u64, + minus: u64, + plus: u64, + exp: i16, + ) -> (u64, u64, u64, i16) { + let d = Decoded { mant, minus, plus, exp, inclusive: false }; + let (plus, minus, v) = normalize_shortest(&d); + assert_eq!(plus.e, minus.e); + assert_eq!(plus.e, v.e); + (plus.f, minus.f, v.f, plus.e) + } + + pub(super) fn normalize_shortest_for_proof(d: &Decoded) -> (Fp, Fp, Fp) { + let (plus, minus, v, e) = normalize_shortest_contract(d.mant, d.minus, d.plus, d.exp); + let (guided_plus, guided_minus, guided_v) = if d.plus == 1 { + let gap = v - minus; + (v + gap, v - gap, v) + } else { + let gap = if d.mant == 1 << 25 { 1u64 << 38 } else { 1u64 << 9 }; + ((1 << 63) + 2 * gap, (1 << 63) - gap, 1 << 63) + }; + // Check the reconstruction before exposing its shared arithmetic. + assert_eq!(guided_plus, plus); + assert_eq!(guided_minus, minus); + assert_eq!(guided_v, v); + (Fp { f: guided_plus, e }, Fp { f: guided_minus, e }, Fp { f: guided_v, e }) + } + + #[kani::proof_for_contract(normalize_shortest_contract)] + #[kani::stub(u64::leading_zeros, crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64)] + #[kani::solver(kissat)] + fn check_normalize_shortest() { + let mant = kani::any::() & ((1u64 << 55) - 1); + let plus = if kani::any::() { 1 } else { 2 }; + kani::assume(mant >= 2); + kani::assume(plus == 1 || mant == 1 << 25 || mant == 1 << 54); + kani::assume(mant as u128 + plus as u128 <= ((1 + plus as u128) << 53)); + let exp = kani::any::(); + kani::assume((-1076..=970).contains(&exp)); + normalize_shortest_contract(mant, 1, plus, exp); + kani::cover(plus == 1 && mant == 2, "normalization includes the smallest symmetric input"); + kani::cover( + plus == 2 && mant == 1 << 25, + "normalization includes asymmetric f32 intervals", + ); + kani::cover( + plus == 2 && mant == 1 << 54, + "normalization includes asymmetric f64 intervals", + ); + } + + #[kani::requires( + mant >= 2 && minus == 1 + && (plus == 1 || (plus == 2 && (mant == 1 << 25 || mant == 1 << 54))) + && exp >= -1076 && exp <= 970 + && mant as u128 + plus as u128 <= ((minus as u128 + plus as u128) << 53) + )] + #[kani::ensures(|result| { + shortest_scaling_valid(*result) + && shortest_scaling_exponent_matches(mant, plus, exp, result.3, result.4) + && shortest_scaling_width_valid(mant, minus, plus, exp, result.0, result.1) + })] + fn scale_shortest_contract( + mant: u64, + minus: u64, + plus: u64, + exp: i16, + ) -> (u64, u64, u64, i16, i16) { + let d = Decoded { mant, minus, plus, exp, inclusive: false }; + let ShortestScaled { plus, minus, v, minusk } = scale_shortest(&d); + assert_eq!(plus.e, minus.e); + assert_eq!(plus.e, v.e); + // Establish the postcondition in separate checked steps, so later + // properties can use the bounds already established by earlier ones. + assert!((ALPHA..=GAMMA).contains(&plus.e)); + assert!((-308..=332).contains(&minusk)); + assert!(plus.f >= 1 << 62 && plus.f < u64::MAX); + assert!(minus.f > 1 && minus.f <= v.f && v.f <= plus.f); + let upper = plus.f + 1; + let delta = upper - (minus.f - 1); + let e = -plus.e as usize; + assert!(delta >= 4); + assert!((delta as u128) << 53 >= upper as u128); + assert!(4 * (upper - v.f) as u128 <= 3 * delta as u128); + assert!(4 * (v.f - (minus.f - 1)) as u128 <= 3 * delta as u128); + if d.plus == 1 { + assert!(2 * ((plus.f - v.f) as u128) < delta as u128); + } else { + assert!((minus.f - 1) & ((1u64 << (e - 16)) - 1) != 0); + } + assert!( + 2 * ((plus.f - v.f) as u128) < delta as u128 + || (minus.f - 1) & ((1u64 << (e - 16)) - 1) != 0 + ); + let (max_kappa, max_ten) = max_pow10_no_more_than((upper >> e) as u32); + assert_eq!(decimal_minimum_upper(1, max_kappa), max_ten as u128); + assert!((max_ten as u128) << e <= upper as u128); + decimal_termination_contract(delta, 1u64 << e, max_kappa); + (plus.f, minus.f, v.f, plus.e, minusk) + } + + fn normalized_interval_valid(plus: u64, minus: u64, v: u64) -> bool { + plus >= 1 << 63 && minus >= 1 << 62 && minus <= v && v <= plus && { + let gap = v - minus; + gap >= 512 + && gap <= 1 << 62 + && if plus - v == gap { + (gap as u128) << 54 >= plus as u128 + } else { + v == 1 << 63 && (gap == 1 << 9 || gap == 1 << 38) && plus == (1 << 63) + 2 * gap + } + } + } + + fn scaled_interval_valid(state: (u64, u64, u64), symmetric: bool) -> bool { + let (plus, minus, v) = state; + plus >= 1 << 62 && plus < u64::MAX && minus > 1 && minus <= v && v <= plus && { + let upper = plus + 1; + let delta = upper - (minus - 1); + delta >= 4 + && (delta as u128) << 53 >= upper as u128 + && 4 * (upper - v) as u128 <= 3 * delta as u128 + && 4 * (v - (minus - 1)) as u128 <= 3 * delta as u128 + && (2 * ((plus - v) as u128) < delta as u128 || (minus - 1) & 0xffff != 0) + && if symmetric { + 2 * ((plus - v) as u128) < delta as u128 + } else { + (minus - 1) & 0xffff != 0 + } + } + } + + #[kani::requires(normalized_interval_valid(plus, minus, v) + && (-1200..=1000).contains(&exp) && index > 0 && index < CACHED_POW10.len())] + #[kani::ensures(|result| scaled_interval_valid( + (result.0, result.1, result.2), plus.wrapping_sub(v) == v.wrapping_sub(minus)) + && result.0.wrapping_sub(result.1) <= plus.wrapping_sub(minus) + && index < CACHED_POW10.len() + && i32::from(result.3) == i32::from(exp) + i32::from(CACHED_POW10[index].1) + 64 + && scaled_width_window_valid( + plus.wrapping_sub(minus) as u128, + CACHED_POW10[index].0, + result.0.wrapping_sub(result.1) as u128, + ))] + fn multiply_shortest_contract( + plus: u64, + minus: u64, + v: u64, + exp: i16, + index: usize, + ) -> (u64, u64, u64, i16) { + let (plus, minus, v) = multiply_shortest( + Fp { f: plus, e: exp }, + Fp { f: minus, e: exp }, + Fp { f: v, e: exp }, + index, + ); + assert_eq!(plus.e, minus.e); + assert_eq!(plus.e, v.e); + (plus.f, minus.f, v.f, plus.e) + } + + fn stub_multiply_shortest(plus: Fp, minus: Fp, v: Fp, index: usize) -> (Fp, Fp, Fp) { + assert_eq!(plus.e, minus.e); + assert_eq!(plus.e, v.e); + let (plus, minus, v, e) = multiply_shortest_contract(plus.f, minus.f, v.f, plus.e, index); + (Fp { f: plus, e }, Fp { f: minus, e }, Fp { f: v, e }) + } + + macro_rules! verify_multiply_shortest { + ($name:ident, $index:literal) => { + #[kani::proof_for_contract(multiply_shortest_contract)] + #[kani::stub_verified(rounded_multiply_contract)] + #[kani::stub(Fp::mul, stub_rounded_multiply)] + #[kani::solver(kissat)] + fn $name() { + let (plus, minus, v, asymmetric) = arbitrary_normalized_interval(); + let exp = kani::any::(); + kani::assume((-1200..=1000).contains(&exp)); + multiply_shortest_contract(plus, minus, v, exp, $index); + kani::cover(asymmetric, "multiplication includes asymmetric intervals"); + kani::cover(!asymmetric, "multiplication includes symmetric intervals"); + kani::cover(exp == -1200, "multiplication includes its smallest exponent"); + kani::cover(exp == 1000, "multiplication includes its largest exponent"); + } + }; + // The same proof on the inputs with `$low <= v < $high`. Index 51 does not finish in + // two hours on the whole domain, so it is proved in two pieces that split on the top + // bit of `v`. The asymmetric intervals all have `v == 1 << 63`, so only the upper + // piece passes the asymmetric cover message. + ($name:ident, $index:literal, $low:expr, $high:expr $(, $asymmetric:literal)?) => { + #[kani::proof_for_contract(multiply_shortest_contract)] + #[kani::stub_verified(rounded_multiply_contract)] + #[kani::stub(Fp::mul, stub_rounded_multiply)] + #[kani::solver(kissat)] + fn $name() { + let (plus, minus, v, asymmetric) = arbitrary_normalized_interval(); + kani::assume(($low..$high).contains(&u128::from(v))); + let exp = kani::any::(); + kani::assume((-1200..=1000).contains(&exp)); + multiply_shortest_contract(plus, minus, v, exp, $index); + $(kani::cover(asymmetric, $asymmetric);)? + kani::cover(!asymmetric, "multiplication includes symmetric intervals"); + kani::cover(exp == -1200, "multiplication includes its smallest exponent"); + kani::cover(exp == 1000, "multiplication includes its largest exponent"); + } + }; + } + + verify_multiply_shortest!(check_multiply_shortest_00, 80); + verify_multiply_shortest!(check_multiply_shortest_01, 79); + verify_multiply_shortest!(check_multiply_shortest_02, 78); + verify_multiply_shortest!(check_multiply_shortest_03, 77); + verify_multiply_shortest!(check_multiply_shortest_04, 76); + verify_multiply_shortest!(check_multiply_shortest_05, 75); + verify_multiply_shortest!(check_multiply_shortest_06, 74); + verify_multiply_shortest!(check_multiply_shortest_07, 73); + verify_multiply_shortest!(check_multiply_shortest_08, 72); + verify_multiply_shortest!(check_multiply_shortest_09, 71); + verify_multiply_shortest!(check_multiply_shortest_10, 70); + verify_multiply_shortest!(check_multiply_shortest_11, 69); + verify_multiply_shortest!(check_multiply_shortest_12, 68); + verify_multiply_shortest!(check_multiply_shortest_13, 67); + verify_multiply_shortest!(check_multiply_shortest_14, 66); + verify_multiply_shortest!(check_multiply_shortest_15, 65); + verify_multiply_shortest!(check_multiply_shortest_16, 64); + verify_multiply_shortest!(check_multiply_shortest_17, 63); + verify_multiply_shortest!(check_multiply_shortest_18, 62); + verify_multiply_shortest!(check_multiply_shortest_19, 61); + verify_multiply_shortest!(check_multiply_shortest_20, 60); + verify_multiply_shortest!(check_multiply_shortest_21, 59); + verify_multiply_shortest!(check_multiply_shortest_22, 58); + verify_multiply_shortest!(check_multiply_shortest_23, 57); + verify_multiply_shortest!(check_multiply_shortest_24, 56); + verify_multiply_shortest!(check_multiply_shortest_25, 55); + verify_multiply_shortest!(check_multiply_shortest_26, 54); + verify_multiply_shortest!(check_multiply_shortest_27, 53); + verify_multiply_shortest!(check_multiply_shortest_28, 52); + verify_multiply_shortest!(check_multiply_shortest_29_v0, 51, 0, 1 << 63); + verify_multiply_shortest!( + check_multiply_shortest_29_v1, + 51, + 1 << 63, + 1 << 64, + "multiplication includes asymmetric intervals" + ); + verify_multiply_shortest!(check_multiply_shortest_30, 50); + verify_multiply_shortest!(check_multiply_shortest_31, 49); + verify_multiply_shortest!(check_multiply_shortest_32, 48); + verify_multiply_shortest!(check_multiply_shortest_33, 47); + verify_multiply_shortest!(check_multiply_shortest_34, 46); + verify_multiply_shortest!(check_multiply_shortest_35, 45); + verify_multiply_shortest!(check_multiply_shortest_36, 44); + verify_multiply_shortest!(check_multiply_shortest_37, 43); + verify_multiply_shortest!(check_multiply_shortest_38, 42); + verify_multiply_shortest!(check_multiply_shortest_39, 41); + verify_multiply_shortest!(check_multiply_shortest_40, 40); + verify_multiply_shortest!(check_multiply_shortest_41, 39); + verify_multiply_shortest!(check_multiply_shortest_42, 38); + verify_multiply_shortest!(check_multiply_shortest_43, 37); + verify_multiply_shortest!(check_multiply_shortest_44, 36); + verify_multiply_shortest!(check_multiply_shortest_45, 35); + verify_multiply_shortest!(check_multiply_shortest_46, 34); + verify_multiply_shortest!(check_multiply_shortest_47, 33); + verify_multiply_shortest!(check_multiply_shortest_48, 32); + verify_multiply_shortest!(check_multiply_shortest_49, 31); + verify_multiply_shortest!(check_multiply_shortest_50, 30); + verify_multiply_shortest!(check_multiply_shortest_51, 29); + verify_multiply_shortest!(check_multiply_shortest_52, 28); + verify_multiply_shortest!(check_multiply_shortest_53, 27); + verify_multiply_shortest!(check_multiply_shortest_54, 26); + verify_multiply_shortest!(check_multiply_shortest_55, 25); + verify_multiply_shortest!(check_multiply_shortest_56, 24); + verify_multiply_shortest!(check_multiply_shortest_57, 23); + verify_multiply_shortest!(check_multiply_shortest_58, 22); + verify_multiply_shortest!(check_multiply_shortest_59, 21); + verify_multiply_shortest!(check_multiply_shortest_60, 20); + verify_multiply_shortest!(check_multiply_shortest_61, 19); + verify_multiply_shortest!(check_multiply_shortest_62, 18); + verify_multiply_shortest!(check_multiply_shortest_63, 17); + verify_multiply_shortest!(check_multiply_shortest_64, 16); + verify_multiply_shortest!(check_multiply_shortest_65, 15); + verify_multiply_shortest!(check_multiply_shortest_66, 14); + verify_multiply_shortest!(check_multiply_shortest_67, 13); + verify_multiply_shortest!(check_multiply_shortest_68, 12); + verify_multiply_shortest!(check_multiply_shortest_69, 11); + verify_multiply_shortest!(check_multiply_shortest_70, 10); + verify_multiply_shortest!(check_multiply_shortest_71, 9); + verify_multiply_shortest!(check_multiply_shortest_72, 8); + verify_multiply_shortest!(check_multiply_shortest_73, 7); + verify_multiply_shortest!(check_multiply_shortest_74, 6); + verify_multiply_shortest!(check_multiply_shortest_75, 5); + verify_multiply_shortest!(check_multiply_shortest_76, 4); + verify_multiply_shortest!(check_multiply_shortest_77, 3); + verify_multiply_shortest!(check_multiply_shortest_78, 2); + verify_multiply_shortest!(check_multiply_shortest_79, 1); + + fn rounded_product_wide(f: u64, coefficient: u64) -> u128 { + (f as u128).wrapping_mul(coefficient as u128).wrapping_add(1 << 63) + } + + #[kani::ensures(|result| *result < u64::MAX + && ((*result as u128) << 64) <= rounded_product_wide(f, coefficient) + && rounded_product_wide(f, coefficient) < ((*result as u128 + 1) << 64))] + fn rounded_multiply_contract(f: u64, coefficient: u64) -> u64 { + Fp { f, e: 0 }.mul(Fp { f: coefficient, e: 0 }).f + } + + fn stub_rounded_multiply(value: Fp, coefficient: Fp) -> Fp { + Fp { f: rounded_multiply_contract(value.f, coefficient.f), e: value.e + coefficient.e + 64 } + } + + #[kani::proof_for_contract(rounded_multiply_contract)] + #[kani::solver(kissat)] + #[cfg_attr( + target_pointer_width = "64", + kani::stub( + u64::carrying_mul, + crate::num::imp::flt2dec::strategy::dragon::dragon_verify::carrying_mul_model + ) + )] + fn check_rounded_multiply() { + let f = kani::any::(); + let coefficient = kani::any::(); + let result = rounded_multiply_contract(f, coefficient); + kani::cover(f == 0 && coefficient == 0 && result == 0, "zero products are covered"); + kani::cover(f == u64::MAX && coefficient == u64::MAX, "maximum product is covered"); + } + + #[kani::requires(f >= 1 << 63 && coefficient >= 1 << 63)] + #[kani::ensures(|result| *result >= 1 << 62 && *result < u64::MAX)] + fn normalized_multiply_contract(f: u64, coefficient: u64) -> u64 { + Fp { f, e: 0 }.mul(Fp { f: coefficient, e: 0 }).f + } + + fn stub_normalized_multiply(value: Fp, coefficient: Fp) -> Fp { + Fp { + f: normalized_multiply_contract(value.f, coefficient.f), + e: value.e + coefficient.e + 64, + } + } + + #[kani::proof_for_contract(normalized_multiply_contract)] + #[kani::solver(kissat)] + #[cfg_attr( + target_pointer_width = "64", + kani::stub( + u64::carrying_mul, + crate::num::imp::flt2dec::strategy::dragon::dragon_verify::carrying_mul_model + ) + )] + fn check_normalized_multiply() { + let f = kani::any::() | (1 << 63); + let coefficient = kani::any::() | (1 << 63); + let result = normalized_multiply_contract(f, coefficient); + kani::cover( + f == 1 << 63 && coefficient == 1 << 63 && result == 1 << 62, + "minimum normalized product is covered", + ); + kani::cover( + f == u64::MAX && coefficient == u64::MAX && result == u64::MAX - 1, + "maximum normalized product is covered", + ); + } + + fn arbitrary_normalized_interval() -> (u64, u64, u64, bool) { + let asymmetric = kani::any::(); + let v = if asymmetric { 1 << 63 } else { kani::any::() }; + let gap = match (asymmetric, kani::any::()) { + (true, true) => 1 << 9, + (true, false) => 1 << 38, + (false, _) => kani::any::(), + }; + kani::assume((512..=1 << 62).contains(&gap)); + let upper_gap = if asymmetric { 2 * gap } else { gap }; + kani::assume(v >= (1 << 62) + gap); + kani::assume(v <= u64::MAX - upper_gap); + let plus = v + upper_gap; + let minus = v - gap; + kani::assume(plus >= 1 << 63); + kani::assume(if asymmetric { + v == 1 << 63 && (gap == 1 << 9 || gap == 1 << 38) + } else { + (gap as u128) << 54 >= plus as u128 + }); + (plus, minus, v, asymmetric) + } + + fn shortest_scaling_width_valid( + mant: u64, + minus: u64, + plus: u64, + exp: i16, + scaled_plus: u64, + scaled_minus: u64, + ) -> bool { + let shift = mant.wrapping_add(plus).leading_zeros(); + let gap = (minus as u128 + plus as u128) << shift; + let (_, index) = shortest_cached_power_index(mant, plus, exp); + scaled_minus <= scaled_plus + && scaled_plus as u128 - scaled_minus as u128 <= gap + && index >= 0 + && (index as usize) < CACHED_POW10.len() + && scaled_width_window_valid( + gap, + CACHED_POW10[index as usize].0, + scaled_plus as u128 - scaled_minus as u128, + ) + } + + // `width` is the difference of two products that `Fp::mul` rounded to nearest, so it is + // less than one away from the exact `gap * cached / 2^64` on either side. + fn scaled_width_window_valid(gap: u128, cached: u64, width: u128) -> bool { + let product = gap * cached as u128; + (width + 1) << 64 > product && width << 64 < product + (1u128 << 64) + } + + // The normalized exponent and the `CACHED_POW10` index that `scale_shortest` selects. + fn shortest_cached_power_index(mant: u64, input_plus: u64, input_exp: i16) -> (i32, i32) { + let normalized_exp = + i32::from(input_exp) - mant.wrapping_add(input_plus).leading_zeros() as i32; + let index = (i32::from(GAMMA) - normalized_exp - 64 - i32::from(CACHED_POW10_FIRST_E)) + * (CACHED_POW10.len() as i32 - 1) + / i32::from(CACHED_POW10_LAST_E - CACHED_POW10_FIRST_E); + (normalized_exp, index) + } + + fn shortest_scaling_exponent_matches( + mant: u64, + input_plus: u64, + input_exp: i16, + exp: i16, + minusk: i16, + ) -> bool { + let (normalized_exp, index) = shortest_cached_power_index(mant, input_plus, input_exp); + index >= 0 && (index as usize) < CACHED_POW10.len() && { + let (_, cached_exp, cached_k) = CACHED_POW10[index as usize]; + i32::from(exp) == normalized_exp + i32::from(cached_exp) + 64 && minusk == cached_k + } + } + + fn decimal_scaled_delta(delta: u64, max_kappa: u8) -> u128 { + match max_kappa { + 0 => delta as u128 * 10_000_000_000_000_000, + 1 => delta as u128 * 1_000_000_000_000_000, + 2 => delta as u128 * 100_000_000_000_000, + 3 => delta as u128 * 10_000_000_000_000, + 4 => delta as u128 * 1_000_000_000_000, + 5 => delta as u128 * 100_000_000_000, + 6 => delta as u128 * 10_000_000_000, + 7 => delta as u128 * 1_000_000_000, + 8 => delta as u128 * 100_000_000, + 9 => delta as u128 * 10_000_000, + _ => 0, + } + } + + fn decimal_minimum_upper(unit: u64, max_kappa: u8) -> u128 { + match max_kappa { + 0 => unit as u128, + 1 => unit as u128 * 10, + 2 => unit as u128 * 100, + 3 => unit as u128 * 1_000, + 4 => unit as u128 * 10_000, + 5 => unit as u128 * 100_000, + 6 => unit as u128 * 1_000_000, + 7 => unit as u128 * 10_000_000, + 8 => unit as u128 * 100_000_000, + 9 => unit as u128 * 1_000_000_000, + _ => u128::MAX, + } + } + + #[kani::requires(max_kappa <= 9 + && (delta as u128) << 53 >= decimal_minimum_upper(unit, max_kappa))] + #[kani::ensures(|result| *result == decimal_scaled_delta(delta, max_kappa) + && *result >= unit as u128)] + fn decimal_termination_contract(delta: u64, unit: u64, max_kappa: u8) -> u128 { + decimal_scaled_delta(delta, max_kappa) + } + + #[kani::proof_for_contract(decimal_termination_contract)] + #[kani::solver(kissat)] + fn check_decimal_termination() { + let delta = kani::any::(); + let unit = kani::any::(); + let max_kappa = kani::any::(); + kani::assume(max_kappa <= 9); + kani::assume((delta as u128) << 53 >= decimal_minimum_upper(unit, max_kappa)); + decimal_termination_contract(delta, unit, max_kappa); + macro_rules! cover_decimal_cases { + ($($kappa:literal),+ $(,)?) => { + $(kani::cover(max_kappa == $kappa, concat!("decimal case ", stringify!($kappa)));)+ + }; + } + cover_decimal_cases!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + fn shortest_scaling_valid(state: (u64, u64, u64, i16, i16)) -> bool { + let (plus, minus, v, exp, minusk) = state; + exp >= ALPHA + && exp <= GAMMA + && minusk >= -308 + && minusk <= 332 + && plus >= 1 << 62 + && plus < u64::MAX + && minus > 1 + && minus <= v + && v <= plus + && { + let upper = plus + 1; + let delta = upper - (minus - 1); + let e = -exp as usize; + let (max_kappa, _) = max_pow10_no_more_than((upper >> e) as u32); + // After max_kappa + 1 integral digits, this factor is the + // scale at digit 17. A threshold reaching 2^e exceeds every + // fractional remainder, so the real loop ends by that digit. + let scaled_delta = decimal_scaled_delta(delta, max_kappa); + // Normalization and multiplication preserve the relative + // interval width. Two outward ulps absorb their rounding + // errors. These postconditions must be proved in CI. + matches!(max_kappa, 0..=9) + && delta >= 4 + && (delta as u128) << 53 >= upper as u128 + && 4 * (upper - v) as u128 <= 3 * delta as u128 + // A centered target stops rounding before digit zero. + // Otherwise, the lower endpoint must stay off decimal + // boundaries through all 16 possible fractional digits. + && ((2 * (plus - v) as u128) < delta as u128 + || (minus - 1) & ((1u64 << (e - 16)) - 1) != 0) + && scaled_delta >= 1u128 << e + } + } + + fn stub_scale_shortest(d: &Decoded) -> ShortestScaled { + let (plus, minus, v, exp, minusk) = scale_shortest_contract(d.mant, d.minus, d.plus, d.exp); + ShortestScaled { + plus: Fp { f: plus, e: exp }, + minus: Fp { f: minus, e: exp }, + v: Fp { f: v, e: exp }, + minusk, + } + } + + // The contract bounds mant + plus by [3, 2^54 + 2], so normalization + // shifts by 9 through 62 bits. Together with exp in [-1076, 970], every + // normalized exponent lies in [-1138, 961]. The cached power partitions + // below cover that entire interval, as checked independently here. + // Reconstructing exp from the actual shift preserves every valid input + // while reducing the cached powers considered by each standalone proof. + #[kani::proof] + fn check_scale_shortest_partitioning() { + let exp = kani::any::(); + kani::assume((-1138..=961).contains(&exp)); + let offset = i32::from(GAMMA) - 64 - i32::from(CACHED_POW10_FIRST_E); + let range = CACHED_POW10.len() as i32 - 1; + let domain = i32::from(CACHED_POW10_LAST_E - CACHED_POW10_FIRST_E); + let index = (offset - i32::from(exp)) * range / domain; + assert!((1..=80).contains(&index)); + let first = offset - ((index + 1) * domain + range - 1) / range + 1; + let last = offset - (index * domain + range - 1) / range; + assert!(first <= i32::from(exp) && i32::from(exp) <= last); + assert!(last - first < 32); + kani::cover(exp == -1138, "partitions include the lowest normalized exponent"); + kani::cover(exp == 961, "partitions include the highest normalized exponent"); + } + + macro_rules! check_scale_shortest_partition { + ($name:ident, $index_guide:ident, $group:literal) => { + fn $index_guide(index: i32) -> i32 { + // Preserve the calculated index and expose its constant value. + assert_eq!(index, 80 - $group); + 80 - $group + } + + #[kani::proof_for_contract(scale_shortest_contract)] + #[kani::stub_verified(normalize_shortest_contract)] + #[kani::stub_verified(multiply_shortest_contract)] + #[kani::stub(multiply_shortest, stub_multiply_shortest)] + #[kani::stub_verified(decimal_termination_contract)] + #[cfg_attr( + target_pointer_width = "64", + kani::stub( + u64::carrying_mul, + crate::num::imp::flt2dec::strategy::dragon::dragon_verify::carrying_mul_model + ) + )] + #[kani::stub(proof_cached_power_index, $index_guide)] + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::solver(kissat)] + fn $name() { + // Every contract input has minus = 1, plus in {1, 2}, and + // mant <= 2^54. Encode those bounds before normalization so + // the verifier need not build paths for impossible inputs. + let mant = kani::any::() & ((1u64 << 55) - 1); + let minus = 1; + let plus = if kani::any::() { 1 } else { 2 }; + kani::assume(mant >= 2); + kani::assume(plus == 1 || mant == 1 << 25 || mant == 1 << 54); + kani::assume(mant as u128 + plus as u128 <= ((minus as u128 + plus as u128) << 53)); + const INDEX: i32 = 80 - $group; + const OFFSET: i32 = GAMMA as i32 - 64 - CACHED_POW10_FIRST_E as i32; + const RANGE: i32 = CACHED_POW10.len() as i32 - 1; + const DOMAIN: i32 = (CACHED_POW10_LAST_E - CACHED_POW10_FIRST_E) as i32; + const FIRST: i32 = OFFSET - ((INDEX + 1) * DOMAIN + RANGE - 1) / RANGE + 1; + const LAST: i32 = OFFSET - (INDEX * DOMAIN + RANGE - 1) / RANGE; + let normalized_exp = FIRST as i16 + i16::from(kani::any::() & 31); + kani::assume(i32::from(normalized_exp) <= LAST); + kani::assume((-1138..=961).contains(&normalized_exp)); + let shift = mant.wrapping_add(plus).leading_zeros() as i16; + let exp = normalized_exp + shift; + let result = scale_shortest_contract(mant, minus, plus, exp); + kani::cover( + mant == 2 && exp == -1075, + "scaling includes the smallest f64 subnormal", + ); + kani::cover(exp == -1076, "scaling includes the smallest decoded exponent"); + kani::cover(exp == 970, "scaling includes the largest decoded exponent"); + kani::cover(plus == 2, "scaling includes unequal neighbor intervals"); + kani::cover( + mant == 1 << 25 && plus == 2, + "scaling includes normal f32 powers of two", + ); + kani::cover( + mant == 1 << 54 && plus == 2, + "scaling includes normal f64 powers of two", + ); + kani::cover(result.3 == ALPHA, "scaling reaches the smallest target exponent"); + kani::cover(result.3 == GAMMA, "scaling reaches the largest target exponent"); + kani::cover( + i32::from(normalized_exp) == crate::cmp::max(FIRST, -1138), + "scaling reaches the first normalized exponent in its range", + ); + kani::cover( + i32::from(normalized_exp) == crate::cmp::min(LAST, 961), + "scaling reaches the last normalized exponent in its range", + ); + } + }; + } + + check_scale_shortest_partition!(check_scale_shortest_00, scaling_cache_index_00, 0); + check_scale_shortest_partition!(check_scale_shortest_01, scaling_cache_index_01, 1); + check_scale_shortest_partition!(check_scale_shortest_02, scaling_cache_index_02, 2); + check_scale_shortest_partition!(check_scale_shortest_03, scaling_cache_index_03, 3); + check_scale_shortest_partition!(check_scale_shortest_04, scaling_cache_index_04, 4); + check_scale_shortest_partition!(check_scale_shortest_05, scaling_cache_index_05, 5); + check_scale_shortest_partition!(check_scale_shortest_06, scaling_cache_index_06, 6); + check_scale_shortest_partition!(check_scale_shortest_07, scaling_cache_index_07, 7); + check_scale_shortest_partition!(check_scale_shortest_08, scaling_cache_index_08, 8); + check_scale_shortest_partition!(check_scale_shortest_09, scaling_cache_index_09, 9); + check_scale_shortest_partition!(check_scale_shortest_10, scaling_cache_index_10, 10); + check_scale_shortest_partition!(check_scale_shortest_11, scaling_cache_index_11, 11); + check_scale_shortest_partition!(check_scale_shortest_12, scaling_cache_index_12, 12); + check_scale_shortest_partition!(check_scale_shortest_13, scaling_cache_index_13, 13); + check_scale_shortest_partition!(check_scale_shortest_14, scaling_cache_index_14, 14); + check_scale_shortest_partition!(check_scale_shortest_15, scaling_cache_index_15, 15); + check_scale_shortest_partition!(check_scale_shortest_16, scaling_cache_index_16, 16); + check_scale_shortest_partition!(check_scale_shortest_17, scaling_cache_index_17, 17); + check_scale_shortest_partition!(check_scale_shortest_18, scaling_cache_index_18, 18); + check_scale_shortest_partition!(check_scale_shortest_19, scaling_cache_index_19, 19); + check_scale_shortest_partition!(check_scale_shortest_20, scaling_cache_index_20, 20); + check_scale_shortest_partition!(check_scale_shortest_21, scaling_cache_index_21, 21); + check_scale_shortest_partition!(check_scale_shortest_22, scaling_cache_index_22, 22); + check_scale_shortest_partition!(check_scale_shortest_23, scaling_cache_index_23, 23); + check_scale_shortest_partition!(check_scale_shortest_24, scaling_cache_index_24, 24); + check_scale_shortest_partition!(check_scale_shortest_25, scaling_cache_index_25, 25); + check_scale_shortest_partition!(check_scale_shortest_26, scaling_cache_index_26, 26); + check_scale_shortest_partition!(check_scale_shortest_27, scaling_cache_index_27, 27); + check_scale_shortest_partition!(check_scale_shortest_28, scaling_cache_index_28, 28); + check_scale_shortest_partition!(check_scale_shortest_29, scaling_cache_index_29, 29); + check_scale_shortest_partition!(check_scale_shortest_30, scaling_cache_index_30, 30); + check_scale_shortest_partition!(check_scale_shortest_31, scaling_cache_index_31, 31); + check_scale_shortest_partition!(check_scale_shortest_32, scaling_cache_index_32, 32); + check_scale_shortest_partition!(check_scale_shortest_33, scaling_cache_index_33, 33); + check_scale_shortest_partition!(check_scale_shortest_34, scaling_cache_index_34, 34); + check_scale_shortest_partition!(check_scale_shortest_35, scaling_cache_index_35, 35); + check_scale_shortest_partition!(check_scale_shortest_36, scaling_cache_index_36, 36); + check_scale_shortest_partition!(check_scale_shortest_37, scaling_cache_index_37, 37); + check_scale_shortest_partition!(check_scale_shortest_38, scaling_cache_index_38, 38); + check_scale_shortest_partition!(check_scale_shortest_39, scaling_cache_index_39, 39); + check_scale_shortest_partition!(check_scale_shortest_40, scaling_cache_index_40, 40); + check_scale_shortest_partition!(check_scale_shortest_41, scaling_cache_index_41, 41); + check_scale_shortest_partition!(check_scale_shortest_42, scaling_cache_index_42, 42); + check_scale_shortest_partition!(check_scale_shortest_43, scaling_cache_index_43, 43); + check_scale_shortest_partition!(check_scale_shortest_44, scaling_cache_index_44, 44); + check_scale_shortest_partition!(check_scale_shortest_45, scaling_cache_index_45, 45); + check_scale_shortest_partition!(check_scale_shortest_46, scaling_cache_index_46, 46); + check_scale_shortest_partition!(check_scale_shortest_47, scaling_cache_index_47, 47); + check_scale_shortest_partition!(check_scale_shortest_48, scaling_cache_index_48, 48); + check_scale_shortest_partition!(check_scale_shortest_49, scaling_cache_index_49, 49); + check_scale_shortest_partition!(check_scale_shortest_50, scaling_cache_index_50, 50); + check_scale_shortest_partition!(check_scale_shortest_51, scaling_cache_index_51, 51); + check_scale_shortest_partition!(check_scale_shortest_52, scaling_cache_index_52, 52); + check_scale_shortest_partition!(check_scale_shortest_53, scaling_cache_index_53, 53); + check_scale_shortest_partition!(check_scale_shortest_54, scaling_cache_index_54, 54); + check_scale_shortest_partition!(check_scale_shortest_55, scaling_cache_index_55, 55); + check_scale_shortest_partition!(check_scale_shortest_56, scaling_cache_index_56, 56); + check_scale_shortest_partition!(check_scale_shortest_57, scaling_cache_index_57, 57); + check_scale_shortest_partition!(check_scale_shortest_58, scaling_cache_index_58, 58); + check_scale_shortest_partition!(check_scale_shortest_59, scaling_cache_index_59, 59); + check_scale_shortest_partition!(check_scale_shortest_60, scaling_cache_index_60, 60); + check_scale_shortest_partition!(check_scale_shortest_61, scaling_cache_index_61, 61); + check_scale_shortest_partition!(check_scale_shortest_62, scaling_cache_index_62, 62); + check_scale_shortest_partition!(check_scale_shortest_63, scaling_cache_index_63, 63); + check_scale_shortest_partition!(check_scale_shortest_64, scaling_cache_index_64, 64); + check_scale_shortest_partition!(check_scale_shortest_65, scaling_cache_index_65, 65); + check_scale_shortest_partition!(check_scale_shortest_66, scaling_cache_index_66, 66); + check_scale_shortest_partition!(check_scale_shortest_67, scaling_cache_index_67, 67); + check_scale_shortest_partition!(check_scale_shortest_68, scaling_cache_index_68, 68); + check_scale_shortest_partition!(check_scale_shortest_69, scaling_cache_index_69, 69); + check_scale_shortest_partition!(check_scale_shortest_70, scaling_cache_index_70, 70); + check_scale_shortest_partition!(check_scale_shortest_71, scaling_cache_index_71, 71); + check_scale_shortest_partition!(check_scale_shortest_72, scaling_cache_index_72, 72); + check_scale_shortest_partition!(check_scale_shortest_73, scaling_cache_index_73, 73); + check_scale_shortest_partition!(check_scale_shortest_74, scaling_cache_index_74, 74); + check_scale_shortest_partition!(check_scale_shortest_75, scaling_cache_index_75, 75); + check_scale_shortest_partition!(check_scale_shortest_76, scaling_cache_index_76, 76); + check_scale_shortest_partition!(check_scale_shortest_77, scaling_cache_index_77, 77); + check_scale_shortest_partition!(check_scale_shortest_78, scaling_cache_index_78, 78); + check_scale_shortest_partition!(check_scale_shortest_79, scaling_cache_index_79, 79); + + // At digit one the loop must stop. If its remainder would already exceed + // threshold, the loop's threshold check forces an earlier stop. With digit + // zero this checks the initial state, which must not decrement at all. + // Callers establish this numeric condition before using the helper model. + fn weed_stops_before_zero( + digit: u8, + remainder: u64, + threshold: u64, + plus1v: u64, + ten_kappa: u64, + ulp: u64, + ) -> bool { + // The caller bounds digit by nine. Saturation preserves comparison + // with threshold: an overflowing mathematical sum exceeds every u64. + // Repeated additions also match the real loop's remainder updates. + let after_1 = remainder.saturating_add(ten_kappa); + let after_2 = after_1.saturating_add(ten_kappa); + let after_3 = after_2.saturating_add(ten_kappa); + let after_4 = after_3.saturating_add(ten_kappa); + let after_5 = after_4.saturating_add(ten_kappa); + let after_6 = after_5.saturating_add(ten_kappa); + let after_7 = after_6.saturating_add(ten_kappa); + let after_8 = after_7.saturating_add(ten_kappa); + let terminal = match digit { + b'0' | b'1' => remainder, + b'2' => after_1, + b'3' => after_2, + b'4' => after_3, + b'5' => after_4, + b'6' => after_5, + b'7' => after_6, + b'8' => after_7, + _ => after_8, + }; + if terminal >= threshold { + true + } else { + let remainder = terminal; + let target = plus1v - ulp; + !(remainder < target + && threshold - remainder >= ten_kappa + && (remainder + ten_kappa < target + || target - remainder >= remainder + ten_kappa - target)) + } + } + + // Quantify the other prefix bytes inside the proof. The adapter checks the + // actual prefix and passes the final digit, which is the only byte changed. + #[kani::requires( + len > 0 && len <= PROOF_BUFLEN + && digit >= b'0' && digit <= b'9' + && remainder < threshold && ten_kappa > 0 + && ulp <= threshold / 4 && ulp <= plus1v && plus1v <= u64::MAX - ulp + && weed_stops_before_zero(digit, remainder, threshold, plus1v, ten_kappa, ulp) + )] + #[kani::ensures(|result| result.as_ref().is_none_or(|&(written, output_exp)| { + written == len && output_exp == exp + }))] + fn round_shortest_contract( + digit: u8, + len: usize, + exp: i16, + remainder: u64, + threshold: u64, + plus1v: u64, + ten_kappa: u64, + ulp: u64, + ) -> Option<(usize, i16)> { + let mut digits: [u8; PROOF_BUFLEN] = kani::any(); + digits[len - 1] = digit; + kani::assume(crate::num::imp::flt2dec::rounding_verify::prefix_all(&digits, len, |byte| { + byte < u8::MAX + })); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_mut_ptr().cast::(); + // SAFETY: the contract bounds len by both distinct arrays' capacities. + unsafe { crate::ptr::copy_nonoverlapping(digits.as_ptr(), start, len) }; + round_and_weed( + // SAFETY: the copy initialized exactly this prefix. + unsafe { buf[..len].assume_init_mut() }, + exp, + remainder, + threshold, + plus1v, + ten_kappa, + ulp, + ) + .map(|(output, output_exp)| { + assert_eq!(output.as_ptr(), start.cast_const()); + let checksum = crate::num::imp::flt2dec::rounding_verify::prefix_checksum(output); + kani::cover(checksum == 0, "shortest rounding returns readable bytes"); + kani::cover(output[len - 1] < digit, "shortest rounding can decrease the final digit"); + (output.len(), output_exp) + }) + } + + fn stub_round_and_weed( + buf: &mut [u8], + exp: i16, + remainder: u64, + threshold: u64, + plus1v: u64, + ten_kappa: u64, + ulp: u64, + ) -> Option<(&[u8], i16)> { + let len = buf.len(); + assert!(len > 0 && len <= PROOF_BUFLEN); + // Read and check every actual byte before invoking the numeric summary. + assert!(crate::num::imp::flt2dec::rounding_verify::bytes_below_max(buf)); + assert!(buf[len - 1] >= b'0' && buf[len - 1] <= b'9', "shortest final digit is decimal"); + assert!(remainder < threshold, "shortest remainder is inside the rounding interval"); + assert!(ten_kappa > 0, "shortest decimal unit is positive"); + assert!(ulp <= threshold / 4, "shortest interval includes four error units"); + assert!( + ulp <= plus1v && plus1v <= u64::MAX - ulp, + "shortest target leaves error-unit room" + ); + assert!( + ulp > plus1v + || weed_stops_before_zero( + buf[len - 1], + remainder, + threshold, + plus1v, + ten_kappa, + ulp + ), + "shortest weeding stops before zero" + ); + + let result = round_shortest_contract( + buf[len - 1], + len, + exp, + remainder, + threshold, + plus1v, + ten_kappa, + ulp, + ); + // Overapproximate output values within the initialized input prefix. + let output: [u8; PROOF_BUFLEN] = kani::any(); + buf.copy_from_slice(&output[..len]); + result.map(|(written, output_exp)| (&buf[..written], output_exp)) + } + + // The last digit is at most nine and the numeric precondition makes the + // loop stop before decrementing one, so at most eight iterations execute. + #[kani::proof_for_contract(round_shortest_contract)] + #[kani::unwind(9)] + #[kani::solver(kissat)] + fn check_round_shortest_contract() { + let digit: u8 = kani::any(); + let len = usize::from(kani::any::()); + let result = round_shortest_contract( + digit, + len, + kani::any(), + kani::any(), + kani::any(), + kani::any(), + kani::any(), + kani::any(), + ); + kani::cover(len == 1 && result.is_some(), "shortest rounding accepts a single digit"); + kani::cover( + len == PROOF_BUFLEN && result.is_some(), + "shortest rounding accepts a full buffer", + ); + kani::cover(result.is_none(), "shortest rounding can request the Dragon fallback"); + } + + // Keep uninitialized padding in this proof: only the initialized input + // prefix is copied before executing the real final-rounding helper. + #[kani::requires( + len <= capacity && capacity <= PROOF_BUFLEN + && exp < i16::MAX && remainder < ten_kappa + )] + #[kani::ensures(|result| result.as_ref().is_none_or(|&(written, _)| { + written >= len && written <= capacity && written - len <= 1 + }))] + fn round_exact_contract( + len: usize, + capacity: usize, + exp: i16, + limit: i16, + remainder: u64, + ten_kappa: u64, + ulp: u64, + ) -> Option<(usize, i16)> { + // Quantify every prefix allowed by the adapter's byte check here. + // The postcondition does not depend on its values, so callers pass + // only the numeric state after checking their actual input bytes. + let digits: [u8; PROOF_BUFLEN] = kani::any(); + kani::assume(crate::num::imp::flt2dec::rounding_verify::prefix_all( + &digits, + len, + |digit| digit < u8::MAX, + )); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_mut_ptr().cast::(); + // SAFETY: the contract bounds len by both distinct arrays' capacities. + // This initializes exactly the prefix required by possibly_round. + unsafe { crate::ptr::copy_nonoverlapping(digits.as_ptr(), start, len) }; + // SAFETY: the copy above initialized the first len bytes. + unsafe { possibly_round(&mut buf[..capacity], len, exp, limit, remainder, ten_kappa, ulp) } + .map(|(output, output_exp)| { + assert_eq!(output.as_ptr(), start.cast_const()); + // Read every returned byte, including any appended carry. This + // checks initialization instead of merely inspecting slice metadata. + let checksum = crate::num::imp::flt2dec::rounding_verify::prefix_checksum(output); + kani::cover(checksum == 0, "final rounding returns readable output bytes"); + (output.len(), output_exp) + }) + } + + // The verified contract overapproximates byte values and the decision to + // return None. It can write only the input prefix or a proved output prefix; + // unused padding in the generator's buffer remains uninitialized. + unsafe fn stub_possibly_round( + buf: &mut [MaybeUninit], + len: usize, + exp: i16, + limit: i16, + remainder: u64, + ten_kappa: u64, + ulp: u64, + ) -> Option<(&[u8], i16)> { + assert!(len <= buf.len() && buf.len() <= PROOF_BUFLEN); + // SAFETY: this adapter has possibly_round's initialized-prefix contract. + // Read every active byte before the model can write to this buffer. + let digits = unsafe { buf[..len].assume_init_ref() }; + assert!(crate::num::imp::flt2dec::rounding_verify::bytes_below_max(digits)); + let result = round_exact_contract(len, buf.len(), exp, limit, remainder, ten_kappa, ulp); + let written = result.map_or(len, |(written, _)| written); + assert!(written <= buf.len()); + // The contract proves metadata and initialized output, without a byte + // value postcondition. Model those bytes here so the verified call has + // an empty write set instead of repeatedly havocing an array. + let output: [u8; PROOF_BUFLEN] = kani::any(); + // SAFETY: the contract bounds written by both distinct arrays. All + // source bytes are initialized; this initializes only the active prefix. + unsafe { + crate::ptr::copy_nonoverlapping(output.as_ptr(), buf.as_mut_ptr().cast(), written) + }; + result.map(|(written, output_exp)| { + // SAFETY: the copy above initialized this prefix. + (unsafe { buf[..written].assume_init_ref() }, output_exp) + }) + } + + #[kani::proof_for_contract(round_exact_contract)] + #[kani::unwind(33)] + #[kani::stub( + crate::num::imp::flt2dec::round_up, + crate::num::imp::flt2dec::rounding_verify::stub_round_up + )] + #[kani::stub_verified(crate::num::imp::flt2dec::rounding_verify::round_up_contract)] + #[kani::solver(kissat)] + fn check_round_exact_contract() { + let len = usize::from(kani::any::()); + let capacity = usize::from(kani::any::()); + let result = round_exact_contract( + len, + capacity, + kani::any(), + kani::any(), + kani::any(), + kani::any(), + kani::any(), + ); + kani::cover(len == 0 && result.is_some(), "final rounding accepts an empty prefix"); + kani::cover( + len == PROOF_BUFLEN && result.is_some(), + "final rounding accepts a full buffer", + ); + kani::cover(result.is_none(), "final rounding can request the Dragon fallback"); + kani::cover( + result.is_some_and(|(written, _)| written > len), + "final rounding can append an initialized carry", + ); + } + + // An arbitrary `Decoded` satisfying every precondition the `grisu` entry + // points assert. `mant + plus < 2^61` (and the `checked_add`/`checked_sub` + // assumptions) keep the scaled `Fp` arithmetic inside `u64`. + fn arbitrary_decoded() -> Decoded { + let mant: u64 = kani::any(); + let minus: u64 = kani::any(); + let plus: u64 = kani::any(); + kani::assume(mant > 0); + kani::assume(minus > 0); + kani::assume(plus > 0); + kani::assume(mant.checked_add(plus).is_some()); + kani::assume(mant.checked_sub(minus).is_some()); + kani::assume(mant + plus < (1 << 61)); + let exp: i16 = kani::any(); + // `[-1076, 970]` is the exponent range of `decode()`; 971 is unreachable. + kani::assume(exp >= -1076 && exp <= 970); + Decoded { mant, minus, plus, exp, inclusive: kani::any() } + } + + // An arbitrary input for the exact-mode proofs: the full documented + // precondition of `format_exact_opt` (`0 < mant < 2^61`), with `exp` + // bounded to the decoder image so `cached_power` stays in its table domain. + fn arbitrary_decoded_exact() -> Decoded { + let mant: u64 = kani::any(); + kani::assume(mant > 0 && mant < (1 << 61)); + let exp: i16 = kani::any(); + kani::assume(exp >= -1076 && exp <= 970); + Decoded { mant, minus: 1, plus: 1, exp, inclusive: kani::any() } + } + + // Keep each division's literal divisor visible to the verifier. Each arm + // has a separate equivalence proof over every u32 dividend. The default + // arm checks the divisor domain at every generator call. + fn div_rem_pow10_model(value: u32, divisor: u32) -> (u32, u32) { + match divisor { + 1 => (value, 0), + 10 => (value / 10, value % 10), + 100 => (value / 100, value % 100), + 1_000 => (value / 1_000, value % 1_000), + 10_000 => (value / 10_000, value % 10_000), + 100_000 => (value / 100_000, value % 100_000), + 1_000_000 => (value / 1_000_000, value % 1_000_000), + 10_000_000 => (value / 10_000_000, value % 10_000_000), + 100_000_000 => (value / 100_000_000, value % 100_000_000), + 1_000_000_000 => (value / 1_000_000_000, value % 1_000_000_000), + _ => panic!("Grisu integral divisor must be a power of ten"), + } + } + + macro_rules! check_decimal_divisor { + ($name:ident, $divisor:literal) => { + #[kani::proof] + #[kani::solver(kissat)] + fn $name() { + let value: u32 = kani::any(); + assert_eq!(div_rem_pow10(value, $divisor), div_rem_pow10_model(value, $divisor)); + kani::cover(value == 0, "decimal division includes zero"); + kani::cover(value == u32::MAX, "decimal division includes the largest dividend"); + kani::cover( + value == $divisor - 1, + "decimal division includes the largest remainder", + ); + } + }; + } + + check_decimal_divisor!(check_decimal_division_model_agrees_00, 1); + check_decimal_divisor!(check_decimal_division_model_agrees_01, 10); + check_decimal_divisor!(check_decimal_division_model_agrees_02, 100); + check_decimal_divisor!(check_decimal_division_model_agrees_03, 1_000); + check_decimal_divisor!(check_decimal_division_model_agrees_04, 10_000); + check_decimal_divisor!(check_decimal_division_model_agrees_05, 100_000); + check_decimal_divisor!(check_decimal_division_model_agrees_06, 1_000_000); + check_decimal_divisor!(check_decimal_division_model_agrees_07, 10_000_000); + check_decimal_divisor!(check_decimal_division_model_agrees_08, 100_000_000); + check_decimal_divisor!(check_decimal_division_model_agrees_09, 1_000_000_000); + + struct CachedPowerIndex(i32); + + impl CachedPowerIndex { + fn check(&self, actual: i32) -> i32 { + assert_eq!(actual, self.0); + self.0 + } + } + + fn checked_cached_power_39(index: i32) -> i32 { + CachedPowerIndex(39).check(index) + } + + // Call the real generator loops and compose the final-rounding proof. The + // wrapper harness below checks a separate obligation. + macro_rules! check_partition { + // No solver proves the shortest harness of these partitions whole, so + // eight slices of eight exponents each replace it. The union of the + // slices is the partition, and every slice checks the same property. + ($name:ident, arbitrary_finite_f32, $group:literal, $cover_fallback:literal) => { + check_partition!(@sliced $name, arbitrary_finite_f32, arbitrary_finite_f32_slice, $group, $cover_fallback); + }; + (f64_00, arbitrary_finite_f64, $group:literal, $cover_fallback:literal) => { + check_partition!(@sliced f64_00, arbitrary_finite_f64, arbitrary_finite_f64_slice, $group, $cover_fallback); + }; + (@sliced $name:ident, $decode:ident, $slice:ident, $group:literal, $cover_fallback:literal) => { + mod $name { + use super::*; + + check_partition!(@exact $decode::<$group>(), $cover_fallback); + check_partition!(@slices $slice, $group, $cover_fallback, s0 0, s1 1, s2 2, s3 3, s4 4, s5 5, s6 6, s7 7); + } + }; + (@slices $slice:ident, $group:literal, $cover_fallback:literal, $($index_name:ident $index:literal),+) => { + $( + mod $index_name { + use super::*; + use crate::num::imp::flt2dec::flt2dec_verify::$slice; + + check_partition!(@shortest $slice::<$group, $index>(), $cover_fallback => scale_shortest_contract); + } + )+ + }; + ($name:ident, $decode:ident, $group:literal, $cover_fallback:literal) => { + check_partition!($name, $decode::<$group>(), $cover_fallback => scale_shortest_contract); + }; + ($name:ident, $decoded:expr, $cover_fallback:literal $(, $cached_index:path)? $(=> $scaling_contract:path)?) => { + mod $name { + use super::*; + + check_partition!(@shortest $decoded, $cover_fallback $(, $cached_index)? $(=> $scaling_contract)?); + check_partition!(@exact $decoded, $cover_fallback $(, $cached_index)?); + } + }; + (@shortest $decoded:expr, $cover_fallback:literal $(, $cached_index:path)? $(=> $scaling_contract:path)?) => { + #[kani::proof] + #[kani::unwind(19)] + $(#[kani::stub(proof_cached_power_index, $cached_index)])? + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::stub(round_and_weed, stub_round_and_weed)] + #[kani::stub(div_rem_pow10, div_rem_pow10_model)] + #[kani::stub_verified(round_shortest_contract)] + #[kani::stub_verified(advance_fraction_threshold)] + $( + #[kani::stub(scale_shortest, stub_scale_shortest)] + #[kani::stub_verified($scaling_contract)] + )? + #[kani::solver(minisat)] + fn check_format_shortest_opt() { + let d = $decoded; + let len = usize::from(kani::any::()); + kani::assume(len >= MAX_SIG_DIGITS && len <= PROOF_BUFLEN); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_ptr().cast::(); + kani::cover(len == MAX_SIG_DIGITS, "shortest uses the minimum buffer"); + kani::cover(len == PROOF_BUFLEN, "shortest uses the largest proof buffer"); + let result = format_shortest_opt(&d, &mut buf[..len]); + if $cover_fallback { + kani::cover(result.is_none(), "shortest can request the Dragon fallback"); + } + let _ = result.map(|(digits, _)| { + kani::cover(digits.len() > 1, "shortest produces multiple digits"); + assert!(!digits.is_empty()); + assert!(digits.len() <= len); + assert_eq!(digits.as_ptr(), start); + }); + } + }; + (@exact $decoded:expr, $cover_fallback:literal $(, $cached_index:path)?) => { + // Integral extraction takes at most ten iterations. Fractional + // extraction stops once err = 10^18 >= 2^59: the cached power + // gives e <= 60, hence maxerr = 2^(e - 1) <= 2^59. The separate + // rounding contract retains its own 33-iteration proof bound. + #[kani::proof] + #[kani::unwind(19)] + $(#[kani::stub(proof_cached_power_index, $cached_index)])? + #[kani::stub( + u64::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u64 + )] + #[kani::stub( + u32::leading_zeros, + crate::num::imp::flt2dec::bit_scan_verify::leading_zeros_u32 + )] + #[kani::stub(possibly_round, stub_possibly_round)] + #[kani::stub_verified(round_exact_contract)] + #[kani::stub_verified(normalized_multiply_contract)] + #[kani::stub(Fp::mul, stub_normalized_multiply)] + #[kani::solver(kissat)] + fn check_format_exact_opt() { + let d = $decoded; + let limit: i16 = kani::any(); + let len = usize::from(kani::any::()); + kani::assume(len > 0 && len <= PROOF_BUFLEN); + let mut buf = [const { MaybeUninit::uninit() }; PROOF_BUFLEN]; + let start = buf.as_ptr().cast::(); + kani::cover(len == 1, "exact uses a one-byte buffer"); + kani::cover(len == PROOF_BUFLEN, "exact uses the largest proof buffer"); + let result = format_exact_opt(&d, &mut buf[..len], limit); + if $cover_fallback { + kani::cover(result.is_none(), "exact can request the Dragon fallback"); + } + let _ = result.map(|(digits, _)| { + kani::cover(digits.is_empty(), "exact can return an empty prefix"); + kani::cover(digits.len() > 1, "exact produces multiple digits"); + assert!(digits.len() <= len); + assert_eq!(digits.as_ptr(), start); + }); + } + }; + } + + for_each_finite_partition!(check_partition); + check_partition!(f64_exp_1023, arbitrary_finite_f64_exponent::<1023>(), false); + + // All positive f64 values in [2^-8, 2^18), with every significand bit symbolic. + // These additional probes assert the cached index instead of assuming it. + check_partition!( + f64_cached_power_39, + arbitrary_finite_f64_range::<0x3f70_0000_0000_0000, 0x4110_0000_0000_0000>(), + false, + checked_cached_power_39 + ); + + // Wholesale havoc stub for the dragon fallback (modelled as an opaque op that + // writes a digit and returns an in-bounds slice of `buf`). + fn stub_dragon_format_exact<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + _limit: i16, + ) -> (&'a [u8], i16) { + let digit: u8 = kani::any(); + kani::assume(digit >= b'0' && digit <= b'9'); + buf[0] = MaybeUninit::new(digit); + // SAFETY: we just initialized element 0. + (unsafe { buf[..1].assume_init_ref() }, kani::any()) + } + + // Wholesale havoc stub for `format_exact_opt`: nondeterministically returns + // `None` (released its borrow of `buf`) or `Some` slice of `buf`. Modelling + // both callees as opaque isolates the WRAPPER's only `unsafe`: the + // lifetime-laundering reborrow `&mut *(buf as *mut _)`, whose soundness rests + // on `buf` being reused only on the `None` path. + fn stub_format_exact_opt<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + _limit: i16, + ) -> Option<(&'a [u8], i16)> { + if kani::any() { + let digit: u8 = kani::any(); + kani::assume(digit >= b'0' && digit <= b'9'); + buf[0] = MaybeUninit::new(digit); + // SAFETY: we just initialized element 0. + Some((unsafe { buf[..1].assume_init_ref() }, kani::any())) + } else { + // The real function writes digits before it gives up; model that + // dirtying so the wrapper's reuse of `buf` on the `None` path is + // exercised against a modified buffer. + buf[0] = MaybeUninit::new(kani::any()); + None + } + } + + #[kani::proof] + #[kani::stub(format_exact_opt, stub_format_exact_opt)] + #[kani::stub( + crate::num::imp::flt2dec::strategy::dragon::format_exact, + stub_dragon_format_exact + )] + fn check_format_exact() { + let d = arbitrary_decoded_exact(); + let limit: i16 = kani::any(); + let mut buf: [MaybeUninit; 4] = [const { MaybeUninit::uninit() }; 4]; + let _ = format_exact(&d, &mut buf, limit); + } + + // Wholesale havoc stubs for `format_shortest`'s callees (no `limit` arg). + fn stub_dragon_format_shortest<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + ) -> (&'a [u8], i16) { + let digit: u8 = kani::any(); + kani::assume(digit >= b'0' && digit <= b'9'); + buf[0] = MaybeUninit::new(digit); + // SAFETY: we just initialized element 0. + (unsafe { buf[..1].assume_init_ref() }, kani::any()) + } + + fn stub_format_shortest_opt<'a>( + _d: &Decoded, + buf: &'a mut [MaybeUninit], + ) -> Option<(&'a [u8], i16)> { + if kani::any() { + let digit: u8 = kani::any(); + kani::assume(digit >= b'0' && digit <= b'9'); + buf[0] = MaybeUninit::new(digit); + // SAFETY: we just initialized element 0. + Some((unsafe { buf[..1].assume_init_ref() }, kani::any())) + } else { + // The real function writes digits before it gives up; model that + // dirtying so the wrapper's reuse of `buf` on the `None` path is + // exercised against a modified buffer. + buf[0] = MaybeUninit::new(kani::any()); + None + } + } + + // `format_shortest` mirrors `format_exact`: its only `unsafe` is the + // lifetime-laundering reborrow, verified by modelling both callees as opaque. + #[kani::proof] + #[kani::stub(format_shortest_opt, stub_format_shortest_opt)] + #[kani::stub( + crate::num::imp::flt2dec::strategy::dragon::format_shortest, + stub_dragon_format_shortest + )] + fn check_format_shortest() { + let d = arbitrary_decoded(); + let mut buf: [MaybeUninit; 4] = [const { MaybeUninit::uninit() }; 4]; + let _ = format_shortest(&d, &mut buf); + } +} diff --git a/scripts/cbmc-skip-unused-loop-assigns.patch b/scripts/cbmc-skip-unused-loop-assigns.patch new file mode 100644 index 0000000000000..159244a89b4e9 --- /dev/null +++ b/scripts/cbmc-skip-unused-loop-assigns.patch @@ -0,0 +1,25 @@ +diff --git a/src/goto-instrument/contracts/dynamic-frames/dfcc_cfg_info.cpp b/src/goto-instrument/contracts/dynamic-frames/dfcc_cfg_info.cpp +--- a/src/goto-instrument/contracts/dynamic-frames/dfcc_cfg_info.cpp ++++ b/src/goto-instrument/contracts/dynamic-frames/dfcc_cfg_info.cpp +@@ -525,15 +525,19 @@ + + auto topsorted = loop_nesting_graph.topsort(); + + bool has_loops_with_contracts = false; ++ bool has_loops_without_assigns = false; + for(const auto idx : topsorted) + { + topsorted_loops.push_back(idx); + has_loops_with_contracts |= has_contract( + loop_nesting_graph[idx].latch, loop_contract_config.check_side_effect); ++ has_loops_without_assigns |= ++ get_loop_assigns(loop_nesting_graph[idx].latch).operands().empty(); + } + +- // We infer loop assigns for all loops in the function. +- if(has_loops_with_contracts) ++ // Explicit assigns clauses do not use inferred targets. Avoid inlining ++ // the function's callees when every loop already provides those targets. ++ if(has_loops_with_contracts && has_loops_without_assigns) + { + dfcc_infer_loop_assigns_for_function( diff --git a/scripts/kani-explicit-for-counter.patch b/scripts/kani-explicit-for-counter.patch new file mode 100644 index 0000000000000..ab18bf780be3c --- /dev/null +++ b/scripts/kani-explicit-for-counter.patch @@ -0,0 +1,107 @@ +diff --git a/kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs b/kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs +--- a/kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs ++++ b/kani-compiler/src/codegen_cprover_gotoc/codegen/statement.rs +@@ -163,8 +163,26 @@ + if localname.contains("kani_loop_modifies") { + let assigns = self.rvalue_to_assign_targets(rhs, location); + self.current_loop_modifies = assigns.clone(); ++ self.current_for_pattern_initialized = false; + return Stmt::skip(location); + } ++ // A for-loop's synthetic counter is created after its explicit ++ // frame. Include it so both havoc and assignment checks cover ++ // iterator advancement, just as inferred frames already do. ++ if !self.current_loop_modifies.is_empty() ++ && localname ++ .rsplit("::") ++ .next() ++ .is_some_and(|name| name.starts_with("kani_index_")) ++ { ++ let counter = unwrap_or_return_codegen_unimplemented_stmt!( ++ self, ++ self.codegen_place_stable(lhs, location) ++ ) ++ .goto_expr; ++ self.current_loop_modifies.push(counter); ++ } ++ + if localname.contains("kani_loop_decreases") { + if !self + .queries +@@ -768,6 +786,34 @@ + ) -> Stmt { + debug!(?func, ?args, ?destination, ?span, "codegen_funcall"); + let instance_opt = self.get_instance(func); ++ // A generated for-loop initializes its pattern before entering the ++ // loop. Add that local once, while its counter identifies the frame. ++ let synthetic_for_frame = self.current_loop_modifies.iter().any(|target| { ++ matches!( ++ target.value(), ++ ExprValue::Symbol { identifier } ++ if identifier.to_string().rsplit("::").next() ++ .is_some_and(|name| name.starts_with("kani_index_")) ++ ) ++ }); ++ if !self.current_for_pattern_initialized ++ && synthetic_for_frame ++ && let TyKind::RigidTy(RigidTy::FnDef(def, _)) = self.operand_ty_stable(func).kind() ++ { ++ let callee = def.name(); ++ if callee == "kani::KaniIter::first" ++ || callee.ends_with("::kani::KaniIter::first") ++ { ++ let location = self.codegen_span_stable(span); ++ let pattern = unwrap_or_return_codegen_unimplemented_stmt!( ++ self, ++ self.codegen_place_stable(destination, location) ++ ) ++ .goto_expr; ++ self.current_loop_modifies.push(pattern); ++ self.current_for_pattern_initialized = true; ++ } ++ } + if let Some(instance) = instance_opt + && matches!(instance.kind, InstanceKind::LlvmIntrinsic) + { +diff --git a/kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs b/kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs +--- a/kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs ++++ b/kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs +@@ -96,6 +96,7 @@ + pub has_loop_contracts: bool, + /// Track loop assign clause + pub current_loop_modifies: Vec, ++ pub current_for_pattern_initialized: bool, + /// Track loop decreases clause + pub current_loop_decreases: Option, + } +@@ -129,6 +130,7 @@ + transformer, + has_loop_contracts: false, + current_loop_modifies: Vec::new(), ++ current_for_pattern_initialized: false, + current_loop_decreases: None, + } + } +diff --git a/kani-compiler/src/kani_middle/transform/loop_contracts.rs b/kani-compiler/src/kani_middle/transform/loop_contracts.rs +--- a/kani-compiler/src/kani_middle/transform/loop_contracts.rs ++++ b/kani-compiler/src/kani_middle/transform/loop_contracts.rs +@@ -209,11 +209,17 @@ + else { + continue; + }; +- if fn_def.name() == "kani::KaniIter::first" { ++ let function_name = fn_def.name(); ++ if function_name == "kani::KaniIter::first" ++ || function_name.ends_with("::kani::KaniIter::first") ++ { + current_firstpat = dest.local; + current_firstpat_pos = blockid; + } +- if fn_def.name() == "kani::KaniIter::nth" && current_firstpat != 0 { ++ if current_firstpat != 0 ++ && (function_name == "kani::KaniIter::nth" ++ || function_name.ends_with("::kani::KaniIter::nth")) ++ { + first_pats_and_nth_pats.push(( + current_firstpat, + dest.local, diff --git a/scripts/kani-std-analysis/cvc5-flat-smt.sh b/scripts/kani-std-analysis/cvc5-flat-smt.sh new file mode 100755 index 0000000000000..b224e31bbcb42 --- /dev/null +++ b/scripts/kani-std-analysis/cvc5-flat-smt.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +# Preserve the bit-vector query while solving multiplication through integers. +# Allow the integer arithmetic introduced by the bit-vector translation. +exec "${KANI_CVC5_BINARY:-cvc5}" --lang=smt2 --force-logic=ALL --solve-bv-as-int=sum "$@" diff --git a/scripts/kani-std-analysis/flt2dec_harnesses.py b/scripts/kani-std-analysis/flt2dec_harnesses.py new file mode 100644 index 0000000000000..030659541c73f --- /dev/null +++ b/scripts/kani-std-analysis/flt2dec_harnesses.py @@ -0,0 +1,430 @@ +"""Assign flt2dec proofs to dedicated CI jobs without duplicating std partitions.""" + +import argparse +import json +import sys + + +DRAGON = "num::imp::flt2dec::strategy::dragon::dragon_verify" +GRISU = "num::imp::flt2dec::strategy::grisu::grisu_verify" +OPERATING_SYSTEMS = ["ubuntu-latest", "macos-latest"] +# Grisu shortest partitions that the Rust macro proves in eight exponent slices. +SLICED_SHORTEST = {"f32_00", "f32_01", "f32_02", "f32_03", "f64_00"} +# A manual Grisu probe checks one loop-contract property in place of the whole harness. +GRISU_PROBES = { + "entry": "num::imp::flt2dec::strategy::grisu::generate_shortest.loop_invariant_base.1", + "step": "num::imp::flt2dec::strategy::grisu::generate_shortest.loop_invariant_step.1", + "integral-step": "num::imp::flt2dec::strategy::grisu::generate_integral_digits.loop_invariant_step.1", +} +# Dragon shortest scaling: dragon.rs estimates K = floor((nbits + exp) * DRAGON_K_ESTIMATE / 2^32) +# for nbits in 2..=55 and proves windows 000..127 one (window, K) pair at a time. +DRAGON_K_ESTIMATE = 1292913986 +DRAGON_FIXED_K_WINDOWS = 128 +# Windows 000 and 001 at K = -319..-311 are the slow band (routing ruling s46). +DRAGON_SLOW_BAND = range(-319, -310) +# Pairs that timed out at 30 or 60 minutes in their group of eight or four (run 39215660085, +# ubuntu or macOS): each runs alone at 180 minutes, its group keeps the other pairs (ruling s54 (a)). +DRAGON_SLOW_PAIRS = ( + (1, -310), (1, -314), (6, -285), (6, -286), (7, -283), (23, -206), (28, -184), (29, -175), + (34, -154), (34, -155), (46, -91), (46, -92), (46, -93), (46, -94), (46, -95), (47, -90), +) +# Pairs whose proof splits on the exponent (dragon.rs `kNNN => K => [eNN => offset]`): pair -> the +# exponent offsets of its pieces; each piece covers 4 exponents and runs alone at 180 minutes. +# Each pair here hit the 180-minute cap alone: (0, -317) in the slow band, the rest as slow pairs. +# They run in 4 pieces of 4 exponents; the 11 pairs whose pieces hit the cap again run in +# 16 pieces of 1 exponent. +DRAGON_SPLIT_PAIRS = { + **{pair: (0, 4, 8, 12) for pair in ((0, -317), (29, -172), (47, -91))}, + **{pair: tuple(range(16)) for pair in ( + (23, -200), (23, -201), (23, -202), (23, -203), (23, -204), (23, -205), (28, -183), + (29, -173), (29, -174), (34, -151), (34, -152), (34, -153), + )}, +} +# Pieces of the 16-piece split pairs that hit the 180-minute cap with CaDiCaL on ubuntu or macOS +# (w023 K = -202 e00 also with Kissat): pair -> the exponent offsets of those pieces. No group +# runs them and the general suite skips them, so CI leaves them unproved (ruling s76 (c); +# w023 K = -200 e01 and e02 by ruling s85: they hit the cap again in 4 and in 8 mantissa +# sub-pieces). +DRAGON_UNPROVED_PIECES = { + (23, -200): (1, 2, 4, 5, 6), (23, -201): (1, 2, 3, 4, 5), (23, -202): (0, 1, 2, 3, 4), + (23, -203): (0, 1, 2), (23, -204): (0, 1, 2), (23, -205): (0,), (28, -183): (0, 1), + (29, -173): (0, 1), (29, -174): (0, 1), (34, -151): (0, 1, 2, 3, 4, 5), (34, -152): (0, 1, 2), + (34, -153): (0, 1, 3), +} +# Pieces whose proof splits again on the mantissa (dragon.rs `eNN => offset => [mN => top]`): +# (window, K, piece offset) -> the sub-piece count. Piece e02 of (34, -153) hit the 180-minute +# cap; its 8 sub-pieces fix the top 3 mantissa bits, each alone at 180 minutes. +DRAGON_MANTISSA_SPLITS = {(34, -153, 2): 8} +# One CI matrix holds at most 256 jobs: core, long (generator and cached proofs), and the fixed-K +# pairs in four: dragon-a (windows 000..033), dragon-b (034..067), dragon-c (068..097), dragon-d (098..127). +# The pieces of the split pairs run in two more: dragon-e (offsets 0..7) and dragon-f (8..15). +MATRIX_JOBS = ("core", "long", "dragon-a", "dragon-b", "dragon-c", "dragon-d", "dragon-e", + "dragon-f") + + +def dragon_window_exponents(window): + """The first and last exponent of one 16-exponent Dragon scaling window.""" + first = -1076 + 16 * window + return first, min(first + 15, 970) + + +def dragon_fixed_k_range(window): + """The K values of one window: the estimate at (2, first) through the estimate at (55, last).""" + first, last = dragon_window_exponents(window) + return range(((2 + first) * DRAGON_K_ESTIMATE) >> 32, (((55 + last) * DRAGON_K_ESTIMATE) >> 32) + 1) + + +def dragon_fixed_k_pairs(): + """Every (window, K) pair of the fixed-K proofs (windows 000..127), in (window, K) order.""" + return [(window, k) for window in range(DRAGON_FIXED_K_WINDOWS) for k in dragon_fixed_k_range(window)] + + +def dragon_fixed_k_name(window, k): + """The catalog name part of one pair: `000-k324` for K = -324, `063-p000` for K = 0.""" + return f"{window:03d}-{'k' if k < 0 else 'p'}{abs(k):03d}" + + +def dragon_fixed_k_harness(window, k): + """The Kani harness of one pair: module wNNN holds one module kNNN (K < 0) or pNNN (K >= 0).""" + return f"{DRAGON}::dragon_scale_fixed_k::w{window:03d}::{'k' if k < 0 else 'p'}{abs(k):03d}::check_scale_shortest" + + +def dragon_fixed_k_piece_harness(window, k, offset, top=None): + """The Kani harness of one piece of a split pair: module eNN (NN = offset) inside the pair module; + a sub-piece of a piece split on the mantissa adds module mN (N = top) inside that.""" + pair = dragon_fixed_k_harness(window, k).removesuffix("::check_scale_shortest") + sub = "" if top is None else f"::m{top}" + return f"{pair}::e{offset:02d}{sub}::check_scale_shortest" + + +def dragon_unproved_harnesses(): + """The Kani harnesses of the unproved pieces (DRAGON_UNPROVED_PIECES), in pair and offset order.""" + return [dragon_fixed_k_piece_harness(window, k, offset) + for (window, k), offsets in DRAGON_UNPROVED_PIECES.items() for offset in offsets] + + +def dragon_matrix_job(window): + """Windows 000..033 run in the dragon-a matrix job, 034..067 in dragon-b, 068..097 in dragon-c + and 098..127 in dragon-d.""" + return next(job for first, job in ((98, "dragon-d"), (68, "dragon-c"), (34, "dragon-b"), (0, "dragon-a")) + if window >= first) + + +def proof_groups(pointer_width=None): + if pointer_width is None: + pointer_width = 64 if sys.maxsize > 2**32 else 32 + if pointer_width not in (16, 32, 64): + raise ValueError("Expected a 16-, 32-, or 64-bit target") + limb_count = 20 if pointer_width == 64 else 40 + groups = [] + + # One CI matrix holds at most 256 jobs, so generator and cached proofs form a second ("long") job. + def add(name, kind, harnesses, timeout_minutes, backend="native", harness_backends=None, + matrix_job="core"): + group = {"name": name, "kind": kind, "harnesses": harnesses, + "timeout_minutes": timeout_minutes, "backend": backend, "matrix_job": matrix_job} + if harness_backends: + group["harness_backends"] = harness_backends + groups.append(group) + + for name, module, proof, batch_size, timeout_minutes in [ + ("dragon-exact", DRAGON, "check_format_exact", 4, 60), + ("dragon-shortest", DRAGON, "check_format_shortest", 4, 60), + ("grisu-exact", GRISU, "check_format_exact_opt", 4, 60), + ("grisu-shortest", GRISU, "check_format_shortest_opt", 2, 120), + ]: + for width, count in ((32, 4), (64, 32)): + for first in range(0, count, batch_size): + last = first + batch_size - 1 + partition = f"f{width}" if count == batch_size else f"f{width}-{first:02d}-{last:02d}" + # Neither cvc5 nor kissat proves a sliced Grisu shortest partition whole; + # the slice groups below hold it instead. f64-00-01 keeps only f64_01. + harnesses = [f"{module}::f{width}_{group:02d}::{proof}" for group in range(first, last + 1) + if name != "grisu-shortest" or f"f{width}_{group:02d}" not in SLICED_SHORTEST] + if harnesses: + add(f"{name}-{partition}", f"generator-f{width}", harnesses, timeout_minutes, + matrix_job="long") + # Each slice holds eight exponents; the eight slices of a partition form the partition. + for partition in sorted(SLICED_SHORTEST) if name == "grisu-shortest" else []: + for index in range(8): + add(f"{name}-{partition.replace('_', '-')}-slice-{index}", f"generator-{partition[:3]}", + [f"{module}::{partition}::s{index}::{proof}"], timeout_minutes, matrix_job="long") + add(f"{name}-fixed-exponent", "probe", [ + f"{module}::f64_exp_1023::{proof}" + ], timeout_minutes) + + # Ubuntu proved the shortest probe in 65 minutes; macOS is about twice as slow. + for mode in ("exact", "shortest"): + add(f"grisu-{mode}-cached-power-39", "probe", [ + f"{GRISU}::f64_cached_power_39::check_format_{mode}_opt" + ], 180 if mode == "shortest" else 60) + + add("dragon-exact-loop-frame", "equivalence", [ + f"{DRAGON}::check_exact_digit_loop_frame" + ], 30) + + add("grisu-fractional-threshold-contract", "contract", [ + f"{GRISU}::check_advance_fraction_threshold" + ], 30, "cvc5") + + foundations = {"contract": [], "equivalence": []} + for name, module, proof, kind in [ + ("division-contract", DRAGON, "check_div_2pow10_contract", "contract"), + ("limb-division-contract", DRAGON, "check_div_rem_digit_contract", "contract"), + ("limb-multiplication-contract", DRAGON, "check_carrying_mul_add_contract", "contract"), + ("bigint-small-division-contract", DRAGON, "check_div_rem_small_contract", "contract"), + ("rounding-contract", "num::imp::flt2dec::rounding_verify", "check_round_up_contract", "contract"), + ("grisu-exact-rounding-contract", GRISU, "check_round_exact_contract", "contract"), + ("grisu-shortest-rounding-contract", GRISU, "check_round_shortest_contract", "contract"), + ("grisu-shortest-normalization-contract", GRISU, "check_normalize_shortest", "contract"), + ("grisu-rounded-multiplication-contract", GRISU, "check_rounded_multiply", "contract"), + ("grisu-normalized-multiplication-contract", GRISU, "check_normalized_multiply", "contract"), + ("grisu-shortest-scaling-partitioning", GRISU, "check_scale_shortest_partitioning", "equivalence"), + ("dragon-shortest-scaling-partitioning", DRAGON, "check_dragon_scale_partitioning", "equivalence"), + ("comparison-equivalence", DRAGON, "check_comparison_models_agree", "equivalence"), + ("zero-fill-equivalence", DRAGON, "check_fill_zeroes_model_agrees", "equivalence"), + ("addition-equivalence", DRAGON, "check_add_model_agrees", "equivalence"), + ("subtraction-equivalence", DRAGON, "check_sub_model_agrees", "equivalence"), + ("bit-scan-equivalence", "num::imp::flt2dec::bit_scan_verify", "check_leading_zeros_models_agree", "equivalence"), + ]: + foundations[kind].append(f"{module}::{proof}") + + for kind, harnesses in foundations.items(): + for first in range(0, len(harnesses), 8): + add(f"foundations-{kind}-{first:02d}", kind, harnesses[first:first + 8], 30) + + # Windows 000..127: one fixed-K proof per (window, K) pair, and the estimate range the pairs cover. + add("dragon-shortest-scaling-k-range", "equivalence", [f"{DRAGON}::check_dragon_scale_k_range"], 30) + + def add_fixed_k(pairs, timeout_minutes): + first, last = pairs[0], pairs[-1] + suffix = dragon_fixed_k_name(*first) + ("" if first == last else "-" + dragon_fixed_k_name(*last)) + add("dragon-shortest-scaling-fixed-k-" + suffix, "contract", + [dragon_fixed_k_harness(window, k) for window, k in pairs], timeout_minutes, + matrix_job=dragon_matrix_job(first[0])) + + def in_slow_band(window, k): + return window in (0, 1) and k in DRAGON_SLOW_BAND + + # Routing (ruling s46): the slow band runs window 000 at 180 minutes alone and window 001 at + # 60 minutes in fours; every other pair runs at 30 minutes in eights, contiguous in + # (window, K) order within its matrix job. The chunks form first; then each slow pair + # (ruling s54 (a)) leaves its chunk, so the other group names stay, and runs alone at + # 180 minutes. A split pair leaves its chunk the same way and runs one group per piece; + # the pieces run in two matrix jobs of their own (256 jobs over both platforms): offsets + # 0..7 in dragon-e and 8..15 in dragon-f. The unproved pieces (DRAGON_UNPROVED_PIECES) run no group. + def add_fixed_k_chunks(pairs, batch_size, timeout_minutes): + for first in range(0, len(pairs), batch_size): + chunk = [pair for pair in pairs[first:first + batch_size] + if pair not in DRAGON_SLOW_PAIRS and pair not in DRAGON_SPLIT_PAIRS] + if chunk: + add_fixed_k(chunk, timeout_minutes) + + for window, batch_size, timeout_minutes in ((0, 1, 180), (1, 4, 60)): + add_fixed_k_chunks([(window, k) for k in DRAGON_SLOW_BAND], batch_size, timeout_minutes) + for job in ("dragon-a", "dragon-b", "dragon-c", "dragon-d"): + add_fixed_k_chunks([(window, k) for window, k in dragon_fixed_k_pairs() + if dragon_matrix_job(window) == job and not in_slow_band(window, k)], 8, 30) + for pair in DRAGON_SLOW_PAIRS: + add_fixed_k([pair], 180) + # A piece split on the mantissa runs one group per sub-piece (name suffix -mN) in place of its group. + for (window, k), offsets in DRAGON_SPLIT_PAIRS.items(): + unproved = DRAGON_UNPROVED_PIECES.get((window, k), ()) + for offset in (offset for offset in offsets if offset not in unproved): + piece_name = f"dragon-shortest-scaling-fixed-k-{dragon_fixed_k_name(window, k)}-e{offset:02d}" + for top in range(DRAGON_MANTISSA_SPLITS.get((window, k, offset), 0)) or (None,): + add(piece_name if top is None else f"{piece_name}-m{top}", "contract", + [dragon_fixed_k_piece_harness(window, k, offset, top)], 180, + matrix_job="dragon-" + "ef"[offset >= 8]) + + # The index 51 proof (group 29) does not finish on the whole domain, so grisu.rs proves + # it in two pieces that split on the top bit of v and together cover every input. + for first in range(0, 80, 10): + last = first + 9 + harnesses = [f"{GRISU}::check_multiply_shortest_{group:02d}{piece}" + for group in range(first, last + 1) + for piece in (("_v0", "_v1") if group == 29 else ("",))] + if first == 0: + harnesses.append(f"{GRISU}::check_decimal_termination") + harnesses.extend(f"{GRISU}::check_fractional_encoding_{power:02d}" + for power in range(16)) + add(f"grisu-multiplication-contract-{first:02d}-{last:02d}", "contract", harnesses, 7, "cvc5") + + # Each cached multiplication proof needs more than the 7-minute arithmetic budget, + # so these native SAT proofs get their own 60-minute groups of four. + cached_lengths = ((16, 1), (32, 2), (64, 3), (128, 5), (256, 10)) if pointer_width == 64 else ( + (16, 2), (32, 3), (64, 5), (128, 10), (256, 19)) + cached_cases = [ + f"{DRAGON}::cached_mul_pow5_{power}::size_{size:02d}" + for power, digits in cached_lengths + for size in range(limb_count - digits + 1) + ] + for first in range(0, len(cached_cases), 4): + last = min(first + 3, len(cached_cases) - 1) + add(f"cached-multiplication-equivalence-{first:03d}-{last:03d}", "equivalence", + cached_cases[first:last + 1], 60, matrix_job="long") + + for first in range(0, 80, 4): + last = min(first + 3, 79) + add(f"grisu-shortest-scaling-contract-{first:02d}-{last:02d}", "contract", [ + f"{GRISU}::check_scale_shortest_{group:02d}" for group in range(first, last + 1) + ], 60) + + shift_cases = [ + f"{DRAGON}::shift_width_{total:02d}::size_{size:02d}" + for total in range(limb_count + 1) + for size in range(total + 1) + if total - size < limb_count + ] + for first in range(0, len(shift_cases), 32): + last = min(first + 31, len(shift_cases) - 1) + add(f"bigint-shift-equivalence-{first:03d}-{last:03d}", "equivalence", + shift_cases[first:last + 1], 7) + + for first in range(0, limb_count, 8): + last = min(first + 7, limb_count - 1) + add(f"small-multiplication-equivalence-{first:02d}-{last:02d}", "equivalence", [ + f"{DRAGON}::check_mul_small_model_agrees_{size:02d}" + for size in range(first, last + 1) + ], 30) + + for first in range(0, 65, 8): + last = min(first + 7, 64) + add(f"estimator-equivalence-{first:02d}-{last:02d}", "equivalence", [ + f"num::imp::flt2dec::estimator_verify::check_estimator_model_agrees_{bits:02d}" + for bits in range(first, last + 1) + ], 30) + + for first in range(0, 10, 8): + last = min(first + 7, 9) + add(f"decimal-division-equivalence-{first:02d}-{last:02d}", "equivalence", [ + f"{GRISU}::check_decimal_division_model_agrees_{exponent:02d}" + for exponent in range(first, last + 1) + ], 30) + return groups + + +def remaining_harnesses(inventory): + if inventory.get("file-version") != "0.1": + raise ValueError("Expected kani-list.json file-version 0.1") + listed = [ + harness + for section in ("standard-harnesses", "contract-harnesses") + for harnesses in inventory[section].values() + for harness in harnesses + ] + # The unproved pieces (DRAGON_UNPROVED_PIECES) run in no group and not in the general suite. + dedicated = {harness for group in proof_groups() for harness in group["harnesses"]}.union( + dragon_unproved_harnesses()) + missing = dedicated.difference(listed) + if missing: + raise ValueError("Dedicated flt2dec harnesses missing from kani list: " + ", ".join(sorted(missing))) + # Keep unrecognized harnesses, including future flt2dec proofs, in the general suite. + # Preserve the original list order and duplicates outside the dedicated catalog. + return [harness for harness in listed if harness not in dedicated] + + +def proof_matrix(selected_groups=None, pointer_width=None, platform="both", grisu_probe="none", + matrix_job=None): + groups = proof_groups(pointer_width) + if grisu_probe != "none" and grisu_probe not in GRISU_PROBES: + raise ValueError("Unknown Grisu probe: " + str(grisu_probe)) + if matrix_job not in (None, *MATRIX_JOBS): + raise ValueError("Unknown matrix job: " + str(matrix_job)) + if grisu_probe != "none" and selected_groups is None: + raise ValueError("Grisu probes require explicitly selected manual proof groups") + if platform != "both" and platform not in OPERATING_SYSTEMS: + raise ValueError("Unknown proof platform: " + str(platform)) + if platform != "both" and selected_groups is None: + raise ValueError("Platform filtering requires explicitly selected manual proof groups") + if selected_groups is not None: + if not isinstance(selected_groups, list) or not selected_groups: + raise ValueError("Select a nonempty JSON array of proof group names") + if not all(isinstance(name, str) for name in selected_groups): + raise ValueError("Every selected proof group name must be a string") + selected = set(selected_groups) + if len(selected) != len(selected_groups): + raise ValueError("Selected proof group names must be distinct") + # `matrix-job:dragon-a` selects every catalog group of that matrix job. + jobs = {name[len("matrix-job:"):] for name in selected if name.startswith("matrix-job:")} + if not jobs <= set(MATRIX_JOBS): + raise ValueError("Unknown matrix jobs: " + ", ".join(sorted(jobs - set(MATRIX_JOBS)))) + selected = {name for name in selected if not name.startswith("matrix-job:")} | { + group["name"] for group in groups if group["matrix_job"] in jobs} + known = {group["name"] for group in groups} + unknown = selected - known + if unknown: + raise ValueError("Unknown proof groups: " + ", ".join(sorted(unknown))) + groups = [dict(group, name="manual-" + group["name"], catalog_name=group["name"]) + for group in groups if group["name"] in selected] + if grisu_probe != "none": + for group in groups: + if not (group["catalog_name"].startswith("grisu-shortest-") + and group["kind"].startswith("generator-")): + raise ValueError("Grisu probes require shortest generator groups") + group.update(name=f"probe-{grisu_probe}-" + group["catalog_name"], + probe_property=GRISU_PROBES[grisu_probe], timeout_minutes=3) + # A manual selection splits between the matrix jobs; any part can be empty. + if matrix_job is not None: + groups = [group for group in groups if group["matrix_job"] == matrix_job] + return {"os": OPERATING_SYSTEMS if platform == "both" else [platform], "group": groups} + + +def group_harnesses(group, backend=None): + if backend not in (None, "native", "cvc5"): + raise ValueError("Expected the native or cvc5 proof backend") + overrides = group.get("harness_backends", {}) + return [harness for harness in group["harnesses"] + if backend is None or overrides.get(harness, group["backend"]) == backend] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--matrix", action="store_true") + mode.add_argument("--group", choices=[group["name"] for group in proof_groups()]) + mode.add_argument("--remaining", metavar="KANI_LIST_JSON") + parser.add_argument("--matrix-groups", metavar="JSON", + help="Select manual proof groups with distinct check names") + parser.add_argument("--matrix-platform", choices=["both", *OPERATING_SYSTEMS], default="both", + help="Select a platform only for explicitly selected manual groups") + parser.add_argument("--backend", choices=["native", "cvc5"], + help="Select one backend's harnesses within a proof group") + parser.add_argument("--matrix-grisu-probe", choices=["none", *GRISU_PROBES], default="none", + help="Diagnose one loop property in selected manual Grisu groups") + parser.add_argument("--matrix-job", choices=list(MATRIX_JOBS), + help="Keep only the groups of one CI matrix job") + args = parser.parse_args() + if args.matrix_groups is not None and not args.matrix: + parser.error("--matrix-groups requires --matrix") + if args.matrix_platform != "both" and not args.matrix: + parser.error("--matrix-platform requires --matrix") + if args.backend is not None and not args.group: + parser.error("--backend requires --group") + if args.matrix_grisu_probe != "none" and not args.matrix: + parser.error("--matrix-grisu-probe requires --matrix") + if args.matrix_job is not None and not args.matrix: + parser.error("--matrix-job requires --matrix") + if args.matrix: + try: + selected = json.loads(args.matrix_groups) if args.matrix_groups is not None else None + matrix = proof_matrix(selected, platform=args.matrix_platform, + grisu_probe=args.matrix_grisu_probe, matrix_job=args.matrix_job) + except (ValueError, TypeError) as error: + parser.error(str(error)) + print(json.dumps(matrix, separators=(",", ":"))) + elif args.group: + group = next(group for group in proof_groups() if group["name"] == args.group) + print("\n".join(group_harnesses(group, args.backend))) + else: + try: + with open(args.remaining, encoding="utf-8") as source: + remaining = remaining_harnesses(json.load(source)) + except (OSError, ValueError, KeyError, TypeError) as error: + parser.error(str(error)) + if remaining: + print("\n".join(remaining)) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/kani-std-analysis/log_parser.py b/scripts/kani-std-analysis/log_parser.py index d020053a4973e..8269dd36e22ea 100755 --- a/scripts/kani-std-analysis/log_parser.py +++ b/scripts/kani-std-analysis/log_parser.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Parse log file of multi-threaded Kani run (terse output) into JSON. +Parse log file of a Kani run (terse output) into JSON. Given a run of Kani on the standard library with `--jobs= --output-format=terse` (and, typically, `autoharness`) enabled this produces a machine-processable JSON result via @@ -251,6 +251,7 @@ def read_kani_list(kani_list_file, scanner_data): contract_harnesses[h] = { 'file_name': file_name, 'crate': crate_from_file(file_name, known_crates), + 'function': h, 'target_safeness': None, 'public_target': None } @@ -565,7 +566,7 @@ def flush(thread_id): def parse_log_lines( lines, contract_harnesses, standard_harnesses, scanner_data): - """Parse (terse) output from multi-threaded Kani run while considering list + """Parse (terse) output from a Kani run while considering list and scanner data.""" # Regular expressions for matching patterns start_work_autoharness_contract_pattern = re.compile( @@ -585,6 +586,7 @@ def parse_log_lines( active_threads = {} # thread_id -> list of result lines all_results = [] thread_id = None + autoharness_info = {} i = 0 while i < len(lines): @@ -594,6 +596,13 @@ def parse_log_lines( line = lines[i].rstrip() i += 1 + # A single worker omits thread labels, including the result marker. + # Associate its output with thread 0 from the start of the harness. + if line.startswith(('Checking harness ', + 'Autoharness: Checking function ')): + line = f'Thread 0: {line}' + thread_id = 0 + # Check if a thread is starting work if start_match := start_work_autoharness_contract_pattern.search(line): init_entry( diff --git a/scripts/kani-std-analysis/overflow-result.rs b/scripts/kani-std-analysis/overflow-result.rs new file mode 100644 index 0000000000000..2981e211a467e --- /dev/null +++ b/scripts/kani-std-analysis/overflow-result.rs @@ -0,0 +1,49 @@ +// Check both fields of the arithmetic tuples affected by CBMC issue #9145. +// Widened arithmetic supplies an independent result and overflow predicate. +#[kani::proof] +fn signed_add_subtract() { + let lhs: i16 = kani::any(); + let rhs: i16 = kani::any(); + let sum = i32::from(lhs) + i32::from(rhs); + let difference = i32::from(lhs) - i32::from(rhs); + let bounds = i32::from(i16::MIN)..=i32::from(i16::MAX); + assert_eq!( + lhs.overflowing_add(rhs), + (sum as i16, !bounds.contains(&sum)) + ); + assert_eq!( + lhs.overflowing_sub(rhs), + (difference as i16, !bounds.contains(&difference)) + ); + kani::cover( + !bounds.contains(&sum), + "addition overflow remains reachable", + ); + kani::cover( + !bounds.contains(&difference), + "subtraction overflow remains reachable", + ); +} + +#[kani::proof] +fn signed_multiply() { + let lhs: i8 = kani::any(); + let rhs: i8 = kani::any(); + let product = i16::from(lhs) * i16::from(rhs); + let overflow = !(i16::from(i8::MIN)..=i16::from(i8::MAX)).contains(&product); + assert_eq!(lhs.overflowing_mul(rhs), (product as i8, overflow)); + kani::cover(overflow, "signed multiplication overflow remains reachable"); +} + +#[kani::proof] +fn unsigned_multiply() { + let lhs: u8 = kani::any(); + let rhs: u8 = kani::any(); + let product = u16::from(lhs) * u16::from(rhs); + let overflow = product > u16::from(u8::MAX); + assert_eq!(lhs.overflowing_mul(rhs), (product as u8, overflow)); + kani::cover( + overflow, + "unsigned multiplication overflow remains reachable", + ); +} diff --git a/scripts/kani-std-analysis/test_cbmc_loop_assigns.cpp b/scripts/kani-std-analysis/test_cbmc_loop_assigns.cpp new file mode 100644 index 0000000000000..7fc413300f51c --- /dev/null +++ b/scripts/kani-std-analysis/test_cbmc_loop_assigns.cpp @@ -0,0 +1,99 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +std::string quote(const std::string &text) +{ + std::string result = "'"; + for(char c : text) + result += c == '\'' ? "'\\''" : std::string(1, c); + return result + "'"; +} + +std::pair run(const std::string &command, const fs::path &log) +{ + int status = std::system((command + " > " + quote(log.string()) + " 2>&1").c_str()); + std::ifstream input(log); + std::ostringstream output; + output << input.rdbuf(); + int exit_code = status >= 0 && WIFEXITED(status) ? WEXITSTATUS(status) : -1; + return {exit_code, output.str()}; +} + +int main(int argc, char **argv) +{ + if(argc != 4) + return 1; + const fs::path tools = argv[1]; + const fs::path scratch = argv[2]; + const std::string compiler = quote(argv[3]); + const std::array, 5> cases = {{ + {"explicit", "__CPROVER_assigns(i, value)"}, + {"inferred", ""}, + {"empty", "__CPROVER_assigns()"}, + {"mixed", "__CPROVER_assigns(i, value)"}, + {"omitted", "__CPROVER_assigns(i)"}, + }}; + for(const auto &test : cases) + { + const auto &name = test.first; + const fs::path source = scratch / (name + ".c"); + const fs::path model = scratch / (name + ".gb"); + const fs::path checked = scratch / (name + ".checked.gb"); + const fs::path log = scratch / (name + ".log"); + // C is an input fixture for the instrumenter. The test driver and all + // verification-result checks are C++, matching the patched implementation. + std::ofstream fixture(source); + fixture + << "void write_next(unsigned *value) { ++*value; }\n" + "int main(void) { unsigned i = 0, value = 0;\n" + "for(; i < 3; ++i)\n" + << test.second + << "\n__CPROVER_loop_invariant(i <= 3 && value == i)\n{ write_next(&value); }\n" + "__CPROVER_assert(value == 3, \"loop value\");\n"; + if(name == "mixed") + fixture + << "for(i = 0; i < 3; ++i)\n" + "__CPROVER_loop_invariant(i <= 3 && value == i + 3)\n" + "{ write_next(&value); }\n" + "__CPROVER_assert(value == 6, \"inferred second loop value\");\n"; + fixture << "return 0; }\n"; + fixture.close(); + const std::array preparation = {{ + compiler + " " + quote(source.string()) + " -o " + quote(model.string()), + quote((tools / "goto-instrument").string()) + + " --dfcc main --apply-loop-contracts --loop-contracts-no-unwind " + + quote(model.string()) + " " + quote(checked.string()), + }}; + for(const auto &command : preparation) + { + const auto result = run(command, log); + if(result.first != 0) + { + std::cerr << result.second; + return 1; + } + } + const auto result = run( + quote((tools / "cbmc").string()) + " " + quote(checked.string()) + + " --unwind 5 --unwinding-assertions", log); + const bool omitted = name == "omitted"; + if(result.first != (omitted ? 10 : 0) || + (omitted && !std::regex_search(result.second, std::regex("assignable[^\n]*FAILURE")))) + { + std::cerr << name << ": unexpected result\n" << result.second; + return 1; + } + std::cout << "loop assigns " << name << ": expected verification result confirmed\n"; + } + return 0; +} diff --git a/scripts/kani-std-analysis/test_cbmc_loop_assigns.sh b/scripts/kani-std-analysis/test_cbmc_loop_assigns.sh new file mode 100644 index 0000000000000..24606b016879a --- /dev/null +++ b/scripts/kani-std-analysis/test_cbmc_loop_assigns.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +tool_dir=$(cd -- "${1:?path to patched CBMC bin directory}" && pwd) +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +c++ -std=c++17 -O1 "$script_dir/test_cbmc_loop_assigns.cpp" -o "$scratch/loop-assigns-tests" +"$scratch/loop-assigns-tests" "$tool_dir" "$scratch" "$tool_dir/goto-cc" diff --git a/scripts/kani-std-analysis/test_cbmc_tap_retry.sh b/scripts/kani-std-analysis/test_cbmc_tap_retry.sh new file mode 100644 index 0000000000000..4b48c62f0765f --- /dev/null +++ b/scripts/kani-std-analysis/test_cbmc_tap_retry.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -eo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$repo_root" + +check_retry() ( + local failures="$1" expected_attempts="$2" expected_status="$3" + local calls=0 waits=0 status=0 + source scripts/run-kani.sh --run list + brew() { + [[ "$#" == 2 && "$1" == tap && "$2" == diffblue/cbmc ]] || return 2 + calls=$((calls + 1)) + [[ "$calls" -gt "$failures" ]] + } + sleep() { + [[ "$1" == 10 ]] || return 2 + waits=$((waits + 1)) + } + prepare_cbmc_tap >/dev/null 2>&1 || status=$? + [[ "$status" == "$expected_status" ]] + [[ "$calls" == "$expected_attempts" ]] + [[ "$waits" == "$((expected_attempts - 1))" ]] +) + +check_retry 0 1 0 +check_retry 1 2 0 +check_retry 3 3 1 + +check_installer_retry() ( + local failures="$1" expected_attempts="$2" expected_status="$3" + local calls=0 status=0 scratch + scratch=$(mktemp -d) + trap 'rm -rf "$scratch"' EXIT + source scripts/run-kani.sh --run list + installer() { + [[ "$#" == 1 && "$1" == 'argument with spaces' ]] || return 2 + [[ -f "$CURL_HOME/.curlrc" ]] || return 3 + calls=$((calls + 1)) + printf '%s\n' "$CURL_HOME" >> "$scratch/calls" + [[ "$calls" -gt "$failures" ]] || return 73 + } + sleep() { + [[ "$1" == 10 ]] || return 2 + printf 'wait\n' >> "$scratch/waits" + } + : > "$scratch/waits" + GITHUB_ACTIONS=true install_kani_dependencies installer 'argument with spaces' \ + >/dev/null 2>&1 || status=$? + [[ "$status" == "$expected_status" ]] + [[ "$(wc -l < "$scratch/calls")" -eq "$expected_attempts" ]] + [[ "$(wc -l < "$scratch/waits")" -eq "$((expected_attempts - 1))" ]] + [[ ! -e "$(head -n 1 "$scratch/calls")" ]] +) + +check_installer_retry 0 1 0 +check_installer_retry 1 2 0 +check_installer_retry 3 3 73 +echo "CBMC tap and dependency retry tests passed." diff --git a/scripts/kani-std-analysis/test_cvc5_flat_smt.sh b/scripts/kani-std-analysis/test_cvc5_flat_smt.sh new file mode 100644 index 0000000000000..75bc501e7ebc8 --- /dev/null +++ b/scripts/kani-std-analysis/test_cvc5_flat_smt.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +cat > "$scratch/solver" <<'SH' +#!/bin/bash +printf '%s\n' "$@" +exit "${SOLVER_EXIT:-0}" +SH +chmod +x "$scratch/solver" +actual=$(KANI_CVC5_BINARY="$scratch/solver" bash "$script_dir/cvc5-flat-smt.sh" 'query with spaces.smt2') +expected=$(printf '%s\n' --lang=smt2 --force-logic=ALL --solve-bv-as-int=sum 'query with spaces.smt2') +[[ "$actual" == "$expected" ]] +status=0 +KANI_CVC5_BINARY="$scratch/solver" SOLVER_EXIT=71 \ + bash "$script_dir/cvc5-flat-smt.sh" >/dev/null || status=$? +[[ "$status" == 71 ]] + +if [[ -n "${KANI_CVC5_BINARY:-}" ]]; then + cat > "$scratch/query.smt2" <<'SMT' +(set-logic QF_AUFBV) +(declare-fun values () (Array (_ BitVec 64) (_ BitVec 8))) +(assert (= values ((as const (Array (_ BitVec 64) (_ BitVec 8))) #x00))) +(declare-fun x () (_ BitVec 64)) +(assert (= x #xffffffffffffffff)) +(assert (= (bvmul x x) #x0000000000000001)) +(check-sat) +(assert (= (bvmul x x) #x0000000000000002)) +(check-sat) +SMT + actual=$(bash "$script_dir/cvc5-flat-smt.sh" --incremental "$scratch/query.smt2") + [[ "$actual" == "$(printf 'sat\nunsat')" ]] +fi +echo 'CVC5 adapter checks passed' diff --git a/scripts/kani-std-analysis/test_flt2dec_harnesses.py b/scripts/kani-std-analysis/test_flt2dec_harnesses.py new file mode 100644 index 0000000000000..194261c092a41 --- /dev/null +++ b/scripts/kani-std-analysis/test_flt2dec_harnesses.py @@ -0,0 +1,473 @@ +from pathlib import Path +import re +import unittest + +from flt2dec_harnesses import ( + DRAGON, DRAGON_MANTISSA_SPLITS, DRAGON_SLOW_BAND, DRAGON_SLOW_PAIRS, DRAGON_SPLIT_PAIRS, DRAGON_UNPROVED_PIECES, MATRIX_JOBS, + OPERATING_SYSTEMS, dragon_fixed_k_harness, dragon_fixed_k_piece_harness, dragon_unproved_harnesses, + dragon_fixed_k_pairs, group_harnesses, proof_groups, proof_matrix, remaining_harnesses, +) + + +class Flt2decHarnessesTests(unittest.TestCase): + def test_default_matrix_keeps_every_group_on_both_platforms(self): + for width in (16, 32, 64): + with self.subTest(pointer_width=width): + matrix = proof_matrix(pointer_width=width) + self.assertEqual(matrix["os"], OPERATING_SYSTEMS) + self.assertEqual(matrix["group"], proof_groups(width)) + + def test_manual_matrix_keeps_group_contents_and_distinct_check_names(self): + names = ["grisu-shortest-f64-02-03", "dragon-exact-loop-frame", + "grisu-multiplication-contract-20-29", "cached-multiplication-equivalence-000-003"] + matrix = proof_matrix(names) + self.assertEqual(matrix["os"], OPERATING_SYSTEMS) + self.assertEqual({group["catalog_name"] for group in matrix["group"]}, set(names)) + original = {group["name"]: group for group in proof_groups()} + for group in matrix["group"]: + self.assertEqual(group["name"], "manual-" + group["catalog_name"]) + for field in ("harnesses", "kind", "backend", "timeout_minutes", "harness_backends", "matrix_job"): + self.assertEqual(group.get(field), original[group["catalog_name"]].get(field)) + + def test_invalid_manual_matrix_selection_fails(self): + for selection in ([], {}, "dragon-exact-loop-frame", [0], ["unknown"], + ["dragon-exact-loop-frame", "dragon-exact-loop-frame"]): + with self.subTest(selection=selection), self.assertRaises(ValueError): + proof_matrix(selection) + + def test_platform_selection_requires_explicit_manual_groups(self): + names = ["dragon-exact-loop-frame"] + for platform in OPERATING_SYSTEMS: + with self.subTest(platform=platform): + matrix = proof_matrix(names, platform=platform) + self.assertEqual(matrix["os"], [platform]) + self.assertEqual(matrix["group"], proof_matrix(names)["group"]) + with self.assertRaises(ValueError): + proof_matrix(platform=platform) + with self.assertRaises(ValueError): + proof_matrix(names, platform="windows-latest") + + def test_loop_probes_have_distinct_names_and_keep_the_selected_harnesses(self): + name = "grisu-shortest-f64-02-03" + original = next(group for group in self.groups if group["name"] == name) + for probe, function, phase in (("entry", "generate_shortest", "base"), + ("step", "generate_shortest", "step"), + ("integral-step", "generate_integral_digits", "step")): + matrix = proof_matrix([name], grisu_probe=probe) + group, = matrix["group"] + with self.subTest(probe=probe): + self.assertEqual(group["name"], f"probe-{probe}-{name}") + self.assertEqual(group["catalog_name"], name) + self.assertEqual(group["harnesses"], original["harnesses"]) + self.assertEqual(group["timeout_minutes"], 3) + self.assertEqual(group["probe_property"], + f"num::imp::flt2dec::strategy::grisu::{function}." + f"loop_invariant_{phase}.1") + self.assertEqual(matrix["os"], OPERATING_SYSTEMS) + self.assertTrue(all("probe_property" not in group for group in proof_matrix()["group"])) + + def test_loop_probes_reject_automatic_and_unrelated_groups(self): + for probe in ("entry", "step", "integral-step", "unknown"): + with self.subTest(probe=probe), self.assertRaises(ValueError): + proof_matrix(grisu_probe=probe) + for name in ("dragon-exact-loop-frame", "grisu-shortest-scaling-contract-00-03"): + with self.subTest(group=name), self.assertRaises(ValueError): + proof_matrix([name], grisu_probe="entry") + + def setUp(self): + self.groups = proof_groups() + self.dedicated = [harness for group in self.groups for harness in group["harnesses"]] + self.inventory = { + "file-version": "0.1", + "standard-harnesses": {"generators": self.dedicated[:144]}, + "contract-harnesses": {"other": self.dedicated[144:] + dragon_unproved_harnesses()}, + } + + def test_complete_disjoint_batches_on_both_platforms(self): + self.assertEqual(OPERATING_SYSTEMS, ["ubuntu-latest", "macos-latest"]) + self.assertEqual(len(self.groups), 707) + self.assertEqual(len({group["name"] for group in self.groups}), len(self.groups)) + self.assertEqual(sum(len(group["harnesses"]) for group in self.groups), 3696) + self.assertEqual(len(set(self.dedicated)), len(self.dedicated)) + for part in MATRIX_JOBS: + parts = proof_matrix(matrix_job=part)["group"] + self.assertLessEqual(len(parts) * len(OPERATING_SYSTEMS), 256) + for group in self.groups: + minutes = group["timeout_minutes"] + with self.subTest(group=group["name"]): + self.assertTrue(group["harnesses"]) + self.assertIn(minutes, (7, 30, 60, 120, 180)) + self.assertLessEqual(minutes * len(group["harnesses"]), 240) + + def test_shift_models_cover_all_safe_whole_limb_offsets(self): + for width, limbs in ((16, 40), (32, 40), (64, 20)): + groups = proof_groups(pointer_width=width) + cases = [harness for group in groups + if group["name"].startswith("bigint-shift-equivalence-") + for harness in group["harnesses"]] + positions = [(int(name.split("::")[-2].rsplit("_", 1)[1]), + int(name.rsplit("_", 1)[1])) for name in cases] + routed = {(size, total - size) for total, size in positions} + expected = {(size, digits) for size in range(limbs + 1) + for digits in range(limbs) if size + digits <= limbs} + with self.subTest(pointer_width=width): + self.assertEqual(routed, expected) + self.assertEqual(len(cases), len(expected)) + + def test_ci_matrix_fits_the_job_limit(self): + # The matrix job runs on a 64-bit host, so only that catalog is expanded. + # Each flt2dec matrix job holds at most 256 jobs. + for part in MATRIX_JOBS: + with self.subTest(matrix_job=part): + matrix = proof_matrix(pointer_width=64, matrix_job=part) + self.assertTrue(matrix["group"]) + self.assertLessEqual(len(matrix["group"]) * len(matrix["os"]), 256) + + def test_matrix_jobs_split_every_catalog(self): + for width in (16, 32, 64): + groups = proof_groups(width) + parts = {part: proof_matrix(pointer_width=width, matrix_job=part)["group"] + for part in MATRIX_JOBS} + with self.subTest(pointer_width=width): + self.assertTrue(all(group["matrix_job"] in parts for group in groups)) + self.assertTrue(all(group["matrix_job"] == part + for part, selected in parts.items() for group in selected)) + self.assertEqual(sorted(group["name"] for selected in parts.values() for group in selected), + sorted(group["name"] for group in groups)) + + def test_generator_and_cached_groups_run_in_the_long_job(self): + for group in self.groups: + in_long_job = (group["kind"].startswith("generator-") + or group["name"].startswith("cached-multiplication-equivalence-")) + in_dragon_job = group["name"].startswith(("dragon-shortest-scaling-fixed-k-", + "dragon-shortest-scaling-contract-")) + with self.subTest(group=group["name"]): + if in_dragon_job: + self.assertIn(group["matrix_job"], MATRIX_JOBS[2:]) + else: + self.assertEqual(group["matrix_job"], "long" if in_long_job else "core") + + def test_manual_selection_splits_between_matrix_jobs(self): + names = ["dragon-exact-loop-frame", "grisu-exact-f32"] + for part, name in (("core", "dragon-exact-loop-frame"), ("long", "grisu-exact-f32")): + with self.subTest(matrix_job=part): + matrix = proof_matrix(names, matrix_job=part) + self.assertEqual([group["catalog_name"] for group in matrix["group"]], [name]) + self.assertEqual(proof_matrix(["dragon-exact-loop-frame"], matrix_job="long")["group"], []) + for part in ("all", ""): + with self.subTest(matrix_job=part), self.assertRaises(ValueError): + proof_matrix(matrix_job=part) + + def test_matrix_job_selection_expands_to_every_group_of_the_job(self): + matrix = proof_matrix(["matrix-job:dragon-a", "matrix-job:dragon-b"]) + self.assertEqual(matrix["os"], OPERATING_SYSTEMS) + self.assertEqual(sorted(group["catalog_name"] for group in matrix["group"]), + sorted(group["name"] for group in self.groups + if group["matrix_job"] in ("dragon-a", "dragon-b"))) + self.assertEqual(proof_matrix(["matrix-job:dragon-a"], matrix_job="core")["group"], []) + self.assertEqual(len(proof_matrix(["matrix-job:dragon-a"], matrix_job="dragon-a")["group"]), + len(proof_matrix(matrix_job="dragon-a")["group"])) + with self.assertRaises(ValueError): + proof_matrix(["matrix-job:unknown"]) + + def test_dragon_fixed_k_groups_cover_every_pair_once_with_the_band_routing(self): + pairs = dragon_fixed_k_pairs() + self.assertEqual(len(pairs), 2748) + self.assertEqual((pairs[0], pairs[21], pairs[22], pairs[-1]), + ((0, -324), (0, -303), (1, -319), (127, 308))) + self.assertEqual(sum(1 for _, k in pairs if k >= 0), 1342) + groups = [group for group in self.groups + if group["name"].startswith("dragon-shortest-scaling-fixed-k-")] + routed = [(group, int(window), (-1 if sign == "k" else 1) * int(k), piece) + for group in groups for harness in group["harnesses"] + for window, sign, k, piece in re.findall( + r"::w(\d{3})::([kp])(\d{3})(?:::e(\d{2}(?:::m\d)?))?::check_scale_shortest$", harness)] + self.assertEqual(len(routed), sum(len(group["harnesses"]) for group in groups)) + # A split pair routes once per piece that CI runs (every piece but the unproved ones; a + # piece split on the mantissa once per sub-piece), so every pair routes at least once and + # no pair, piece or sub-piece routes twice. + self.assertEqual(sorted({(window, k) for _, window, k, _ in routed}), pairs) + self.assertEqual(len({(window, k, piece) for _, window, k, piece in routed}), len(routed)) + self.assertEqual(sum(piece == "" for _, _, _, piece in routed) + len(DRAGON_SPLIT_PAIRS), len(pairs)) + self.assertEqual([harness for group in groups for harness in group["harnesses"] + if harness == dragon_fixed_k_harness(0, -324)], + [f"{DRAGON}::dragon_scale_fixed_k::w000::k324::check_scale_shortest"]) + for group, window, k, piece in routed: + in_band = window in (0, 1) and k in DRAGON_SLOW_BAND + slow = (window, k) in DRAGON_SLOW_PAIRS or (window, k) in DRAGON_SPLIT_PAIRS + minutes, batch_size = ((180, 1) if slow or (in_band and window == 0) + else (60, 4) if in_band else (30, 8)) + with self.subTest(group=group["name"], window=window, k=k): + self.assertEqual(group["matrix_job"], "dragon-" + "ef"[int(piece[:2]) >= 8] if piece else + "dragon-" + "abcd"[(window >= 34) + (window >= 68) + (window >= 98)]) + self.assertEqual(group["timeout_minutes"], minutes) + self.assertLessEqual(len(group["harnesses"]), batch_size) + self.assertEqual(len(DRAGON_SLOW_PAIRS), 16) + self.assertEqual(sum(1 for group in groups if group["timeout_minutes"] == 180), 195) + self.assertEqual(sum(1 for group in groups if group["timeout_minutes"] == 60), 3) + self.assertEqual(tuple(len(proof_matrix(matrix_job=job)["group"]) + for job in MATRIX_JOBS[2:]), (108, 100, 81, 81, 69, 102)) + + def test_all_float_partitions_and_equivalence_cases_remain(self): + self.assertIn( + "num::imp::flt2dec::strategy::dragon::dragon_verify::check_exact_digit_loop_frame", + self.dedicated, + ) + generators = [group for group in self.groups if group["kind"].startswith("generator-")] + self.assertEqual(sum(len(group["harnesses"]) for group in generators), 179) + for family in ("dragon-exact", "dragon-shortest", "grisu-exact", "grisu-shortest"): + targets = [harness for group in generators if group["name"].startswith(family + "-") + for harness in group["harnesses"]] + # A slice harness adds an `sN` module below its partition module. + partitions = {next(part for part in harness.split("::") if part.startswith(("f32_", "f64_"))) + for harness in targets} + self.assertEqual(partitions, {f"f32_{i:02d}" for i in range(4)} | + {f"f64_{i:02d}" for i in range(32)}) + cases = [harness for group in self.groups if group["name"].startswith("estimator-") + for harness in group["harnesses"]] + self.assertEqual({int(harness.rsplit("_", 1)[1]) for harness in cases}, set(range(65))) + multiplication = [harness for group in self.groups + if group["name"].startswith("small-multiplication-") + for harness in group["harnesses"]] + self.assertEqual({int(harness.rsplit("_", 1)[1]) for harness in multiplication}, set(range(20))) + + for width, limb_count in ((16, 40), (32, 40), (64, 20)): + with self.subTest(pointer_width=width): + cases = [harness for group in proof_groups(width) + if group["name"].startswith("small-multiplication-") + for harness in group["harnesses"]] + self.assertEqual({int(harness.rsplit("_", 1)[1]) for harness in cases}, + set(range(limb_count))) + division = [harness for group in self.groups + if group["name"].startswith("decimal-division-") + for harness in group["harnesses"]] + self.assertEqual({int(harness.rsplit("_", 1)[1]) for harness in division}, set(range(10))) + scaling = [harness for group in self.groups + if group["name"].startswith("grisu-shortest-scaling-contract-") + for harness in group["harnesses"]] + self.assertEqual({int(harness.rsplit("_", 1)[1]) for harness in scaling}, set(range(80))) + + def test_arithmetic_backend_covers_every_cached_power(self): + arithmetic = [harness for group in self.groups + for harness in group_harnesses(group, "cvc5")] + prefix = "num::imp::flt2dec::strategy::grisu::grisu_verify::" + expected = {f"{prefix}check_multiply_shortest_{index:02d}" + for index in range(80) if index != 29} + expected.update(f"{prefix}check_multiply_shortest_29_{piece}" for piece in ("v0", "v1")) + expected.add(f"{prefix}check_decimal_termination") + expected.add(f"{prefix}check_advance_fraction_threshold") + expected.update(f"{prefix}check_fractional_encoding_{power:02d}" for power in range(16)) + self.assertEqual(set(arithmetic), expected) + self.assertEqual(len(arithmetic), len(expected)) + self.assertIn(f"{prefix}check_rounded_multiply", self.dedicated) + self.assertIn(f"{prefix}check_normalized_multiply", self.dedicated) + + def test_backend_split_keeps_every_harness_exactly_once(self): + for width in (16, 32, 64): + for group in proof_groups(width): + with self.subTest(pointer_width=width, group=group["name"]): + native = group_harnesses(group, "native") + arithmetic = group_harnesses(group, "cvc5") + self.assertEqual(group_harnesses(group), group["harnesses"]) + self.assertCountEqual(native + arithmetic, group["harnesses"]) + self.assertTrue(set(native).isdisjoint(arithmetic)) + self.assertTrue(set(group.get("harness_backends", {})) <= set(group["harnesses"])) + self.assertTrue(all(harness in native for harness in group["harnesses"] + if "::cached_mul_pow5_" in harness)) + with self.assertRaises(ValueError): + group_harnesses(self.groups[0], "unknown") + + def test_cached_big_multiplication_covers_every_native_coefficient(self): + root = Path(__file__).resolve().parents[2] + source = (root / "library/core/src/num/imp/flt2dec/strategy/dragon.rs").read_text() + start = source.index("pub fn mul_pow10(") + body = source[start:source.index("\n}\n", start)] + called = re.findall(r"\.mul_digits\(&POW5TO(\d+)\)", body) + self.assertEqual(len(called), len(re.findall(r"\.mul_digits\(", body))) + declared = re.findall(r"cached_mul_pow5_(\d+)\s*=>\s*POW5TO(\d+)", source) + self.assertTrue(all(case == coefficient for case, coefficient in declared)) + self.assertEqual({case for case, _ in declared}, set(called)) + routed = {re.search(r"::cached_mul_pow5_(\d+)::", harness).group(1) + for harness in self.dedicated if "::cached_mul_pow5_" in harness} + self.assertEqual(routed, set(called)) + + def test_cached_big_multiplication_covers_every_accepted_size(self): + for width in (16, 32, 64): + digit_bits = 64 if width == 64 else 32 + capacity = 1280 // digit_bits + groups = proof_groups(pointer_width=width) + cases = [harness for group in groups for harness in group["harnesses"] + if "::cached_mul_pow5_" in harness] + routed = {tuple(map(int, re.search(r"::cached_mul_pow5_(\d+)::size_(\d+)$", + harness).groups())) for harness in cases} + expected = {(power, size) for power in (16, 32, 64, 128, 256) + for size in range(capacity + 1 - + ((5 ** power).bit_length() + digit_bits - 1) // digit_bits)} + with self.subTest(pointer_width=width): + self.assertEqual(routed, expected) + self.assertEqual(len(cases), len(expected)) + for group in groups: + self.assertLessEqual(group["timeout_minutes"] * len(group["harnesses"]), 240) + + def test_sliced_grisu_shortest_partitions_cover_every_exponent(self): + prefix = "num::imp::flt2dec::strategy::grisu::grisu_verify::" + partitions = ("f32_00", "f32_01", "f32_02", "f32_03", "f64_00") + slices = [group for group in self.groups + if group["name"].startswith("grisu-shortest-") and "-slice-" in group["name"]] + self.assertEqual(sorted(harness for group in slices for harness in group["harnesses"]), + sorted(f"{prefix}{partition}::s{index}::check_format_shortest_opt" + for partition in partitions for index in range(8))) + for group in slices: + with self.subTest(group=group["name"]): + self.assertEqual(len(group["harnesses"]), 1) + self.assertEqual((group["backend"], group["matrix_job"], group["timeout_minutes"]), + ("native", "long", 120)) + self.assertNotIn("harness_backends", group) + # The slices replace the whole shortest harness; the exact harness stays whole. + for partition in partitions: + with self.subTest(partition=partition): + self.assertNotIn(f"{prefix}{partition}::check_format_shortest_opt", self.dedicated) + self.assertIn(f"{prefix}{partition}::check_format_exact_opt", self.dedicated) + group = next(group for group in self.groups if group["name"] == "grisu-shortest-f64-00-01") + self.assertEqual(group["harnesses"], [f"{prefix}f64_01::check_format_shortest_opt"]) + self.assertEqual(group["backend"], "native") + self.assertNotIn("harness_backends", group) + + def test_cached_big_multiplication_uses_dedicated_native_groups(self): + for width in (16, 32, 64): + for group in proof_groups(pointer_width=width): + cached = [harness for harness in group["harnesses"] if "::cached_mul_pow5_" in harness] + dedicated = group["name"].startswith("cached-multiplication-equivalence-") + with self.subTest(pointer_width=width, group=group["name"]): + self.assertEqual(cached, group["harnesses"] if dedicated else []) + if dedicated: + self.assertNotIn("harness_backends", group) + self.assertEqual( + (group["kind"], group["backend"], group["timeout_minutes"]), + ("equivalence", "native", 60)) + self.assertLessEqual(len(group["harnesses"]), 4) + names = [group["name"] for group in proof_groups(pointer_width=64) + if group["name"].startswith("cached-multiplication-equivalence-")] + self.assertEqual(len(names), 21) + self.assertEqual(names[0], "cached-multiplication-equivalence-000-003") + self.assertEqual(names[-1], "cached-multiplication-equivalence-080-083") + + def test_cached_power_catalog_matches_rust_declarations(self): + root = Path(__file__).resolve().parents[2] + source = (root / "library/core/src/num/imp/flt2dec/strategy/grisu.rs").read_text() + for macro, prefix, expected_index, pieces in ( + ("check_scale_shortest_partition", "check_scale_shortest", lambda group: group, {}), + ("verify_multiply_shortest", "check_multiply_shortest", lambda group: 80 - group, + {29: ("v0", "v1")}), + ): + pattern = (rf"{macro}!\(\s*({prefix}_(\d+)(?:_(v\d))?)\s*," + rf"(?:\s*scaling_cache_index_\d+\s*,)?\s*(\d+)\s*[,)]") + declarations = re.findall(pattern, source) + expected = {(group, piece, expected_index(group)) for group in range(80) + for piece in pieces.get(group, ("",))} + with self.subTest(macro=macro): + self.assertEqual(len(declarations), len(expected)) + self.assertEqual({(int(group), piece, int(index)) + for _, group, piece, index in declarations}, expected) + declared = {name for name, _, _, _ in declarations} + routed = {name.rsplit("::", 1)[1] for name in self.dedicated + if re.fullmatch(prefix + r"_\d+(?:_v\d)?", name.rsplit("::", 1)[1])} + self.assertEqual(declared, routed) + # The two pieces of the index 51 proof must cover every u64 value of v. + split = re.findall(r"verify_multiply_shortest!\(\s*check_multiply_shortest_29_(v\d)\s*," + r"\s*51\s*,\s*([^,]+?)\s*,\s*([^,)]+?)\s*[,)]", source) + self.assertEqual(split, [("v0", "0", "1 << 63"), ("v1", "1 << 63", "1 << 64")]) + + def test_dragon_split_pairs_run_one_group_per_piece(self): + self.assertEqual(len(DRAGON_SPLIT_PAIRS), 15) + self.assertEqual(sum(len(offsets) == 16 for offsets in DRAGON_SPLIT_PAIRS.values()), 12) + self.assertEqual(set(DRAGON_SPLIT_PAIRS) & set(DRAGON_SLOW_PAIRS), set()) + names = {group["name"]: group for group in self.groups} + harnesses = [harness for group in self.groups for harness in group["harnesses"]] + for (window, k), offsets in DRAGON_SPLIT_PAIRS.items(): + plain = f"dragon-shortest-scaling-fixed-k-{window:03d}-{'k' if k < 0 else 'p'}{abs(k):03d}" + with self.subTest(window=window, k=k): + self.assertIn(offsets, ((0, 4, 8, 12), tuple(range(16)))) + self.assertNotIn(plain, names) + self.assertNotIn(dragon_fixed_k_harness(window, k), harnesses) + unproved = DRAGON_UNPROVED_PIECES.get((window, k), ()) + # A piece split on the mantissa has sub-piece groups in place of its group. + for offset in [o for o in offsets if (window, k, o) not in DRAGON_MANTISSA_SPLITS]: + piece = dragon_fixed_k_piece_harness(window, k, offset) + if offset in unproved: + self.assertNotIn(f"{plain}-e{offset:02d}", names) + self.assertNotIn(piece, harnesses) + else: + group = names[f"{plain}-e{offset:02d}"] + self.assertEqual(group["harnesses"], [piece]) + self.assertEqual(group["timeout_minutes"], 180) + self.assertEqual(group["matrix_job"], "dragon-" + "ef"[offset >= 8]) + self.assertEqual(dragon_fixed_k_piece_harness(0, -317, 0), + f"{DRAGON}::dragon_scale_fixed_k::w000::k317::e00::check_scale_shortest") + + def test_dragon_mantissa_splits_run_one_group_per_sub_piece(self): + self.assertEqual(DRAGON_MANTISSA_SPLITS, {(34, -153, 2): 8}) + names = {group["name"]: group for group in self.groups} + for (window, k, offset), count in DRAGON_MANTISSA_SPLITS.items(): + piece = (f"dragon-shortest-scaling-fixed-k-{window:03d}-{'k' if k < 0 else 'p'}{abs(k):03d}" + f"-e{offset:02d}") + with self.subTest(window=window, k=k, offset=offset): + self.assertIn(offset, DRAGON_SPLIT_PAIRS[(window, k)]) + self.assertNotIn(offset, DRAGON_UNPROVED_PIECES.get((window, k), ())) + self.assertIn(count, (2, 4, 8)) + self.assertNotIn(piece, names) + self.assertEqual(sorted(name for name in names if name.startswith(piece)), + [f"{piece}-m{top}" for top in range(count)]) + for top in range(count): + group = names[f"{piece}-m{top}"] + self.assertEqual(group["harnesses"], + [dragon_fixed_k_piece_harness(window, k, offset, top)]) + self.assertEqual(group["timeout_minutes"], 180) + self.assertEqual(group["matrix_job"], "dragon-" + "ef"[offset >= 8]) + self.assertEqual(dragon_fixed_k_piece_harness(34, -153, 2, 0), + f"{DRAGON}::dragon_scale_fixed_k::w034::k153::e02::m0::check_scale_shortest") + + def test_dragon_unproved_pieces_run_nowhere(self): + # Rulings s76 (c) and s85: 40 pieces in 12 pairs, one list for both platforms. + self.assertEqual(len(DRAGON_UNPROVED_PIECES), 12) + self.assertEqual(sum(len(offsets) for offsets in DRAGON_UNPROVED_PIECES.values()), 40) + for pair, offsets in DRAGON_UNPROVED_PIECES.items(): + with self.subTest(pair=pair): + self.assertEqual(DRAGON_SPLIT_PAIRS[pair], tuple(range(16))) + self.assertEqual(offsets, tuple(sorted(set(offsets)))) + # Each pair keeps at least one piece that CI proves. + self.assertLess(len(offsets), 16) + unproved = dragon_unproved_harnesses() + self.assertEqual(len(set(unproved)), 40) + self.assertEqual(set(unproved) & set(self.dedicated), set()) + self.assertEqual(unproved[0], f"{DRAGON}::dragon_scale_fixed_k::w023::k200::e01::check_scale_shortest") + # The general suite skips them too, and the kani list must still hold them. + self.inventory["contract-harnesses"]["retained"] = ["unrelated::proof"] + self.assertEqual(remaining_harnesses(self.inventory), ["unrelated::proof"]) + self.inventory["contract-harnesses"]["other"] = self.dedicated[144:] + with self.assertRaisesRegex(ValueError, "Dedicated flt2dec harnesses missing"): + remaining_harnesses(self.inventory) + + def test_only_exact_catalog_names_are_removed(self): + retained = ["num::imp::flt2dec::check_wrapper", "num::imp::flt2dec::check_future_proof", + self.dedicated[0] + "_extra", "unrelated::proof", "unrelated::proof"] + self.inventory["standard-harnesses"]["retained"] = retained[:3] + self.inventory["contract-harnesses"]["retained"] = retained[3:] + self.assertEqual(remaining_harnesses(self.inventory), retained) + + def test_missing_dedicated_harness_fails(self): + self.inventory["standard-harnesses"]["generators"].pop() + with self.assertRaisesRegex(ValueError, "Dedicated flt2dec harnesses missing"): + remaining_harnesses(self.inventory) + + def test_unknown_inventory_version_fails(self): + self.inventory["file-version"] = "0.2" + with self.assertRaisesRegex(ValueError, "file-version 0.1"): + remaining_harnesses(self.inventory) + + def test_empty_remaining_inventory(self): + self.assertEqual(remaining_harnesses(self.inventory), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/kani-std-analysis/test_flt2dec_runtime.rs b/scripts/kani-std-analysis/test_flt2dec_runtime.rs new file mode 100644 index 0000000000000..418fb7f6c2d97 --- /dev/null +++ b/scripts/kani-std-analysis/test_flt2dec_runtime.rs @@ -0,0 +1,153 @@ +#![feature(staged_api, cfg_target_has_reliable_f16_f128, ergonomic_clones, float_masks, f16)] +#![allow(internal_features, unexpected_cfgs, dead_code)] +#![unstable(feature = "flt2dec_runtime_test", issue = "none")] + +pub use std::{clone, cmp, f64, fmt, iter, mem, ops, option, primitive, ptr, result}; + +#[path = "../../library/core/src/num/imp/bignum.rs"] +pub mod actual_bignum; +#[path = "../../library/core/src/num/imp/flt2dec/decoder.rs"] +pub mod actual_decoder; +#[path = "../../library/core/src/num/imp/diy_float.rs"] +pub mod actual_diy_float; +#[path = "../../library/core/src/num/imp/flt2dec/strategy/dragon.rs"] +pub mod actual_dragon; +#[path = "../../library/core/src/num/imp/flt2dec/estimator.rs"] +pub mod actual_estimator; +#[path = "../../library/core/src/num/imp/flt2dec/strategy/grisu.rs"] +pub mod actual_grisu; +#[path = "../../library/core/src/num/imp/traits.rs"] +pub mod actual_traits; +#[path = "runtime-baseline-dragon.rs"] +pub mod baseline_dragon; +#[path = "runtime-baseline-grisu.rs"] +pub mod baseline_grisu; +#[path = "runtime-common.rs"] +pub mod runtime_common; + +pub mod num { + pub use std::num::*; + pub mod imp { + pub use crate::actual_bignum as bignum; + pub use crate::actual_diy_float as diy_float; + pub use crate::actual_traits::FloatExt; + pub mod flt2dec { + pub use crate::actual_decoder::Decoded; + pub use crate::actual_estimator as estimator; + pub use crate::runtime_common::{MAX_SIG_DIGITS, round_up}; + pub mod strategy { + pub use crate::actual_dragon as dragon; + } + } + } +} + +fn check_case(decoded: actual_decoder::Decoded, check_roundtrip: impl Fn(&str)) { + [17, 32].into_iter().for_each(|len| { + let mut expected_buf = [std::mem::MaybeUninit::uninit(); 32]; + let mut actual_buf = [std::mem::MaybeUninit::uninit(); 32]; + let expected = baseline_grisu::format_shortest_opt(&decoded, &mut expected_buf[..len]) + .map(|(digits, exponent)| (digits.to_vec(), exponent)); + let actual = actual_grisu::format_shortest_opt(&decoded, &mut actual_buf[..len]) + .map(|(digits, exponent)| (digits.to_vec(), exponent)); + assert_eq!(actual, expected, "decoded input: {decoded:?}, buffer: {len}"); + actual.iter().for_each(|(digits, exponent)| { + assert!(!digits.is_empty() && digits.len() <= 17); + assert!(digits.iter().all(u8::is_ascii_digit)); + let significand: String = digits.iter().map(|digit| char::from(*digit)).collect(); + check_roundtrip(&format!( + "{significand}e{}", + i32::from(*exponent) - digits.len() as i32 + )); + }); + }); +} + +fn sample_bits(index: u64) -> u64 { + let value = index.wrapping_add(0x9e3779b97f4a7c15); + let value = (value ^ (value >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + let value = (value ^ (value >> 27)).wrapping_mul(0x94d049bb133111eb); + value ^ (value >> 31) +} + +fn check_exact(decoded: actual_decoder::FullDecoded) { + match decoded { + actual_decoder::FullDecoded::Finite(decoded) => { + [0, 1, 2, 17, 32].into_iter().for_each(|len| { + [i16::MIN, -309, -1, 0, 1, 309, i16::MAX].into_iter().for_each(|limit| { + let mut expected_buf = [std::mem::MaybeUninit::uninit(); 32]; + let mut actual_buf = [std::mem::MaybeUninit::uninit(); 32]; + let (expected, expected_exp) = + baseline_dragon::format_exact(&decoded, &mut expected_buf[..len], limit); + let (actual, actual_exp) = + actual_dragon::format_exact(&decoded, &mut actual_buf[..len], limit); + assert_eq!( + (actual, actual_exp), + (expected, expected_exp), + "decoded input: {decoded:?}, buffer: {len}, limit: {limit}", + ); + assert!(actual.iter().all(u8::is_ascii_digit)); + }); + }); + } + actual_decoder::FullDecoded::Nan + | actual_decoder::FullDecoded::Infinite + | actual_decoder::FullDecoded::Zero => (), + } +} + +#[test] +fn dragon_exact_f64_matches_base() { + (0..2047u64) + .flat_map(|exponent| [0, (1 << 52) - 1].map(move |mantissa| (exponent << 52) | mantissa)) + .chain((0..2000).map(sample_bits)) + .for_each(|bits| check_exact(actual_decoder::decode(f64::from_bits(bits)).1)); +} + +#[test] +fn dragon_exact_f32_matches_base() { + (0..255u32) + .flat_map(|exponent| [0, (1 << 23) - 1].map(move |mantissa| (exponent << 23) | mantissa)) + .chain((0..2000).map(sample_bits).map(|bits| bits as u32)) + .for_each(|bits| check_exact(actual_decoder::decode(f32::from_bits(bits)).1)); +} + +#[test] +fn shortest_f64_matches_base_and_roundtrips() { + let boundaries = (0..2047u64).flat_map(|exponent| { + [0, 1, 1 << 51, (1 << 52) - 1].map(move |mantissa| (exponent << 52) | mantissa) + }); + boundaries.chain((0..20000).map(sample_bits).map(|bits| bits & i64::MAX as u64)).for_each( + |bits| { + let (_, decoded) = actual_decoder::decode(f64::from_bits(bits)); + match decoded { + actual_decoder::FullDecoded::Finite(decoded) => check_case(decoded, |rendered| { + assert_eq!(rendered.parse::().ok().map(f64::to_bits), Some(bits)); + }), + actual_decoder::FullDecoded::Nan + | actual_decoder::FullDecoded::Infinite + | actual_decoder::FullDecoded::Zero => (), + } + }, + ); +} + +#[test] +fn shortest_f32_matches_base_and_roundtrips() { + let boundaries = (0..255u32).flat_map(|exponent| { + [0, 1, 1 << 22, (1 << 23) - 1].map(move |mantissa| (exponent << 23) | mantissa) + }); + boundaries + .chain((0..20000).map(sample_bits).map(|bits| bits as u32 & i32::MAX as u32)) + .for_each(|bits| { + let (_, decoded) = actual_decoder::decode(f32::from_bits(bits)); + match decoded { + actual_decoder::FullDecoded::Finite(decoded) => check_case(decoded, |rendered| { + assert_eq!(rendered.parse::().ok().map(f32::to_bits), Some(bits)); + }), + actual_decoder::FullDecoded::Nan + | actual_decoder::FullDecoded::Infinite + | actual_decoder::FullDecoded::Zero => (), + } + }); +} diff --git a/scripts/kani-std-analysis/test_flt2dec_runtime.sh b/scripts/kani-std-analysis/test_flt2dec_runtime.sh new file mode 100644 index 0000000000000..0ba17f2200091 --- /dev/null +++ b/scripts/kani-std-analysis/test_flt2dec_runtime.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +repo_dir=$(cd -- "$script_dir/../.." && pwd) +common="$script_dir/runtime-common.rs" +baseline="$script_dir/runtime-baseline-grisu.rs" +dragon_baseline="$script_dir/runtime-baseline-dragon.rs" +[[ ! -e "$common" && ! -e "$baseline" && ! -e "$dragon_baseline" ]] +scratch=$(mktemp -d) +trap 'rm -f "$common" "$baseline" "$dragon_baseline"; rm -rf "$scratch"' EXIT + +curl --fail --location --retry 3 --output "$baseline" \ + https://raw.githubusercontent.com/model-checking/verify-rust-std/f9a85eb1e74426b0200e2e14c8ba02bccbc2e7de/library/core/src/num/imp/flt2dec/strategy/grisu.rs +curl --fail --location --retry 3 --output "$dragon_baseline" \ + https://raw.githubusercontent.com/model-checking/verify-rust-std/f9a85eb1e74426b0200e2e14c8ba02bccbc2e7de/library/core/src/num/imp/flt2dec/strategy/dragon.rs +python3 -I - "$repo_dir" "$common" <<'PY' +from pathlib import Path +import sys + +source = (Path(sys.argv[1]) / "library/core/src/num/imp/flt2dec/mod.rs").read_text() +constant = next(line for line in source.splitlines() if line.startswith("pub const MAX_SIG_DIGITS:")) +start = source.index("pub fn round_up(") +end = source.index("\n}\n", start) + 3 +Path(sys.argv[2]).write_text(constant + "\n" + source[start:end]) +PY + +rustc +nightly-2026-09-25 --edition 2024 --test \ + -C opt-level=2 -C debug-assertions=yes -C overflow-checks=yes \ + "$script_dir/test_flt2dec_runtime.rs" -o "$scratch/runtime-tests" +"$scratch/runtime-tests" diff --git a/scripts/kani-std-analysis/test_kani_bootstrap.sh b/scripts/kani-std-analysis/test_kani_bootstrap.sh new file mode 100644 index 0000000000000..96091189778b0 --- /dev/null +++ b/scripts/kani-std-analysis/test_kani_bootstrap.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -eo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$repo_root" +source scripts/run-kani.sh --run list + +test_dir=$(mktemp -d) +trap 'rm -rf -- "$test_dir"' EXIT +export GIT_CONFIG_COUNT=3 +export GIT_CONFIG_KEY_0=protocol.file.allow +export GIT_CONFIG_VALUE_0=always +export GIT_CONFIG_KEY_1=core.hooksPath +export GIT_CONFIG_VALUE_1=/dev/null +export GIT_CONFIG_KEY_2=commit.gpgsign +export GIT_CONFIG_VALUE_2=false + +git init --quiet "$test_dir/charon" +printf '%s\n' 'required compiler dependency' > "$test_dir/charon/required" +git -C "$test_dir/charon" add required +git -C "$test_dir/charon" -c user.name=Test -c user.email=test@example.invalid commit --quiet -m fixture +charon_commit=$(git -C "$test_dir/charon" rev-parse HEAD) + +git init --quiet "$test_dir/origin" +cat > "$test_dir/origin/.gitmodules" < "$test_dir/attempts" +git() { + if [[ "$1" == fetch ]]; then + local attempts + attempts=$(<"$test_dir/attempts") + attempts=$((attempts + 1)) + printf '%s\n' "$attempts" > "$test_dir/attempts" + if [[ "$attempts" -lt 3 ]]; then + return 128 + fi + fi + command git "$@" +} +sleep() { :; } + +# A partial build directory must be initialized, and unavailable benchmark +# repositories must not prevent fetching the required compiler dependency. +mkdir "$test_dir/build" +setup_kani_repo "file://$test_dir/origin" "$test_dir/build" unused "$commit" +[[ $(<"$test_dir/attempts") == 3 ]] +[[ $(git -C "$test_dir/build" rev-parse HEAD) == "$commit" ]] +[[ $(git -C "$test_dir/build/charon" rev-parse HEAD) == "$charon_commit" ]] +[[ -f "$test_dir/build/charon/required" ]] +[[ ! -e "$test_dir/build/firecracker/.git" ]] +[[ ! -e "$test_dir/build/tests/perf/s2n-quic/.git" ]] +[[ "$PWD" == "$repo_root" ]] + +setup_kani_repo "file://$test_dir/origin" "$test_dir/build" unused "$commit" +[[ $(<"$test_dir/attempts") == 4 ]] + +attempts=0 +git() { attempts=$((attempts + 1)); return 42; } +if retry_kani_git fetch; then + echo 'Failed Git downloads were accepted' >&2 + exit 1 +else + status=$? +fi +[[ "$status" == 42 && "$attempts" == 3 ]] +echo 'Kani bootstrap tests passed' diff --git a/scripts/kani-std-analysis/test_kani_runtime_reuse.sh b/scripts/kani-std-analysis/test_kani_runtime_reuse.sh new file mode 100644 index 0000000000000..351454bd96df3 --- /dev/null +++ b/scripts/kani-std-analysis/test_kani_runtime_reuse.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$repo_root" +source scripts/run-kani.sh --run list + +test_dir=$(mktemp -d) +trap 'rm -rf -- "$test_dir"' EXIT +build_dir="$test_dir/kani" +stub_commit=expected +get_current_commit() { printf '%s\n' "$stub_commit"; } + +reject_reuse() { + if check_binary_exists "$build_dir" expected >/dev/null; then + echo "Incomplete or stale Kani runtime was accepted" >&2 + exit 1 + fi +} + +reject_reuse +mkdir -p "$build_dir/scripts" "$build_dir/target/kani/bin" "$build_dir/target/kani/lib" +touch "$build_dir/scripts/kani" "$build_dir/target/kani/bin/kani-driver" \ + "$build_dir/target/kani/bin/kani-compiler" "$build_dir/target/kani/lib/libkani.rlib" +chmod +x "$build_dir/scripts/kani" "$build_dir/target/kani/bin/kani-driver" \ + "$build_dir/target/kani/bin/kani-compiler" +reject_reuse +kani_patch_hash > "$build_dir/target/kani/std-proof-patch.sha256" +check_binary_exists "$build_dir" expected + +printf '%s\n' stale-patch > "$build_dir/target/kani/std-proof-patch.sha256" +reject_reuse +kani_patch_hash > "$build_dir/target/kani/std-proof-patch.sha256" + +stub_commit=stale +reject_reuse +stub_commit=expected +chmod -x "$build_dir/target/kani/bin/kani-compiler" +reject_reuse +chmod +x "$build_dir/target/kani/bin/kani-compiler" +rm "$build_dir/target/kani/lib/libkani.rlib" +reject_reuse +touch "$build_dir/target/kani/lib/libkani.rlib" +rm -rf -- "$build_dir/target" +reject_reuse +echo "Kani runtime reuse tests passed." + +probe_property=num::imp::flt2dec::strategy::grisu::generate_shortest.loop_invariant_step.1 +if (export GITHUB_EVENT_NAME=pull_request KANI_MANUAL_PROBE=false KANI_PROBE_PROPERTY="$probe_property"; + source scripts/run-kani.sh --run list) >/dev/null 2>&1; then + echo "Automatic property filtering was accepted" >&2 + exit 1 +fi +( + export GITHUB_EVENT_NAME=workflow_dispatch KANI_MANUAL_PROBE=true KANI_PROBE_PROPERTY="$probe_property" + source scripts/run-kani.sh --run list + [[ " ${kani_cbmc_args[*]} " == *" --property $probe_property "* ]] +) +if (export GITHUB_EVENT_NAME=workflow_dispatch KANI_MANUAL_PROBE=true KANI_PROBE_PROPERTY=unknown; + source scripts/run-kani.sh --run list) >/dev/null 2>&1; then + echo "Unknown diagnostic property was accepted" >&2 + exit 1 +fi +echo "Manual property probe guards passed." diff --git a/scripts/kani-std-analysis/test_log_parser.py b/scripts/kani-std-analysis/test_log_parser.py new file mode 100644 index 0000000000000..d0b1e29eb6284 --- /dev/null +++ b/scripts/kani-std-analysis/test_log_parser.py @@ -0,0 +1,151 @@ +import json +import tempfile +import unittest + +from log_parser import parse_log_lines, read_kani_list + + +class ReadKaniListTests(unittest.TestCase): + def read_standard_harnesses(self, harnesses, scanner_data): + with tempfile.NamedTemporaryFile(mode='w+') as source: + json.dump({'standard-harnesses': harnesses, + 'contract-harnesses': {}, 'contracts': []}, source) + source.flush() + return read_kani_list(source.name, scanner_data)[1] + + def test_exported_name_in_two_crates_records_all_crates(self): + files = ['/repo/library/panic_abort/src/lib.rs', + '/repo/library/panic_unwind/src/lib.rs'] + scanner_data = {'other_function': { + crate: {'target_safeness': 'unsafe', 'public_target': False} + for crate in ('panic_abort', 'panic_unwind')}} + result = self.read_standard_harnesses( + {file: ['__rust_panic_cleanup'] for file in files}, scanner_data) + entry = result['__rust_panic_cleanup'] + self.assertEqual(entry['file_name'], files[0]) + self.assertEqual(entry['crate'], ['panic_abort', 'panic_unwind']) + self.assertEqual(entry['function'], '__rust_panic_cleanup') + + def test_repeated_identical_entry_preserves_scalar_metadata(self): + result = self.read_standard_harnesses( + {'library/core/src/example.rs': ['check_example', 'check_example']}, {}) + self.assertEqual(result['check_example']['file_name'], + 'library/core/src/example.rs') + self.assertIsNone(result['check_example']['crate']) + + def test_generic_contract_maps_concrete_automatic_harness(self): + concrete = ' as Iterator>::get_unchecked' + generic = ' as Iterator>::get_unchecked' + file = '/repo/library/core/src/array/iter.rs' + with tempfile.NamedTemporaryFile(mode='w+') as source: + json.dump({'standard-harnesses': {}, + 'contract-harnesses': {file: [concrete]}, + 'contracts': [{'function': generic, 'file': file, + 'harnesses': ['kani::internal::automatic_harness']}]}, + source) + source.flush() + contracts, standard = read_kani_list(source.name, {}) + self.assertEqual(standard, {}) + self.assertEqual(set(contracts), {concrete}) + self.assertEqual(contracts[concrete]['function'], generic) + self.assertEqual(contracts[concrete]['file_name'], file) + self.assertIsNone(contracts[concrete]['target_safeness']) + + +class ParseLogLinesTests(unittest.TestCase): + def test_serial_manual_success_failure_and_timeout(self): + lines = """Checking harness successful... +VERIFICATION RESULT: + ** 0 of 2 failed +VERIFICATION:- SUCCESSFUL +Verification Time: 1s +Checking harness failed... +VERIFICATION RESULT: + ** 1 of 5 failed +Failed Checks: pointer dereference +VERIFICATION:- FAILED +Verification Time: 2s +Checking harness timed_out... +VERIFICATION:- TIMEOUT +CBMC timed out. +""".splitlines() + results = parse_log_lines(lines, {}, {}, {}) + self.assertEqual([entry['thread_id'] for entry in results], [0, 0, 0]) + self.assertEqual( + [entry['result']['harness'] for entry in results], + ['successful', 'failed', 'timed_out']) + self.assertEqual( + [entry['result']['result'] for entry in results], + ['SUCCESSFUL', 'FAILED', 'TIMEOUT']) + self.assertEqual( + [entry['result']['time'] for entry in results], ['1s', '2s', 'TO']) + self.assertEqual(results[0]['result']['n_failed_properties'], 0) + self.assertEqual(results[1]['result']['n_failed_properties'], 1) + self.assertEqual(results[1]['result']['n_total_properties'], 5) + self.assertIn('Failed Checks: pointer dereference', + results[1]['result']['output']) + self.assertIsNone(results[2]['result']['n_failed_properties']) + + def test_parallel_results_can_finish_out_of_order(self): + lines = """Thread 0: Checking harness first... +Thread 1: Checking harness second... +Thread 1: +VERIFICATION RESULT: + ** 1 of 5 failed +VERIFICATION:- FAILED +Verification Time: 2s +Thread 0: +VERIFICATION RESULT: + ** 0 of 2 failed +VERIFICATION:- SUCCESSFUL +Verification Time: 3s +""".splitlines() + results = parse_log_lines(lines, {}, {}, {}) + self.assertEqual([entry['thread_id'] for entry in results], [1, 0]) + self.assertEqual(results[0]['result']['harness'], 'second') + self.assertEqual(results[0]['result']['result'], 'FAILED') + self.assertEqual(results[0]['result']['n_failed_properties'], 1) + self.assertEqual(results[1]['result']['harness'], 'first') + self.assertEqual(results[1]['result']['result'], 'SUCCESSFUL') + self.assertEqual(results[1]['result']['time'], '3s') + + def test_serial_autoharness_results_match_threaded_results(self): + header = """Kani generated automatic harnesses for 1 functions ++--+ +| Crate | Selected Function | ++==+ +| core | example | ++--+ +Kani did not generate automatic harnesses for 0 functions ++--+ +| Crate | Skipped Function | Reason | ++==+ ++--+ +""" + metadata = {'example': { + 'crate': 'core', 'function': 'example', 'target_safeness': 'safe', + 'public_target': True, 'file_name': 'example.rs'}} + for contract in ['', "'s contract"]: + with self.subTest(contract=contract): + start = (f'Autoharness: Checking function example{contract} ' + 'against all possible inputs...\n') + result = ('VERIFICATION RESULT:\n ** 0 of 2 failed\n' + 'VERIFICATION:- SUCCESSFUL\nVerification Time: 1s\n') + serial = parse_log_lines( + (header + start + result).splitlines(), {}, metadata, {}) + threaded = parse_log_lines( + (header + 'Thread 0: ' + start + 'Thread 0:\n' + result) + .splitlines(), {}, metadata, {}) + self.assertEqual(serial, threaded) + self.assertEqual(len(serial), 1) + self.assertTrue(serial[0]['result']['is_autoharness']) + self.assertEqual(serial[0]['result']['with_contract'], + bool(contract)) + + def test_incomplete_serial_harness_is_rejected(self): + with self.assertRaises(AssertionError): + parse_log_lines(['Checking harness incomplete...'], {}, {}, {}) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/kani-std-analysis/test_run_kani_list.sh b/scripts/kani-std-analysis/test_run_kani_list.sh new file mode 100644 index 0000000000000..5139f97da76ae --- /dev/null +++ b/scripts/kani-std-analysis/test_run_kani_list.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -eo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$repo_root" + +# Exercise the real argument parser and list dispatch without building Kani. +check_list() ( + local expected_command="$1" + local expected_format="$2" + shift 2 + source scripts/run-kani.sh --run list "$@" + + check_binary_exists() { return 0; } + get_kani_path() { echo test_kani; } + test_kani() { + [[ "$1" != --version ]] || return 0 + [[ "$1" == "$expected_command" ]] + local format_count=0 + local actual_format="" + while (( $# > 0 )); do + case "$1" in + --format) + format_count=$((format_count + 1)) + actual_format="$2" + shift + ;; + --format=*) + format_count=$((format_count + 1)) + actual_format="${1#--format=}" + ;; + esac + shift + done + [[ "$format_count" == 1 && "$actual_format" == "$expected_format" ]] + } + main >/dev/null +) + +check_list list markdown +check_list list json --kani-args --format json +check_list list json --kani-args --format=json +check_list autoharness markdown --with-autoharness +check_list autoharness json --with-autoharness --kani-args --format json + +check_cvc5_encoding() ( + export KANI_FLAT_SMT_CVC5=true KANI_FLAT_SMT_Z3=false KANI_ARITHMETIC_REFINEMENT=false + source scripts/run-kani.sh --run list + # The Yices dialect quantifies constant-array initialization, which the + # integer arithmetic backend cannot reliably solve for coverage checks. + [[ " ${kani_cbmc_args[*]} " == *" --bitwuzla "* ]] + [[ " ${kani_cbmc_args[*]} " == *" --external-smt2-solver "* ]] + [[ " ${kani_cbmc_args[*]} " != *" --yices "* ]] +) +check_cvc5_encoding +echo "Kani list argument tests passed." diff --git a/scripts/kani-std-analysis/test_z3_flat_smt.sh b/scripts/kani-std-analysis/test_z3_flat_smt.sh new file mode 100755 index 0000000000000..338e22e065ead --- /dev/null +++ b/scripts/kani-std-analysis/test_z3_flat_smt.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +test_dir=$(mktemp -d) +trap 'rm -rf -- "$test_dir"' EXIT +solver_adapter="$(dirname "${BASH_SOURCE[0]}")/z3-flat-smt.sh" + +cat > "$test_dir/quantified.smt2" <<'SMT' +(set-logic QF_AUFBV) +(declare-const a (Array (_ BitVec 8) (_ BitVec 8))) +(assert (forall ((i (_ BitVec 8))) (= (select a i) (_ bv0 8)))) +SMT +cat "$test_dir/quantified.smt2" > "$test_dir/unsat.smt2" +cat >> "$test_dir/unsat.smt2" <<'SMT' +(assert (not (= (select a (_ bv0 8)) (_ bv0 8)))) +(check-sat) +SMT +actual_result=$("$solver_adapter" "$test_dir/unsat.smt2") +[[ "$actual_result" == unsat ]] + +cat "$test_dir/quantified.smt2" > "$test_dir/sat.smt2" +echo '(check-sat)' >> "$test_dir/sat.smt2" +actual_result=$("$solver_adapter" "$test_dir/sat.smt2") +[[ "$actual_result" == sat ]] + +echo '(assert missing_symbol)' > "$test_dir/invalid.smt2" +if "$solver_adapter" "$test_dir/invalid.smt2" > "$test_dir/stdout" 2> "$test_dir/stderr"; then + echo "Invalid solver input was accepted" >&2 + exit 1 +fi + +echo "Quantified SMT adapter checks passed" diff --git a/scripts/kani-std-analysis/z3-flat-smt.sh b/scripts/kani-std-analysis/z3-flat-smt.sh new file mode 100755 index 0000000000000..bc458b21a91f8 --- /dev/null +++ b/scripts/kani-std-analysis/z3-flat-smt.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +if (( $# != 1 )); then + echo "Usage: $0 SMT2_FILE" >&2 + exit 2 +fi + +quantified_input=$(mktemp) +trap 'rm -f -- "$quantified_input"' EXIT + +# Kani contracts can contain quantifiers even when CBMC's flat encoding +# declares QF_AUFBV. Keep every assertion and permit quantified formulas. +awk ' + $0 == "(set-logic QF_AUFBV)" { print "(set-logic AUFBV)"; next } + { print } +' "$1" > "$quantified_input" + +z3 -smt2 "$quantified_input" diff --git a/scripts/run-kani.sh b/scripts/run-kani.sh index 55227b2921c11..8b263e8564d55 100755 --- a/scripts/run-kani.sh +++ b/scripts/run-kani.sh @@ -14,7 +14,50 @@ usage() { } # Initialize variables +# Generator proofs need more objects; helper jobs can select a smaller capacity. declare -a command_args +kani_object_bits="${KANI_OBJECT_BITS:-14}" +# Optional encoding experiments preserve the verification checks and bounds. +kani_cbmc_args=(--object-bits "$kani_object_bits") +if [[ -n "${KANI_PROBE_PROPERTY:-}" ]]; then + if [[ "${GITHUB_EVENT_NAME:-}" != workflow_dispatch || "${KANI_MANUAL_PROBE:-false}" != true ]]; then + echo "Property probes require an explicitly selected manual diagnostic run." >&2 + exit 2 + fi + case "$KANI_PROBE_PROPERTY" in + num::imp::flt2dec::strategy::grisu::generate_shortest.loop_invariant_base.1|num::imp::flt2dec::strategy::grisu::generate_shortest.loop_invariant_step.1|num::imp::flt2dec::strategy::grisu::generate_integral_digits.loop_invariant_step.1) + kani_cbmc_args+=(--property "$KANI_PROBE_PROPERTY") ;; + *) echo "Unknown diagnostic loop property." >&2; exit 2 ;; + esac +fi +if [[ "${KANI_SYMEX_CACHE_DEREFERENCES:-false}" == true ]]; then + kani_cbmc_args+=(--symex-cache-dereferences) +fi +if [[ "${KANI_ARRAY_FIELD_SENSITIVITY:-true}" == false ]]; then + kani_cbmc_args+=(--no-array-field-sensitivity) +fi +if [[ "${KANI_FLAT_SMT_CVC5:-false}" == true ]]; then + if [[ "${KANI_FLAT_SMT_Z3:-false}" == true || "${KANI_ARITHMETIC_REFINEMENT:-false}" == true ]]; then + echo "CVC5 cannot be combined with another arithmetic backend." >&2 + exit 2 + fi + kani_cvc5_wrapper="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/kani-std-analysis/cvc5-flat-smt.sh" + # This flat encoding uses native constant arrays without datatype wrappers + # or quantified array initializers. + kani_cbmc_args+=(--bitwuzla --external-smt2-solver "$kani_cvc5_wrapper") +elif [[ "${KANI_FLAT_SMT_Z3:-false}" == true ]]; then + if [[ "${KANI_ARITHMETIC_REFINEMENT:-false}" == true ]]; then + echo "Flat SMT encoding cannot be combined with SAT arithmetic refinement." >&2 + exit 2 + fi + # The Yices-compatible encoding avoids CBMC's datatype construction failure. + # The adapter declares quantified logic without changing any assertions. + kani_flat_smt_wrapper="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/kani-std-analysis/z3-flat-smt.sh" + echo "Using Z3 with CBMC's flat SMT encoding and quantified logic." >&2 + kani_cbmc_args+=(--yices --external-smt2-solver "$kani_flat_smt_wrapper") +elif [[ "${KANI_ARITHMETIC_REFINEMENT:-false}" == true ]]; then + kani_cbmc_args+=(--refine-arithmetic) +fi path="" run_command="verify-std" with_autoharness="false" @@ -114,13 +157,29 @@ read_commit_from_toml() { echo "$commit" } -setup_kani_repo() { +retry_kani_git() { + local attempt status + for attempt in 1 2 3; do + if git "$@"; then + return 0 + else + status=$? + fi + if [[ "$attempt" -lt 3 ]]; then + echo "Kani Git download failed; retrying ($attempt/3)" >&2 + sleep "$((attempt * 5))" + fi + done + return "$status" +} + +setup_kani_repo() ( local repo_url="$1" local directory="$2" local branch="$3" local commit="$4" - if [[ ! -d "${directory}" ]]; then + if [[ ! -d "${directory}/.git" ]]; then mkdir -p "${directory}" pushd "${directory}" > /dev/null @@ -130,13 +189,13 @@ setup_kani_repo() { pushd "${directory}" > /dev/null fi - git fetch --depth 1 origin "$commit" --quiet - git checkout "$commit" --quiet - # Workaround for occasionally failing to copy a file in s2n-quic that we - # don't actually care about - GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive --depth 1 --quiet + retry_kani_git fetch --depth 1 origin "$commit" --quiet || return $? + git checkout "$commit" --quiet || return $? + # Cargo resolves the compiler's optional Charon dependency. Firecracker + # and s2n-quic are benchmark fixtures and are not needed to build Kani. + GIT_LFS_SKIP_SMUDGE=1 retry_kani_git submodule update --init --recursive --depth 1 --quiet -- charon || return $? popd > /dev/null -} +) get_current_commit() { local directory="$1" @@ -147,9 +206,69 @@ get_current_commit() { fi } +install_kani_dependencies() ( + # Keep this download policy scoped to dependency installation in CI. + if [[ "${GITHUB_ACTIONS:-false}" != true ]]; then + exec "$@" + fi + + local kani_download_config + kani_download_config=$(mktemp -d) + trap 'rm -f -- "$kani_download_config/.curlrc" + rmdir -- "$kani_download_config"' EXIT + # The pinned installers use curl -O for their archives. HTTP errors must + # fail before extraction, and retries must not run without time limits. + cat > "$kani_download_config/.curlrc" <<'EOF' +fail +retry = 5 +retry-all-errors +retry-delay = 5 +retry-max-time = 300 +connect-timeout = 30 +max-time = 120 +EOF + # Homebrew's API refresh uses its own curl policy. Retry the installer too, + # so a transient brew update failure does not abort the proof job. + local attempt status=0 + for attempt in 1 2 3; do + if CURL_HOME="$kani_download_config" "$@"; then + return 0 + else + status=$? + fi + echo "Kani dependency installation failed (attempt $attempt of 3)" >&2 + if [[ "$attempt" -lt 3 ]]; then + sleep 10 + fi + done + return "$status" +) + +prepare_cbmc_tap() { + local attempt + for attempt in 1 2 3; do + if brew tap diffblue/cbmc; then + return 0 + fi + echo "CBMC tap fetch failed (attempt $attempt of 3)" >&2 + if [[ "$attempt" -lt 3 ]]; then + sleep 10 + fi + done + return 1 +} + +kani_patch_hash() { + shasum -a 256 "$WORK_DIR/scripts/kani-explicit-for-counter.patch" | awk '{print $1}' +} + build_kani() { local directory="$1" pushd "$directory" + local proof_patch="$WORK_DIR/scripts/kani-explicit-for-counter.patch" + if ! git apply --reverse --check "$proof_patch" >/dev/null 2>&1; then + git apply "$proof_patch" + fi source "kani-dependencies" # Check if installed versions are correct. if ./scripts/check-cbmc-version.py \ @@ -160,7 +279,7 @@ build_kani() { os_name=$(uname -s) if [[ "$os_name" == "Linux" ]]; then - ./scripts/setup/ubuntu/install_deps.sh + install_kani_dependencies ./scripts/setup/ubuntu/install_deps.sh elif [[ "$os_name" == "Darwin" ]]; then # Homebrew 6.0+ refuses to load formulae from third-party taps # unless they are explicitly trusted, which breaks the @@ -168,13 +287,15 @@ build_kani() { # Trust the tap first (no-op on older Homebrew). Mirrors # model-checking/kani#4785. brew trust diffblue/cbmc 2>/dev/null || true - ./scripts/setup/macos/install_deps.sh + prepare_cbmc_tap || return 1 + install_kani_dependencies ./scripts/setup/macos/install_deps.sh else echo "Unknown operating system" fi fi - cargo build-dev --release + CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-2}" cargo build-dev --release + kani_patch_hash > target/kani/std-proof-patch.sha256 popd } @@ -198,11 +319,20 @@ get_harnesses() { fi # Extract the harnesses inside "standard-harnesses" and "contract-harnesses" # into an array called ALL_HARNESSES and the length of that array into HARNESS_COUNT - ALL_HARNESSES=($(jq -r ' - ([.["standard-harnesses"] | to_entries | .[] | .value[]] + - [.["contract-harnesses"] | to_entries | .[] | .value[]]) | - .[] - ' $WORK_DIR/kani-list.json)) + if [[ "${KANI_SEPARATE_FLT2DEC:-false}" == true ]]; then + # Dedicated jobs verify these exact names on both operating systems. + # Fail if any expected target is absent instead of silently dropping it. + local remaining_harnesses + remaining_harnesses=$(python3 -I "$WORK_DIR/scripts/kani-std-analysis/flt2dec_harnesses.py" \ + --remaining "$WORK_DIR/kani-list.json") + ALL_HARNESSES=($remaining_harnesses) + else + ALL_HARNESSES=($(jq -r ' + ([.["standard-harnesses"] | to_entries | .[] | .value[]] + + [.["contract-harnesses"] | to_entries | .[] | .value[]]) | + .[] + ' "$WORK_DIR/kani-list.json")) + fi HARNESS_COUNT=${#ALL_HARNESSES[@]} } @@ -210,7 +340,12 @@ get_harnesses() { run_verification_subset() { local kani_path="$1" local harnesses=("${@:2}") # All arguments after kani_path are harness names - + + if (( ${#harnesses[@]} == 0 )); then + echo "No harnesses assigned to this partition." + return + fi + # Build the --harness arguments local harness_args="" for harness in "${harnesses[@]}"; do @@ -219,33 +354,33 @@ run_verification_subset() { echo "Running verification for harnesses:" printf '%s\n' "${harnesses[@]}" - # Use KANI_JOBS to cap the number of parallel harnesses; some harnesses peak - # at close to 10 GB of memory, so running one per core can exhaust the - # memory of smaller CI runners (e.g., 4-core/16 GB ubuntu-latest). - local jobs_arg="-j" - if [[ -n "${KANI_JOBS:-}" ]]; then - jobs_arg="--jobs=${KANI_JOBS}" - fi + # Honor KANI_JOBS and default to one verifier per runner. "$kani_path" verify-std -Z unstable-options ./library \ $unstable_args \ --no-assert-contracts \ $harness_args --exact \ - $jobs_arg \ + --jobs "${KANI_JOBS:-1}" \ --output-format=terse \ "${command_args[@]}" \ - --cbmc-args --object-bits 12 + --cbmc-args "${kani_cbmc_args[@]}" } # Check if binary exists and is up to date check_binary_exists() { local build_dir="$1" local expected_commit="$2" - local kani_path=$(get_kani_path "$build_dir") - if [[ -d "${build_dir}" ]]; then local current_commit=$(get_current_commit "$build_dir") if [[ "$current_commit" = "$expected_commit" ]]; then - return 0 + if [[ -x "$build_dir/scripts/kani" && + -x "$build_dir/target/kani/bin/kani-driver" && + -x "$build_dir/target/kani/bin/kani-compiler" && + -f "$build_dir/target/kani/lib/libkani.rlib" && + -f "$build_dir/target/kani/std-proof-patch.sha256" ]] && + [[ $(cat "$build_dir/target/kani/std-proof-patch.sha256") == $(kani_patch_hash) ]]; then + return 0 + fi + echo "Kani runtime is missing or incomplete. Rebuilding..." else echo "Kani repository is out of date. Rebuilding..." fi @@ -271,7 +406,7 @@ main() { echo "Building Kani from commit: $commit" # Clone repository and checkout specific commit - setup_kani_repo "$REPO_URL" "$build_dir" "$BRANCH_NAME" "$commit" + setup_kani_repo "$REPO_URL" "$build_dir" "$BRANCH_NAME" "$commit" || return $? # Build project build_kani "$build_dir" @@ -286,6 +421,23 @@ main() { echo "Running Kani command..." "$kani_path" --version + if [[ "${KANI_PATCHED_CBMC:-false}" == true || "${KANI_FLAT_SMT_Z3:-false}" == true || "${KANI_FLAT_SMT_CVC5:-false}" == true ]]; then + bash "$WORK_DIR/scripts/setup-flat-smt-cbmc.sh" "$WORK_DIR/cbmc_build" + export PATH="$WORK_DIR/cbmc_build/bin:$PATH" + fi + if [[ "${KANI_FLAT_SMT_CVC5:-false}" == true ]]; then + bash "$WORK_DIR/scripts/setup-cvc5.sh" "$WORK_DIR/cvc5_build" + export KANI_CVC5_BINARY="$WORK_DIR/cvc5_build/bin/cvc5" + bash "$(dirname "$kani_cvc5_wrapper")/test_cvc5_flat_smt.sh" + fi + if [[ "${KANI_FLAT_SMT_Z3:-false}" == true ]]; then + bash "$(dirname "$kani_flat_smt_wrapper")/test_z3_flat_smt.sh" + "$kani_path" "$WORK_DIR/scripts/kani-std-analysis/overflow-result.rs" \ + -Z unstable-options --jobs 1 --harness-timeout 60s \ + --solver minisat --output-format=terse \ + --cbmc-args --yices --external-smt2-solver "$kani_flat_smt_wrapper" + fi + if [[ "$run_command" == "verify-std" ]]; then if [[ -n "$WORKER_INDEX" && -n "$WORKER_TOTAL" ]]; then echo "Running as parallel worker $WORKER_INDEX of $WORKER_TOTAL" @@ -314,7 +466,7 @@ main() { $unstable_args \ --no-assert-contracts \ "${command_args[@]}" \ - --cbmc-args --object-bits 12 + --cbmc-args "${kani_cbmc_args[@]}" fi elif [[ "$run_command" == "autoharness" ]]; then # Run verification for a subset of automatically generated harnesses @@ -324,14 +476,27 @@ main() { $unstable_args \ --no-assert-contracts \ "${command_args[@]}" \ - --cbmc-args --object-bits 12 + --cbmc-args "${kani_cbmc_args[@]}" elif [[ "$run_command" == "list" ]]; then echo "Running Kani list command..." + local list_format_args=(--format markdown) + local argument + for argument in "${command_args[@]}"; do + case "$argument" in + --format|--format=*) + list_format_args=() + break + ;; + esac + done if [[ "$with_autoharness" == "true" ]]; then - "$kani_path" autoharness -Z autoharness --list $unstable_args --std ./library --format markdown \ + "$kani_path" autoharness -Z autoharness --list $unstable_args --std ./library \ + "${list_format_args[@]}" \ "${command_args[@]}" else - "$kani_path" list $unstable_args ./library --std --format markdown + "$kani_path" list $unstable_args ./library --std \ + "${list_format_args[@]}" \ + "${command_args[@]}" fi elif [[ "$run_command" == "metrics" ]]; then local current_dir=$(pwd) @@ -364,7 +529,7 @@ main() { $unstable_args \ --no-assert-contracts \ "${command_args[@]}" \ - --cbmc-args --object-bits 12 + --cbmc-args "${kani_cbmc_args[@]}" # remove metadata file for Kani-generated "dummy" crate that we won't # get scanner data for local target=$(find "target/kani_verify_std/target/" -mindepth 1 -maxdepth 1 \ @@ -393,4 +558,6 @@ main() { fi } -main +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main +fi diff --git a/scripts/setup-cvc5.sh b/scripts/setup-cvc5.sh new file mode 100755 index 0000000000000..9925ff4a813e3 --- /dev/null +++ b/scripts/setup-cvc5.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euo pipefail + +# Keep the arithmetic backend identical across CI platforms. +install_dir=${1:?Usage: setup-cvc5.sh INSTALL_DIRECTORY} +case "$(uname -s):$(uname -m)" in + Linux:x86_64) + package=cvc5-Linux-x86_64-static + checksum=6d44abc233980a14d72cc5809287d27c3335b1d6ee863381d0b5ffcbd0d8de56 + ;; + Darwin:arm64) + package=cvc5-macOS-arm64-static + checksum=18e0bd283d44f720f72bf80175169ef63e985628b7ba1502aaf812f57f981461 + ;; + Darwin:x86_64) + package=cvc5-macOS-x86_64-static + checksum=bdde9557bbb9b812af270c3f418836c285957b3ed81385b87428d2d595ffbf47 + ;; + *) echo 'Unsupported CVC5 platform' >&2; exit 2 ;; +esac + +stamp="cvc5-1.2.1:$checksum" +if [[ -x "$install_dir/bin/cvc5" && -f "$install_dir/version" ]] && + [[ "$(cat "$install_dir/version")" == "$stamp" ]]; then + exit 0 +fi + +mkdir -p "$install_dir" +staging=$(mktemp -d "$install_dir/download.XXXXXX") +trap 'rm -rf "$staging"' EXIT +curl --fail --location --retry 3 --silent --show-error \ + "https://github.com/cvc5/cvc5/releases/download/cvc5-1.2.1/$package.zip" \ + --output "$staging/cvc5.zip" +actual=$(shasum -a 256 "$staging/cvc5.zip") +[[ "${actual%% *}" == "$checksum" ]] || { echo 'CVC5 checksum mismatch' >&2; exit 1; } +unzip -q "$staging/cvc5.zip" -d "$staging" +mkdir -p "$install_dir/bin" +cp "$staging/$package/bin/cvc5" "$install_dir/bin/cvc5" +cp -R "$staging/$package/licenses" "$install_dir/" +"$install_dir/bin/cvc5" --version +printf '%s\n' "$stamp" > "$install_dir/version" diff --git a/scripts/setup-flat-smt-cbmc.sh b/scripts/setup-flat-smt-cbmc.sh new file mode 100644 index 0000000000000..0282ce695db3e --- /dev/null +++ b/scripts/setup-flat-smt-cbmc.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# CBMC 6.11's flat SMT encoding reverses arithmetic result and overflow fields. +# Backport the approved fix from https://github.com/diffblue/cbmc/pull/9145. +# Keep diagnostic traces opt-in with https://github.com/diffblue/cbmc/pull/9150. +# Kani explicitly requests --trace when generating concrete playback tests. +# Skip unused loop write-set inference when every loop has an explicit frame. +# Link CaDiCaL next to MiniSAT. Kani requests --sat-solver cadical by default, +# and a CBMC without CaDiCaL uses MiniSAT instead. +release=cbmc-6.11.0 +fix=e36c27dd2e57ef6ce39c02a382d1c60ff6f4593f +trace_fix=d7caa9d6ff314348a99cb31778e569239ff6dd27 +sat_impl="minisat2;cadical" +repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +dfcc_patch="$repository_root/scripts/cbmc-skip-unused-loop-assigns.patch" +dfcc_fix=$(shasum -a 256 "$dfcc_patch" | awk '{print $1}') +revision="$release-$fix-$trace_fix-$dfcc_fix-$sat_impl" +build_root=${1:-"$repository_root/cbmc_build"} +mkdir -p "$build_root/bin" +build_root=$(cd "$build_root" && pwd) + +if [[ -x "$build_root/bin/cbmc" && -x "$build_root/bin/goto-instrument" && + -x "$build_root/bin/goto-cc" && + -f "$build_root/bin/revision" ]] && + [[ $(cat "$build_root/bin/revision") == "$revision" ]]; then + echo "Patched CBMC tools are up to date." + exit 0 +fi + +download() { + local url=$1 destination=$2 expected=$3 actual + curl --fail --location --retry 3 --silent --show-error "$url" -o "$destination" + actual=$(shasum -a 256 "$destination" | awk '{print $1}') + if [[ "$actual" != "$expected" ]]; then + echo "Checksum mismatch for $destination" >&2 + exit 1 + fi +} + +work_root=$(mktemp -d "$build_root/work.XXXXXX") +download "https://api.github.com/repos/diffblue/cbmc/tarball/$release" \ + "$work_root/source.tar.gz" \ + e1e338604c77e040c5a142dd12c6fbb41c43d079588a07d5b0f89b3c57409d0a +download "https://github.com/diffblue/cbmc/commit/$fix.patch" \ + "$work_root/overflow.patch" \ + 24824fb1ae03d82d5ae81002e60c933d4d516caa9620759c95da3bf43ee4fbe2 +download "https://github.com/diffblue/cbmc/commit/$trace_fix.patch" \ + "$work_root/traces.patch" \ + 3446691194b090e199b2e5d9ccbad1687154656530666f1a3526d0b3ae82618e +mkdir "$work_root/source" +tar -xzf "$work_root/source.tar.gz" -C "$work_root/source" --strip-components 1 +patch --batch -p1 -d "$work_root/source" < "$work_root/overflow.patch" +patch --batch -p1 -d "$work_root/source" < "$work_root/traces.patch" +patch --batch -p1 -d "$work_root/source" < "$dfcc_patch" + +# Keep CBMC's pinned MiniSAT checksum, using Debian's HTTPS endpoint. +python3 -I - "$work_root/source/src/solvers/CMakeLists.txt" <<'PY' +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +source = path.read_text() +old = 'http://ftp.debian.org/debian/pool/main/m/minisat2/minisat2_2.2.1.orig.tar.gz' +new = 'https://deb.debian.org/debian/pool/main/m/minisat2/minisat2_2.2.1.orig.tar.gz' +assert source.count(old) == 1 +path.write_text(source.replace(old, new)) +PY + +cmake -S "$work_root/source" -B "$work_root/build" \ + -DCMAKE_BUILD_TYPE=Release -DWITH_JBMC=OFF -DWITH_MEMORY_ANALYZER=OFF \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -Dsat_impl="$sat_impl" +cmake --build "$work_root/build" --target cbmc goto-instrument goto-cc --parallel 2 +install -m 755 "$work_root/build/bin/cbmc" "$build_root/bin/cbmc" +install -m 755 "$work_root/build/bin/goto-instrument" "$build_root/bin/goto-instrument" +install -m 755 "$work_root/build/bin/goto-cc" "$build_root/bin/goto-cc" +printf '%s\n' "$revision" > "$build_root/bin/revision" +rm -rf "$work_root"