Skip to main content

supercov_engine/
probe_v2.rs

1use serde::{Deserialize, Serialize};
2use supercov_contracts::{PROBE_V2_JS_MAX_CONDITIONS, PROBE_V2_RADIX};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "lowercase")]
6pub enum ConditionValue {
7    Unreached,
8    False,
9    True,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DecisionVector {
14    pub values: Vec<ConditionValue>,
15    pub outcome: bool,
16}
17
18/// Decode the frozen base-3 probe frame without accepting aliased high digits.
19pub fn decode(condition_count: usize, encoded: u64, outcome: bool) -> Option<DecisionVector> {
20    if condition_count > PROBE_V2_JS_MAX_CONDITIONS {
21        return None;
22    }
23    let mut remaining = encoded;
24    let mut values = Vec::with_capacity(condition_count);
25    for _ in 0..condition_count {
26        let digit = remaining % u64::from(PROBE_V2_RADIX);
27        values.push(match digit {
28            0 => ConditionValue::Unreached,
29            1 => ConditionValue::False,
30            2 => ConditionValue::True,
31            _ => unreachable!("a radix-3 remainder is always 0, 1, or 2"),
32        });
33        remaining /= u64::from(PROBE_V2_RADIX);
34    }
35    (remaining == 0).then_some(DecisionVector { values, outcome })
36}
37
38#[cfg(test)]
39mod tests {
40    use serde::Deserialize;
41
42    use super::*;
43
44    #[derive(Deserialize)]
45    struct Fixture {
46        conditions: usize,
47        encoded: u64,
48        outcome: bool,
49        vector: FixtureVector,
50    }
51
52    #[derive(Deserialize)]
53    struct FixtureVector {
54        values: Vec<Option<bool>>,
55        outcome: bool,
56    }
57
58    #[test]
59    fn matches_every_language_neutral_contract_vector() {
60        let fixtures: Vec<Fixture> =
61            serde_json::from_str(include_str!("../test-assets/probe-v2/vectors.json"))
62                .expect("probe vectors must be valid JSON");
63        for fixture in fixtures {
64            let expected = DecisionVector {
65                values: fixture
66                    .vector
67                    .values
68                    .into_iter()
69                    .map(|value| match value {
70                        None => ConditionValue::Unreached,
71                        Some(false) => ConditionValue::False,
72                        Some(true) => ConditionValue::True,
73                    })
74                    .collect(),
75                outcome: fixture.vector.outcome,
76            };
77            assert_eq!(
78                decode(fixture.conditions, fixture.encoded, fixture.outcome),
79                Some(expected)
80            );
81        }
82    }
83
84    #[test]
85    fn rejects_width_and_high_digits_instead_of_aliasing() {
86        assert_eq!(decode(33, 0, false), None);
87        assert_eq!(decode(2, 9, false), None);
88    }
89}