You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@Nat subtraction silently underflows to a negative i64. The type system accepts @Nat - @Nat : @Nat and the codegen emits a plain i64.sub — no runtime check, no promotion to @Int. A @Nat local can hold a negative value at runtime despite the type's claim that it represents non-negative integers. This is a soundness issue with the refinement type system, not just a documentation gap.
Reproducer
public fn test_underflow_trap(@Nat -> @Nat)
requires(true) ensures(true) effects(pure)
{
@Nat.0 - 1
}
public fn main(@Unit -> @Int)
requires(true) ensures(true) effects(pure)
{
-- Pass 0 to underflow-prone subtraction; type system allows it,
-- runtime returns -1 bound to a @Nat local.
let @Nat = test_underflow_trap(0);
nat_to_int(@Nat.0)
}
vera check ✅ vera verify ✅ (the requires(true) ensures no contract objection) vera compile ✅ vera run prints -1.
The @Nat local holds a negative value. Any downstream code that relied on the Nat >= 0 invariant to reason about correctness now has a silent bug.
Why this matters
Vera's core selling point is contract-verified code with refinement types that the compiler checks at the border between user code and helpers. When a type system promises "this value is non-negative" and the runtime produces a negative value in that slot, every contract that transitively depends on that invariant is undermined — including contracts the verifier proved sound under the (now violated) assumption.
Concretely: an ensures(@Nat.result < array_length(arr)) contract on a helper that internally does @Nat.0 - k with an insufficient precondition can pass the verifier (Tier 1), compile, and still produce an out-of-bounds access at runtime via the negative-@Nat path. The Array.[...] indexing operator trusts its @Nat index not to be negative; a negative @Nat therefore becomes a memory-safety concern on top of a logical correctness one.
Discussion: which semantics should Vera adopt?
Four reasonable choices, each with precedent:
Runtime trap on underflow (Rust u64::checked_sub + panic, or .unwrap() default). Straightforward; minimal compile-time disruption; programs crash loudly on the bug instead of silently. Adds one compare-and-branch per @Nat subtraction.
Saturating semantics (a - b = max(0, a - b)). Documented, still type-safe, cheaper than a trap at run time. Surprising to the reader unless clearly stated.
Promote to @Int (@Nat - @Nat : @Int). Reflects the mathematical reality that Nat isn't closed under subtraction. The callee has to handle the signed result explicitly, often via a contract or conversion. Maximally safe but invasive — every subtraction site needs to be re-typed.
Reject without a proof that the result is non-negative. The verifier would need to prove @Nat.b <= @Nat.a at each subtraction site or the compile fails with a Tier-1 contract error. Closest to Vera's existing contract philosophy; closes the soundness hole at compile time rather than runtime; forces the user to think about the subtraction at every site.
Option 4 feels the most Vera-native — contract-driven, compile-time, no runtime cost. Option 1 is a reasonable Tier-3 fallback for cases where the verifier can't discharge the proof.
Related edge cases to decide alongside
@Nat division by zero currently traps at WASM level (wasm trap: integer divide by zero). Documented here for context — same class of "refinement invariant not checked at run time" issue, different direction (here the type doesn't promise anything, but the trap is opaque; see Runtime traps need Vera-native diagnostics, not raw wasmtime stack traces #516).
@Int overflow: probably WASM wrap but undocumented.
Float64 NaN / infinity in contracts: already handled (Tier 3 runtime check).
Scope
Language-level decision: pick one of the four options above. Probably worth a spec section discussing the trade-offs.
Checker change: implement the chosen enforcement.
Test: conformance test that pins the chosen semantics.
Summary
@Natsubtraction silently underflows to a negative i64. The type system accepts@Nat - @Nat : @Natand the codegen emits a plaini64.sub— no runtime check, no promotion to@Int. A@Natlocal can hold a negative value at runtime despite the type's claim that it represents non-negative integers. This is a soundness issue with the refinement type system, not just a documentation gap.Reproducer
vera check✅vera verify✅ (therequires(true)ensures no contract objection)vera compile✅vera runprints-1.The
@Natlocal holds a negative value. Any downstream code that relied on theNat >= 0invariant to reason about correctness now has a silent bug.Why this matters
Vera's core selling point is contract-verified code with refinement types that the compiler checks at the border between user code and helpers. When a type system promises "this value is non-negative" and the runtime produces a negative value in that slot, every contract that transitively depends on that invariant is undermined — including contracts the verifier proved sound under the (now violated) assumption.
Concretely: an
ensures(@Nat.result < array_length(arr))contract on a helper that internally does@Nat.0 - kwith an insufficient precondition can pass the verifier (Tier 1), compile, and still produce an out-of-bounds access at runtime via the negative-@Natpath. TheArray.[...]indexing operator trusts its@Natindex not to be negative; a negative@Nattherefore becomes a memory-safety concern on top of a logical correctness one.Discussion: which semantics should Vera adopt?
Four reasonable choices, each with precedent:
Runtime trap on underflow (Rust
u64::checked_sub+ panic, or.unwrap()default). Straightforward; minimal compile-time disruption; programs crash loudly on the bug instead of silently. Adds one compare-and-branch per@Natsubtraction.Saturating semantics (
a - b = max(0, a - b)). Documented, still type-safe, cheaper than a trap at run time. Surprising to the reader unless clearly stated.Promote to
@Int(@Nat - @Nat : @Int). Reflects the mathematical reality thatNatisn't closed under subtraction. The callee has to handle the signed result explicitly, often via a contract or conversion. Maximally safe but invasive — every subtraction site needs to be re-typed.Reject without a proof that the result is non-negative. The verifier would need to prove
@Nat.b <= @Nat.aat each subtraction site or the compile fails with a Tier-1 contract error. Closest to Vera's existing contract philosophy; closes the soundness hole at compile time rather than runtime; forces the user to think about the subtraction at every site.Option 4 feels the most Vera-native — contract-driven, compile-time, no runtime cost. Option 1 is a reasonable Tier-3 fallback for cases where the verifier can't discharge the proof.
Related edge cases to decide alongside
@Natdivision by zero currently traps at WASM level (wasm trap: integer divide by zero). Documented here for context — same class of "refinement invariant not checked at run time" issue, different direction (here the type doesn't promise anything, but the trap is opaque; see Runtime traps need Vera-native diagnostics, not raw wasmtime stack traces #516).@Intoverflow: probably WASM wrap but undocumented.Scope
Not in scope
@Intoverflow semantics (same class of question but different direction; separate decision).divide by zeroerror message (that's Runtime traps need Vera-native diagnostics, not raw wasmtime stack traces #516).