1use creusot_std::prelude::*;
4#[cfg(not(creusot))]
5use derive_more::{Display, From, Into};
6
7macro_rules! token_count {
8 ($name:ident, $doc:literal) => {
9 #[doc = $doc]
10 #[krabka_macros::model_type(
11 proof(DeepModel),
12 runtime(
13 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Display, From, Into
14 )
15 )]
16 pub struct $name(pub u64);
17 };
18}
19
20token_count!(
21 AvailableTokens,
22 "Tokens currently sitting in the bucket, available to grant."
23);
24token_count!(
25 RefillTokens,
26 "Tokens accrued since the last refill, to be added to `available` this call."
27);
28token_count!(
29 BurstCapacity,
30 "The burst cap: the maximum the bucket may hold after a refill."
31);
32token_count!(RequestedTokens, "Tokens the caller is asking to consume.");
33token_count!(
34 GrantedTokens,
35 "Tokens actually granted by a consume call (`<= requested`)."
36);
37token_count!(
38 NewAvailable,
39 "The bucket's new `available` count after a consume call commits."
40);
41
42#[cfg(creusot)]
44#[cfg_attr(test, mutants::skip)]
45#[logic(open)]
46pub fn capped(available: Int, refill: Int, burst: Int) -> Int {
54 if available + refill <= burst {
55 available + refill
56 } else {
57 burst
58 }
59}
60
61#[ensures(result.0.0@ <= requested.0@)]
63#[ensures(result.1.0@ <= burst.0@)]
64#[ensures(result.0.0@ + result.1.0@ == capped(available.0@, refill.0@, burst.0@))]
65#[ensures(result.0.0@ == if requested.0@ <= capped(available.0@, refill.0@, burst.0@) {
66 requested.0@
67} else {
68 capped(available.0@, refill.0@, burst.0@)
69})]
70#[must_use]
71pub fn plan_consume(
72 available: AvailableTokens,
73 refill: RefillTokens,
74 burst: BurstCapacity,
75 requested: RequestedTokens,
76) -> (GrantedTokens, NewAvailable) {
77 let total = available.0.saturating_add(refill.0).min(burst.0);
78 proof_assert!(total@ == capped(available.0@, refill.0@, burst.0@));
79 let grant = requested.0.min(total);
80 (GrantedTokens(grant), NewAvailable(total - grant))
81}
82
83#[cfg(test)]
84mod tests {
85 use {super::*, proptest::prelude::*};
86
87 #[yare::parameterized(
88 available = { 100, 0, 1000, 50, 50, 50 },
89 request_capped_by_available = { 100, 0, 1000, 200, 100, 0 },
90 request_after_refill = { 900, 500, 1000, 200, 200, 800 },
91 empty_bucket = { 0, 0, 1000, 100, 0, 0 },
92 saturating_refill = { u64::MAX, u64::MAX, 1000, 1000, 1000, 0 },
93 )]
94 fn plan_consume_grants_and_caps(
95 available: u64,
96 refill: u64,
97 burst: u64,
98 requested: u64,
99 grant: u64,
100 new_available: u64,
101 ) {
102 assert2::assert!(
103 plan_consume(
104 AvailableTokens(available),
105 RefillTokens(refill),
106 BurstCapacity(burst),
107 RequestedTokens(requested)
108 ) == (GrantedTokens(grant), NewAvailable(new_available))
109 );
110 }
111
112 proptest! {
113 #[test]
114 fn plan_consume_invariants(
115 available in 0u64..=u64::MAX,
116 refill in 0u64..=u64::MAX,
117 burst in 0u64..1_000_000,
118 requested in 0u64..=u64::MAX,
119 ) {
120 let (grant, new) = plan_consume(
121 AvailableTokens(available),
122 RefillTokens(refill),
123 BurstCapacity(burst),
124 RequestedTokens(requested),
125 );
126 let capped = available.saturating_add(refill).min(burst);
127 prop_assert!(grant.0 <= requested);
128 prop_assert!(grant.0 <= capped);
129 prop_assert_eq!(new.0, capped - grant.0);
130 prop_assert!(new.0 <= burst, "burst cap");
131 }
132 }
133}