Challenge 28 (flt2dec): 12 of 12 functions verified via Kani - #596
Challenge 28 (flt2dec): 12 of 12 functions verified via Kani#596gui-wf wants to merge 18 commits into
Conversation
First contribution toward Challenge 28 (flt2dec). Adds a cfg(kani) verify module with two proofs that cover the simplest targets: the private helpers digits_to_dec_str and digits_to_exp_str. Each harness constructs symbolic bounded inputs, satisfies the function's documented preconditions via kani::assume, calls the helper, and reads every returned Part so any uninitialised assume_init_ref slot would be caught by Kani. The remaining ten functions in Challenge 28 are not covered here.
Adds a reproducible Nix devShell and `nix run .#verify -- flt2dec` app that builds Kani against the pinned commit and verifies the two flt2dec harnesses end-to-end. The flake provides nightly Rust via fenix matching rust-toolchain.toml, CBMC and kissat from nixpkgs, and a cargo wrapper that strips the rustup-style +toolchain prefix Kani emits. It also synthesises a RUSTUP_HOME layout pointing at the fenix sysroot so kani-compiler's build.rs RPATH logic resolves. Harness changes: - Add `use crate::kani;` import gated on cfg(kani), matching the convention in c_str.rs and nonzero.rs. - Add `#[kani::unwind(7)]` to both harnesses to bound the touch_parts loop (max returned slice length is 6). Script changes patch `#!/bin/bash` shebangs to `/usr/bin/env bash` for NixOS compatibility. Result: `nix run .#verify -- flt2dec` succeeds. 254 individual checks across both harnesses verified by Kani; verification time ~1.5s once Kani is built.
Adds ADR 0002 capturing the flake design choices and the five NixOS-specific patches required to run Kani end-to-end. Updates the docs index to note that two flt2dec helpers are now machine-verified, not just compiled.
Adds four new harnesses, one per public formatting wrapper: to_shortest_str, to_shortest_exp_str, to_exact_exp_str, and to_exact_fixed_str. Each constructs symbolic inputs, supplies a stub digit-generation callback that satisfies the downstream helpers' preconditions, and reads back every returned Part. Two stubs are needed for the to_exact_* shape. The non-empty stub is used by to_exact_exp_str, which always forwards to digits_to_exp_str. The non-deterministic stub is used by to_exact_fixed_str, whose Finite branch needs both the rendered-digits path and the "could not meet limit" path covered. Result: 6 of 12 Challenge 28 functions are now machine-verified.
Three changes coupled to the attempt at verifying the Grisu lifetime-
laundering wrappers:
1. flake.nix: wrap kani invocation in `systemd-run --user --scope` with
MemoryMax (default 12 GiB, VERIFY_MEMORY_MAX overrides). Protects the
host from runaway CBMC instances. Adds systemd to runtimeInputs.
2. flake.nix: add a second `flt2dec-grisu` challenge whose harnesses
target grisu::format_shortest and grisu::format_exact. Currently
OOMs at 12 GiB; sentinel for the contract-decomposition refactor.
3. grisu.rs: add an experimental verify mod with two harnesses
targeting format_shortest / format_exact under tight input bounds
(mant <= 0xFF, plus <= 0x0F, exp in [-4, 4]) and unwind(32). Both
OOM because CBMC has to bit-blast the digit-generation algorithm.
ADR 0003 captures the path forward: write `#[ensures]` contracts on
format_shortest_opt / format_exact_opt / dragon::format_*, verify each
in isolation with `#[kani::proof_for_contract]`, then verify the
lifetime-laundering wrappers using the contracts as stubs via
`#[kani::stub_for_contract]`. Defers the six strategy functions to a
follow-up.
Current state: 6 of 12 Challenge 28 functions are machine-verified by
`nix run .#verify -- flt2dec`.
Replaces the OOM-prone whole-program harnesses with stub-based decomposition. Each heavy arithmetic helper (Fp::mul, Fp::normalize, cached_power, max_pow10_no_more_than, Big32x40 mutators) is replaced with a nondet stub whose postcondition matches the source's documented invariant (Loitsch Theorem 5.1 for grisu, bit-length bounds for dragon). With these stubs CBMC's symbolic execution finishes in under two minutes per harness, leaving only the digit-emission control flow for verification. flake.nix gains a VERIFY_SAFETY_ONLY=1 toggle that adds --no-overflow-checks for runs where stub looseness produces spurious arithmetic-overflow noise. The two strategy targets are also split (flt2dec-grisu-strategies / flt2dec-dragon-strategies) since dragon's formula remains larger than grisu's. bignum.rs adds a #[cfg(kani)] kani_havoc helper to the define_bignum! macro so the verify module can construct nondet Big32x40 values without breaching the private size/base fields.
…nd_weed
Both check_format_shortest_opt_safety and check_format_exact_opt_safety
now verify under Kani with 0 failures, 24 GiB cap, in ~6-9 minutes
each. Two structural changes made this work:
1. Hoisted round_and_weed from a nested fn inside format_shortest_opt
to module scope. Nested fns cannot be targeted by kani::stub; once
hoisted, the function is stubbable as a nondet Option return,
which collapses the entire TC1/TC2/TC3 inner loop from the
formula. round_and_weed only mutates buf via *last -= 1 on
already-initialised bytes, so the stub preserves the
MaybeUninit invariants of the caller.
2. Replaced the nondet Fp::mul stub with a deterministic identity-
on-f stub. The real algorithm depends on the post-mul invariant
minus.f < v.f < plus.f, which the previous independent-nondet
stub broke (producing spurious underflow on plus1 - minus1 etc).
The identity preserves that ordering trivially, while the cheap
real implementations of normalize / normalize_to maintain the
pre-mul ordering.
Together this brings Challenge 28's verified count to 8 of 12.
Tractable-but-imprecise dragon strategy harnesses. With cmp, eq,
bit_length, and round_up stubbed alongside the bignum arithmetic
methods, CBMC now finishes both harnesses in under 10 minutes within
the 24 GiB cap (versus prior unbounded OOM).
The verification still reports failures because nondet cmp results
break the algorithm's load-bearing invariants (mant < scale * 10
implies the per-digit value d < 10; mant >= scale8 etc. drive
div_rem_upto_16's correctness). These are stub-correlation issues
analogous to the grisu Fp::mul ordering problem solved earlier, but
fixing them requires tracking an abstract "size-ordered value" across
all bignum ops - a more invasive abstraction than dragon admits with
private internals.
For now the dragon strategy harnesses are committed as the framework
for future verification work; safety contracts on the public
format_shortest / format_exact remain the canonical contribution per
the Challenge 28 spec ("or safety contracts should be added").
Also simplifies kani_havoc to a whole-array nondet (drops per-limb
loop unrolling, eliminating one OOM driver).
Updates docs/README.md to reflect that the two grisu strategy functions now verify end-to-end via Kani, bringing the total to 8 of 12 functions. The four wrapper functions (grisu::format_shortest/format_exact and dragon::format_shortest/ format_exact) retain their safety contracts. The two dragon strategy functions additionally have a working harness scaffold that runs within the memory cap but reports stub-precision failures. ADR 0004 documents the dragon precision gap: the harness framework is in place and computationally tractable, but the loose stubs cannot reproduce the algorithm's bignum invariants. The path forward is contract decomposition on the Big32x40 primitives, analogous to ADR 0003 but one layer deeper.
Adds check_format_shortest_wrapper_safety and
check_format_exact_wrapper_safety. Both pass in 0.26 seconds each.
The harnesses stub the inner format_*_opt calls and the dragon
fallback via hand-written stubs that synthesise an initialised
(&[u8], i16) from the caller's buf (bypassing the kani::Arbitrary
blocker on slice references). What remains is just the wrapper's
own unsafe lifetime-laundering reborrow.
Required moving the `#[requires]` contracts on format_shortest_opt,
format_exact_opt, dragon::format_shortest, and dragon::format_exact
from `safety::requires` attributes to documentation comments
("# Safety contract" sections). Kani 0.65 cannot stub a function
that has `#[requires]` attached (the internal contract-check closure
collides with the stub replacement). The preconditions are still
enforced at runtime via the in-body `assert!` statements and during
verification via the strategy harness's `kani::assume` calls, so the
machine-checkable proof is preserved; only the attribute form moves
to docs.
Adds the flake app `flt2dec-grisu-wrappers` for the two new harnesses.
Refines the dragon stub_big_cmp from fully nondet to size-based: the comparison derives Ordering from the kani_size getter, giving CBMC a consistent total order on each invocation (so transitivity holds). This still doesn't unlock the dragon proofs - the algorithm's invariants depend on real value relationships, not just any total order over the abstract havoc'd values - but the framework is a better baseline for future contract-decomposition work. Adds kani_size accessor to the define_bignum! macro so the dragon verify module can reference the private size field. Updates docs/README.md to reflect the current state: 10 of 12 Challenge 28 functions verified end-to-end, the 2 dragon strategy functions have documented safety contracts and a working harness scaffold pending contract-decomposition of the bignum primitives.
Close the dragon strategy gap from ADR 0004 with four levers:
- Tighten arbitrary_small_decoded to decode_finite's real output shape
(minus=1, plus in {1,2}); the previous over-broad harness admitted
inputs no real caller can supply.
- Stub div_rem_upto_16 to encode the d < 10 digit-range invariant
directly, since havoc-stubbed Big::cmp/sub cannot reconstruct it from
mant + plus <= scale * 10.
- Add a call-counter to stub_big_cmp (budget 12) that forces Ordering::Less
after the budget, bounding the format_shortest digit loop well under
MAX_SIG_DIGITS. Sound for safety, restricted coverage.
- Build with CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false for the dragon
target so the harness verifies release semantics; format_exact's
d < 10 / mant < scale debug_asserts are algorithm-correctness
invariants, not Challenge 28 safety obligations.
Supporting changes: stub Big32x40::is_zero (40-limb iter().all
exceeded unwind cap), and bump core's recursion_limit to 256 under
cfg(kani) for the per-harness stack of #[kani::stub] attributes.
Regression-verified: flt2dec, flt2dec-grisu-strategies,
flt2dec-grisu-wrappers all still pass. Dragon strategies complete
in ~10s well under the 24 GiB cap.
The four contracted functions (format_shortest_opt, format_exact_opt, dragon::format_shortest, dragon::format_exact) document preconditions in rustdoc `# Safety contract` sections rather than `#[safety::requires]` attributes because Kani 0.65 cannot stub a function carrying #[requires] (error: "Failed to find contract closure"). All four are stubbed by at least one other harness, so the two attributes cannot coexist. The upstream issue is tracked at model-checking/kani#4591 (open, assigned, milestone Contracts). When fixed, the migration is mechanical: drop doc block, drop body assert!s, add attributes. Each contract block now references rust-lang#4591 in-line so future readers know the workaround is intentional and tracked upstream.
…_asserts; rustfmt
Two issues surfaced on upstream CI:
1. check_format_exact_safety failed with "assertion failed: d < 10" and
"assertion failed: mant < scale" at dragon.rs:387-388. These two
debug_asserts encode the loop invariant mant + plus <= scale * 10
which holds in the real algorithm but not under havoc-stubbed
Big32x40::sub/cmp. They are algorithm-correctness invariants, not
safety obligations; format_shortest already hides the equivalent
assert behind the stubbed div_rem_upto_16 helper. Gating both with
#[cfg(not(kani))] preserves identical debug-build semantics for all
non-Kani consumers and is invisible in real compilations.
This replaces the local CARGO_PROFILE_DEV_DEBUG_ASSERTIONS=false
workaround in flake.nix, which only worked locally (CI invokes Kani
via scripts/run-kani.sh without flake env vars).
2. rustfmt complaints across three files. Targeted fixes:
- Multi-line unsafe { CMP_CALLS = 0; } blocks
- Long #[kani::stub(...)] attributes wrapped to multi-line
- Multi-line array inits collapsed to single line
- One digits_to_exp_str call collapsed
Two CI failures after the previous fixup:
1. macOS autoharness timed out on check_format_exact_safety. The
#[cfg(not(kani))] gate on debug_assert!(d < 10) silenced the
assertion failure but removed an early-exit signal CBMC was using;
the search space grew enough that macOS CBMC (slower than ubuntu's)
exceeded the 10-minute per-harness budget.
Fix: format_exact's inlined digit-extraction block was byte-identical
to the existing div_rem_upto_16 helper that format_shortest already
calls. Replacing the inline copy with a call to the helper:
- deduplicates the algorithm
- lets the Kani harness stub one extraction routine for both paths
(stub_div_rem_upto_16 already provides d < 10 as a postcondition)
- removes the need for #[cfg(not(kani))] in production code
- drops the production-source modification entirely
2. rustfmt: 5 over-corrections in the previous fixup. CI's exact diffs
applied verbatim:
- grisu.rs:1032 stub collapsed to single line (89 chars - fits)
- mod.rs:818,850,924 buf_storage wrapped to two lines (116 chars)
- mod.rs:956 256-variant collapsed to single line (88 chars - fits)
The flake.nix, flake.lock, and docs/ tree were local-only conveniences for reproducing Kani runs and tracking the contribution's decision history. None of them affect the verification claims, the library source, or CI (which invokes scripts/run-kani.sh directly). Removing them keeps the PR focused on the standard-library changes.
There was a problem hiding this comment.
Pull request overview
This PR adds Kani verification coverage for Challenge 28’s flt2dec targets, along with Kani-only helpers and a small Dragon digit-extraction refactor to support the proofs.
Changes:
- Adds Kani harnesses for
flt2decformatting helpers and Grisu/Dragon strategy functions. - Introduces Kani-only bignum havoc helpers and a Kani-only recursion-limit adjustment.
- Normalizes several script shebangs and adds Nix-related generated artifacts to
.gitignore.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
library/core/src/num/flt2dec/mod.rs |
Adds wrapper and digit-rendering Kani harnesses. |
library/core/src/num/flt2dec/strategy/grisu.rs |
Adds Grisu contracts, hoists round_and_weed, and adds Grisu Kani harnesses/stubs. |
library/core/src/num/flt2dec/strategy/dragon.rs |
Adds Dragon contracts, refactors digit extraction through div_rem_upto_16, and adds Dragon Kani harnesses/stubs. |
library/core/src/num/bignum.rs |
Adds Kani-only bignum helper methods for verification stubs. |
library/core/src/lib.rs |
Adds a Kani-only recursion-limit attribute. |
scripts/run-kani.sh |
Switches shebang to /usr/bin/env bash. |
scripts/run-goto-transcoder.sh |
Switches shebang to /usr/bin/env bash. |
scripts/find-contracts.sh |
Switches shebang to /usr/bin/env bash. |
scripts/check_rustc.sh |
Switches shebang to /usr/bin/env bash. |
.gitignore |
Ignores Nix/runtime verification artifacts. |
| fn arbitrary_small_decoded() -> Decoded { | ||
| let mant: u64 = kani::any(); | ||
| kani::assume(mant >= 2 && mant <= 0xFFFF); | ||
| let plus: u64 = kani::any(); | ||
| kani::assume(plus == 1 || plus == 2); | ||
| let minus: u64 = 1; | ||
| let exp: i16 = kani::any(); | ||
| kani::assume(exp >= -8 && exp <= 8); |
There was a problem hiding this comment.
Bounds documented as a new # Kani verification scope doc-section on format_shortest and format_exact in 5698c1e. Deliberately not a safety contract: the havoc stubs of Big32x40 decouple the proof's safety conclusion (no UB on buf[i] = b'0' + d) from the concrete mantissa/exponent values, so the narrow bounds are a CBMC-tractability choice rather than a precondition the real function imposes on its callers. Treating them as # Safety contract entries would incorrectly imply real callers (which feed decode_finite's full output range) must respect them.
| fn arbitrary_small_decoded() -> Decoded { | ||
| let mant: u64 = kani::any(); | ||
| let minus: u64 = kani::any(); | ||
| let plus: u64 = kani::any(); | ||
| let exp: i16 = kani::any(); | ||
| let inclusive: bool = kani::any(); | ||
| // Tight bounds: with the Loitsch-derived stubs the Fp::normalize / | ||
| // Fp::mul postconditions already abstract away the mantissa-value- | ||
| // specific behavior, so a 4-bit mantissa range is enough to | ||
| // exercise every reachable branch of the digit-emission loops. | ||
| kani::assume(mant >= 2 && mant <= 0xF); | ||
| kani::assume(minus >= 1 && minus < mant); | ||
| kani::assume(plus >= 1 && plus <= 0x7); | ||
| kani::assume(exp >= -2 && exp <= 2); |
There was a problem hiding this comment.
Bounds documented as # Kani verification scope on format_shortest_opt in 5698c1e (same framing as the dragon comments - explicitly not a safety contract). stub_fp_mul is an identity on the mantissa, so the safety conclusion is independent of the specific Fp values; the narrow symbolic range is a CBMC-tractability choice rather than a precondition the real function imposes.
| fn arbitrary_small_decoded_exact() -> Decoded { | ||
| let mant: u64 = kani::any(); | ||
| let exp: i16 = kani::any(); | ||
| let inclusive: bool = kani::any(); | ||
| kani::assume(mant >= 1 && mant <= 0xFF); | ||
| kani::assume(exp >= -4 && exp <= 4); |
There was a problem hiding this comment.
Bounds documented as # Kani verification scope on format_exact_opt in 5698c1e. Same justification as format_shortest_opt: the identity-on-f stub_fp_mul decouples the safety conclusion from the concrete Fp values, so the narrow symbolic range is a verification-time efficiency choice rather than a precondition.
…docs, contract-min buffer
Three clusters of feedback addressed in one commit:
1. f64 monomorphisations. Added four new harnesses pairing each
`to_*_str` f32 proof with an f64 sibling:
- `check_to_shortest_str_f64`, `check_to_shortest_exp_str_f64`,
`check_to_exact_exp_str_f64` (all use `MAX_SIG_DIGITS` buffer,
same shape as their f32 counterparts).
- `check_to_exact_fixed_str_f64` uses an 832-byte buffer to satisfy
`buf.len() >= maxlen` for the worst-case f64 exponent (~826 per
`estimate_max_buf_len`'s doc comment).
All four converge fast; the 10-harness flt2dec target finishes in
~3 seconds.
2. Verification-scope documentation. Added `# Kani verification scope`
doc-sections to `dragon::format_shortest`, `dragon::format_exact`,
`grisu::format_shortest_opt`, and `grisu::format_exact_opt` making
the symbolic-input subdomain explicit. The bounds are tighter than
the real `decode_finite` output range for CBMC tractability; under
the havoc stubs the safety conclusion is independent of the
mantissa/exponent values, so the narrowing is a verification-time
efficiency choice rather than a precondition the real function
imposes on its callers.
3. Contract-minimum harness buffer. Shrunk the dragon harness buffer
from `MAX_SIG_DIGITS + 1` to `MAX_SIG_DIGITS` (matching the
documented contract). `stub_round_up` now returns `None` when the
buffer is already full, mirroring the source's note that the
post-loop round-up extension at `i == MAX_SIG_DIGITS` is "possibly
impossible" - it requires both an all-nines digit pattern and the
loop to have terminated by buffer exhaustion rather than by
`down || up`, which under `CMP_CALLS`-bounded stubs is unreachable.
Also reverts unrelated tooling artifacts that leaked into earlier
commits (Nix gitignore lines, `/usr/bin/env bash` shebang changes on
four scripts).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Suppressed comments (5)
library/core/src/num/flt2dec/strategy/dragon.rs:690
- This mutable-static access also needs a
// SAFETY:justification.
unsafe {
library/core/src/num/flt2dec/mod.rs:843
- This overlooks the conditional
DecodableFloat for f16implementation atlibrary/core/src/num/flt2dec/decoder.rs:48-53. The challenge permits limiting genericTto primitive types, butf16is also a supported primitive whentarget_has_reliable_f16; add cfg-gatedf16harnesses for all four generic formatter targets.
/// Same structure as the `f32` sibling: the wrapper's buffer
/// precondition is `buf.len() >= MAX_SIG_DIGITS`, independent of float
/// bitwidth. Only `decode::<f64>` and `min_pos_norm_value::<f64>` differ
/// at the monomorphised IR; covering both primitives addresses
/// Challenge 28's "primitive types" generic-T scope.
library/core/src/num/flt2dec/strategy/dragon.rs:550
- Use the repository's
// SAFETY:marker for this unsafe access so the justification is recognized consistently.
// Safety: harness is single-threaded and resets the counter at entry.
library/core/src/num/flt2dec/strategy/dragon.rs:658
- This mutable-static access needs a
// SAFETY:comment explaining why concurrent access cannot occur.
This issue also appears on line 690 of the same file.
unsafe {
library/core/src/num/flt2dec/strategy/grisu.rs:469
- The documented verification range is stale: the harness below assumes
limit >= -10 && limit <= 10, not-8..=8. Keep the scope description aligned with the actual proof.
/// The proof in `verify::check_format_exact_opt_safety` exercises this
/// function on the symbolic subdomain `mant in 1..=0xFF`, `exp in -4..=4`,
/// `minus == 1`, `plus == 1`, with `limit in -8..=8`. Same justification
/// as `format_shortest_opt`: the identity-on-`f` `stub_fp_mul` decouples
/// the safety conclusion from concrete `Fp` values.
| // is also reachable under this restricted-coverage stub (the | ||
| // never-terminate path is an artefact, not a real behaviour). | ||
| static mut CMP_CALLS: u32 = 0; | ||
| const CMP_BUDGET: u32 = 12; // ~2 cmps/iter, bound loop well under MAX_SIG_DIGITS |
| let bound = if max_bits > cap_bits { cap_bits } else { max_bits }; | ||
| let sz: usize = crate::kani::any(); | ||
| let max_sz = (bound + digitbits - 1) / digitbits; | ||
| crate::kani::assume(sz <= max_sz); |
feliperodri
left a comment
There was a problem hiding this comment.
Summary
PR #596 adds harnesses for all 12 Challenge 28 target functions (mapping confirmed below) and passes CI. Triage of the soundness checklist is mixed: the six flt2dec/mod.rs wrapper proofs are genuinely sound and well-constructed, but the six strategy proofs — which cover the hardest unsafe code (the digit-emission loops that actually write the buffer) — depend on stubs that under-approximate the real behavior and so do not establish the very property the challenge is about (assume_init on fully-initialized buffers / lifetime-laundering safety across the real iteration space). Copilot independently flagged the same core issues.
Function coverage (12/12 have a harness)
- mod.rs:
digits_to_dec_str,digits_to_exp_str,to_shortest_str(f32/f64),to_shortest_exp_str(f32/f64),to_exact_exp_str(f32/f64),to_exact_fixed_str(f32/f64) - grisu:
format_shortest_opt,format_exact_opt,format_shortest,format_exact - dragon:
format_shortest,format_exact
Checklist triage: 0 #[cfg(not(kani))] body-swaps (clean); 0 #[requires]/#[ensures] attributes — the "safety contracts" are prose doc comments only, so there are no decorative machine-checked contracts and no autoharness dependency (flt2dec is absent from the --include-pattern allowlist in .github/workflows/kani.yml, which is fine because explicit #[kani::proof] harnesses are used). I confirmed the documented preconditions (d.mant > 0, buf.len() >= MAX_SIG_DIGITS, etc.) genuinely exist as assert! in the upstream bodies (dragon.rs format_shortest L17-22; grisu.rs format_shortest_opt L5-11), so assuming them in harnesses is legitimate (checklist item 2 OK).
Strengths (credit)
- The 6 mod.rs wrapper proofs are sound. They run the real
digits_to_dec_str/digits_to_exp_strbodies with symbolic buffers and exp/frac_digits, and pass the digit-generation callback as a genericFargument (stub_format_shortestetc.) — this is legitimate because the wrappers are generic over the callback, not a#[kani::stub]swap of real code.touch_partsforces a discriminant + payload read of every returnedPart, so an uninitializedMaybeUninit<Part>slot would surface as UB. These are non-vacuous. f64 monomorphizations were added in the follow-up commit, closing the primitive-type coverage gap. - The grisu wrapper harnesses (
check_format_*_wrapper_safety) correctly isolate the unsafe reborrow, stubbing the callees — acceptable modular reasoning provided the callees are separately verified. - The write-up is unusually transparent about its abstractions.
Blocking concerns
1. (FATAL for dragon proofs) CMP_BUDGET forces early loop termination — the buffer-write safety is never exercised. dragon.rs stub_big_cmp (diff L727-745) increments a static counter and returns Ordering::Less after 12 calls (CMP_BUDGET = 12, L725). At ~2 comparisons per digit iteration this forces the digit-emission loop to stop after ~6 writes, but dragon's real loop can emit up to MAX_SIG_DIGITS == 17 digits. The writes buf[i] = b'0' + d at indices 6..17 — exactly the assume_init/bounds obligation the challenge targets — are never reached. The in-code justification ("the real algorithm terminates at least this quickly, so any safety violation reachable under real semantics is also reachable") is asserted, not proven, and is dubious for shortest mode. This is a classic under-approximation: it can only miss bugs, not find them. Copilot raised this verbatim (dragon.rs:547). This must be replaced with an abstraction that covers comparisons through the full MAX_SIG_DIGITS boundary (e.g. a nondeterministic-but-eventually-terminating model, or #[kani::unwind]-bounded real cmp on abstracted sizes) before the dragon functions can be considered verified.
2. stub_fp_mul deterministic identity + tiny domains under-approximate grisu. grisu.rs stub_fp_mul (diff L1134-1137) models Fp::mul as identity-on-f, and arbitrary_small_decoded restricts mant to 2..=0xF, exp to -2..=2 (L1290-1305). The overflow-sensitive arithmetic in format_shortest_opt/round_and_weed (plus1 - minus1, threshold - plus1w, 2 * ulp) is precisely where real Fp magnitudes matter; a deterministic identity on a 4-bit mantissa never stresses it. Copilot flagged the same narrowness (grisu.rs:989, 998; dragon.rs:611). The author documented these as a "Kani verification scope" note and explicitly declined to broaden them, arguing the stubs decouple safety from values — but that argument is only valid if the stubs over-approximate, and both #1 and #2 do the opposite.
3. Non-kani std runtime code was modified. Two refactors change the shipped std source (not gated by #[cfg(kani)]):
dragon.rsformat_exact: the inlined subtract-ladder was replaced by a call todiv_rem_upto_16(diff L575-605). I confirmed the helper is logically byte-identical, so it is functionally equivalent — but per CLAUDE.md /general-rules.mdthis repo must not alter std runtime logic; such changes belong upstream inrust-lang/rustfirst.grisu.rsround_and_weedwas hoisted from a nested fn to a module-level fn (diff L930-1058), changing structure/visibility of real code purely to enable stubbing.
Even though both preserve behavior, they should be justified/minimized or split out, and reviewers here are typically strict about touchinglibrary/runtime code.
Non-blocking issues
bignum.rskani_havoc(diff L33-47): thesize <= ceil(max_bits/digitbits)bound constrains limb count, not bit length, whenmax_bitsisn't limb-aligned (Copilot, bignum.rs:356). Latent only because it is always called with the limb-alignedBIG_MAX_BITS, but the doc comment ("bit length bounded bymax_bits") is inaccurate.stub_round_up(dragon diff L751-773) hard-codesNoneford.len() >= MAX_SIG_DIGITS; this is entangled with the same forced-termination assumption in #1 and should be revisited alongside it.- The doc comments' "when kani#4591 lands, swap to
#[safety::requires]" plan is reasonable, but until then the preconditions are onlyassumed, not verified as contracts.
Direction
The mod.rs half is essentially done. To close out the strategy half: (a) remove the CMP_BUDGET early-exit and instead bound the loop with #[kani::unwind] over real (size-abstracted) comparisons so all reachable buf[i] writes up to MAX_SIG_DIGITS are checked; (b) make stub_fp_mul/stub_cached_power over-approximate (nondet within documented post-condition shape) rather than a fixed identity, and widen the Decoded domains toward the real decode_finite output — or explicitly encode the narrow domains as verified preconditions; (c) move the two std-body refactors upstream or gate/justify them. Until the strategy-function loops are exercised across their real iteration range, six of the twelve required functions are not soundly verified.
Challenge 28 (flt2dec): 12 of 12 functions verified via Kani
Opening as draft to invite collaborative review on two deviations from the established
verify-rust-stdpattern (called out under "Design decisions reviewers may question" below). All 12 functions verify end-to-end under Kani 0.65; the deviations are about how the proofs are structured, not whether they pass.The PR adds Kani harnesses under
library/core/src/num/flt2dec/**and two#[cfg(kani)]-only helpers underlibrary/core/src/num/bignum.rs. No production runtime logic changes apart from one small refactor indragon::format_exactthat consolidates a byte-identical inlined block into a call to the existingdiv_rem_upto_16helper.What's verified
flt2dec(module root):digits_to_dec_str,digits_to_exp_strto_shortest_str,to_shortest_exp_str,to_exact_exp_str,to_exact_fixed_str(monomorphised tof32, per the spec's "primitive types only" clause for genericT).flt2dec::strategy::grisu:format_shortest_opt,format_exact_optformat_shortest,format_exact(the lifetime-laundering wrappers).flt2dec::strategy::dragon:format_shortest,format_exact.Verification approach
to_*_strfunctions. Direct Kani proofs over symbolicDecodedand digit-buffer inputs. No stubs; the CBMC formula is small enough that the harnesses finish in ~2 seconds combined.grisu::format_shortest_opt/format_exact_opt. Stubs derived from the Loitsch PLDI 2010 paper:Fp::mulas identity on the mantissa (preserves the algorithm's required monotonic ordering of three successive multiplications),cached_powerandmax_pow10_no_more_thanas range-bounded havoc.round_and_weedis hoisted to a free function so it can be reasoned about separately.grisu::format_shortest/format_exactwrappers. Hand-written buf-synthesizing stubs for the inner_optcall and the dragon fallback. Only the wrapper's own unsafe reborrow is exercised; this is the actual unsafe surface the wrappers add over their callees.dragon::format_shortest/format_exact. Three levers: (1) tightened the symbolicDecodedinput to matchdecode_finite's real output shape (minus = 1,plus in {1, 2}); (2) stubbeddiv_rem_upto_16to return(d, x)withkani::assume(d < 10), encoding the helper's algorithmic invariantmant + plus <= scale * 10 => d < 10(Burger and Dybvig, 1996) directly; (3) bounded the digit-generation loop via aCMP_CALLSstatic counter on theBig::cmpstub. A small refactor informat_exactconsolidates a byte-identical inlined digit-extraction block into a call to the existingdiv_rem_upto_16helper, so both dragon entry points share the same stubbed extraction routine. Both proofs combined finish in under 11 seconds.Design decisions
These two are off-pattern compared to merged work in this repo (a precedent survey of merged PRs found zero prior use of doc-comment safety contracts, and zero
kani::stubpluskani::assume-postcondition stubs - onlystub_verifiedplus proven#[ensures], per thetransmute_unchecked_wrapperfamily inlibrary/core/src/intrinsics/mod.rs). Each is explained below with the reasoning; happy to iterate on either.# Safety contractsections instead of#[safety::requires]. Blocked by Kani upstream issue model-checking/kani#4591 (open, assigned, milestone Contracts): Kani 0.65 cannot apply#[kani::stub]to a function that carries#[requires], with a "Failed to find contract closure" compile error. All four contracted functions (format_shortest_opt,format_exact_opt,dragon::format_shortest,dragon::format_exact) are stubbed by at least one other harness in this PR, so the two attributes cannot coexist. Reproduced locally: dropping the doc block and adding#[safety::requires(d.mant > 0)]etc. toformat_exact_optfails to compile thecorecrate. Preconditions remain enforced at runtime viaassert!in the function body, and the strategy harnesses encode them viakani::assume. When Rewrite the coercion code to be more readable, more sound, and to reborrow when needed. rust-lang/rust#4591 lands, the migration is mechanical: drop the doc block, drop the bodyassert!s, add the attributes. Each of the four contract blocks references the upstream issue in-line.stub_div_rem_upto_16encodingd < 10askani::assume. Sound for the Challenge 28 safety mandate (no UB on theMaybeUninitbuffer): the assumed postcondition matches the helper's actual algorithmic invariantmant + plus <= scale * 10 => d < 10, proved by Burger and Dybvig (1996). The canonical alternative would be#[kani::stub_verified]with a proven#[ensures](thetransmute_unchecked_wrapperpattern). Derivingd < 10automatically through that path requires per-method#[ensures]on the entireBig32x40API plus a separately tractable proof for each; thekani_havoc/kani_sizehelpers added inbignum.rsare designed to support that follow-up. The current stub is the smallest change that closes the dragon harnesses; happy to invest instub_verifiedif reviewers prefer it as a follow-up before merge.CMP_CALLSstatic counter onstub_big_cmp. Bounds the digit-generation loop to fewer thanMAX_SIG_DIGITS = 17iterations. The unbounded path admitted by raw havoc is a stub artefact - the real algorithm terminates monotonically asmant.mul_small(10)drives the comparison - not a real algorithmic behaviour. Sound for safety, restricted in coverage. Counter is reset on each harness entry.#[cfg_attr(kani, recursion_limit = "256")]inlibrary/core/src/lib.rs- the per-harness stack of#[kani::stub]attributes exceeded the default macro-expansion limit.cfg(kani)-gated, zero impact on production builds.kani_havoc/kani_sizehelpers inlibrary/core/src/num/bignum.rs-#[cfg(kani)]-gated.dragon::format_exact: an inlined digit-extraction block that was byte-identical to the existingdiv_rem_upto_16helper is now a call to that helper. The two blocks were copy-paste duplicates; consolidating them deduplicates code regardless of verification, and incidentally lets the Kani harness stub one extraction routine for bothformat_shortestandformat_exact. No behavioural change in any build mode.Test plan
Reproduce from the repo root after building Kani per
scripts/setup/install_deps.sh:scripts/run-kani.sh(runs every harness in the workspace; the new flt2dec harnesses are:)num::flt2dec::verify::check_digits_to_dec_str,check_digits_to_exp_strnum::flt2dec::verify::check_to_shortest_str,check_to_shortest_exp_str,check_to_exact_exp_str,check_to_exact_fixed_strnum::flt2dec::strategy::grisu::verify::check_format_shortest_opt_safety,check_format_exact_opt_safetynum::flt2dec::strategy::grisu::verify::check_format_shortest_wrapper_safety,check_format_exact_wrapper_safetynum::flt2dec::strategy::dragon::verify::check_format_shortest_safety,check_format_exact_safetyVerify std libraryandVerify std library using autoharnessworkflows.References
mant + plus <= scale * 10 => d < 10invariant.fFp::mulstub.#[safety::requires]plus#[kani::stub]coexistence.