diff --git a/.codecov.yml b/.codecov.yml index afc5bcd..37f41de 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,4 +1,26 @@ codecov: require_ci_to_pass: yes - bot: romac +coverage: + precision: 2 + round: nearest + range: "50...100" + + status: + project: + default: + target: auto + threshold: 5% + removed_code_behavior: adjust_base + paths: + - "itf" + patch: + default: + target: auto + threshold: 5% + paths: + - "itf" + + changes: + default: + informational: true diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 14839c0..58f3e22 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,31 +1,41 @@ name: Coverage on: - pull_request: push: branches: main + paths: + - itf/** + pull_request: + paths: + - itf/** jobs: coverage: runs-on: ubuntu-latest + defaults: + run: + working-directory: itf env: CARGO_TERM_COLOR: always steps: - - uses: actions/checkout@v3 - - name: Install Rust - uses: actions-rs/toolchain@v1 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable - override: true + toolchain: nightly + components: llvm-tools-preview + - name: Install cargo-nextest + uses: taiki-e/install-action@cargo-nextest - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - name: Install cargo-nextest - uses: taiki-e/install-action@nextest - name: Generate code coverage run: cargo llvm-cov nextest --all-features --workspace --lcov --output-path lcov.info + - name: Generate text report + run: cargo llvm-cov report - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + files: itf/lcov.info fail_ci_if_error: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c8c92..c34e078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ # CHANGELOG +## Unreleased + +- Deserialize ITF values into native Rust types with a custom deserializer + instead of having to go through `Itf` wrapper type. + ([#6](https://github.com/informalsystems/itf-rs/pull/6)) + ## v0.1.2 +*November 10th, 2023* + - Add `From where T: From` instance for `ItfBigInt` ## v0.1.1 +*November 10th, 2023* + - Add support for new `timestamp` field in meta section of ITF traces ## v0.1 diff --git a/README.md b/README.md index 6642655..ee61e63 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ Rust library for consuming [Apalache ITF Traces][itf-adr]. **Trace:** [`MissionariesAndCannibals.itf.json`](./apalache-itf/tests/fixtures/MissionariesAndCannibals.itf.json) ```rust -use serde::Deserialize; +use std::collections::{BTreeSet, BTreeMap}; -use itf::{trace_from_str, ItfMap, ItfSet}; +use serde::Deserialize; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] enum Bank { #[serde(rename = "N")] North, @@ -33,7 +33,7 @@ enum Bank { South, } -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] enum Person { #[serde(rename = "c1_OF_PERSON")] Cannibal1, @@ -51,30 +51,31 @@ enum Person { #[derive(Clone, Debug, Deserialize)] struct State { pub bank_of_boat: Bank, - pub who_is_on_bank: ItfMap>, + pub who_is_on_bank: BTreeMap>, } let data = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); -let trace: Trace = trace_from_str(data).unwrap(); +let trace: itf::Trace = itf::trace_from_str(data).unwrap(); dbg!(trace); ``` **Output:** -```rust -trace = Trace { - meta: TraceMeta { - description: None, +```rust,ignore +[itf/examples/cannibals.rs:45] trace = Trace { + meta: Meta { + format: None, + format_description: None, source: Some( "MC_MissionariesAndCannibalsTyped.tla", ), + description: None, var_types: { "bank_of_boat": "Str", "who_is_on_bank": "Str -> Set(PERSON)", }, - format: None, - format_description: None, + timestamp: None, other: {}, }, params: [], @@ -85,7 +86,7 @@ trace = Trace { loop_index: None, states: [ State { - meta: StateMeta { + meta: Meta { index: Some( 0, ), @@ -96,16 +97,16 @@ trace = Trace { who_is_on_bank: { West: {}, East: { - Missionary2, Cannibal1, Cannibal2, Missionary1, + Missionary2, }, }, }, }, State { - meta: StateMeta { + meta: Meta { index: Some( 1, ), @@ -115,18 +116,18 @@ trace = Trace { bank_of_boat: West, who_is_on_bank: { West: { - Missionary2, Cannibal2, + Missionary2, }, East: { - Missionary1, Cannibal1, + Missionary1, }, }, }, }, State { - meta: StateMeta { + meta: Meta { index: Some( 2, ), @@ -139,15 +140,15 @@ trace = Trace { Cannibal2, }, East: { - Missionary2, Cannibal1, Missionary1, + Missionary2, }, }, }, }, State { - meta: StateMeta { + meta: Meta { index: Some( 3, ), @@ -157,8 +158,8 @@ trace = Trace { bank_of_boat: West, who_is_on_bank: { West: { - Missionary1, Cannibal2, + Missionary1, Missionary2, }, East: { @@ -168,7 +169,7 @@ trace = Trace { }, }, State { - meta: StateMeta { + meta: Meta { index: Some( 4, ), @@ -177,19 +178,19 @@ trace = Trace { value: State { bank_of_boat: East, who_is_on_bank: { - East: { - Cannibal2, - Cannibal1, - }, West: { Missionary1, Missionary2, }, + East: { + Cannibal1, + Cannibal2, + }, }, }, }, State { - meta: StateMeta { + meta: Meta { index: Some( 5, ), @@ -198,13 +199,13 @@ trace = Trace { value: State { bank_of_boat: West, who_is_on_bank: { - East: {}, West: { Cannibal1, Cannibal2, Missionary1, Missionary2, }, + East: {}, }, }, }, @@ -227,7 +228,9 @@ Copyright © 2023 Informal Systems Inc. and itf-rs authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files in this repository except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 +```text +https://www.apache.org/licenses/LICENSE-2.0 +``` Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/itf/Cargo.toml b/itf/Cargo.toml index fd07a05..bdfc786 100644 --- a/itf/Cargo.toml +++ b/itf/Cargo.toml @@ -13,6 +13,7 @@ rust-version = "1.65" [dependencies] num-bigint = { version = "0.4", features = ["serde"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -thiserror = "1" +num-traits = { version = "0.2" } +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["raw_value"] } +serde_with = { version = "3.4.0" } diff --git a/itf/examples/cannibals.rs b/itf/examples/cannibals.rs new file mode 100644 index 0000000..49a25cf --- /dev/null +++ b/itf/examples/cannibals.rs @@ -0,0 +1,48 @@ +#![allow(dead_code)] + +use std::collections::{BTreeMap, BTreeSet}; + +use serde::Deserialize; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +enum Bank { + #[serde(rename = "N")] + North, + + #[serde(rename = "W")] + West, + + #[serde(rename = "E")] + East, + + #[serde(rename = "S")] + South, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +enum Person { + #[serde(rename = "c1_OF_PERSON")] + Cannibal1, + + #[serde(rename = "c2_OF_PERSON")] + Cannibal2, + + #[serde(rename = "m1_OF_PERSON")] + Missionary1, + + #[serde(rename = "m2_OF_PERSON")] + Missionary2, +} + +#[derive(Clone, Debug, Deserialize)] +struct State { + pub bank_of_boat: Bank, + pub who_is_on_bank: BTreeMap>, +} + +fn main() { + let data = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); + let trace: itf::Trace = itf::trace_from_str(data).unwrap(); + + dbg!(trace); +} diff --git a/itf/src/de.rs b/itf/src/de.rs new file mode 100644 index 0000000..a8513ec --- /dev/null +++ b/itf/src/de.rs @@ -0,0 +1,20 @@ +use serde::de::DeserializeOwned; + +use crate::Value; + +mod error; +pub use error::Error; + +mod helpers; +pub use helpers::As; +pub use helpers::Integer; + +mod deserializer; + +#[doc(hidden)] +pub fn decode_value(value: Value) -> Result +where + T: DeserializeOwned, +{ + T::deserialize(value) +} diff --git a/itf/src/de/deserializer.rs b/itf/src/de/deserializer.rs new file mode 100644 index 0000000..bbeaeef --- /dev/null +++ b/itf/src/de/deserializer.rs @@ -0,0 +1,478 @@ +use serde::de::value::{MapDeserializer, SeqDeserializer}; +use serde::de::{ + DeserializeSeed, Deserializer, EnumAccess, Expected, IntoDeserializer, Unexpected, + VariantAccess, Visitor, +}; +use serde::Deserialize; + +use crate::de::Error; +use crate::value::{BigInt, Map, Set, Tuple, Value}; + +impl Value { + fn invalid_type(&self, exp: &dyn Expected) -> E + where + E: serde::de::Error, + { + serde::de::Error::invalid_type(self.unexpected(), exp) + } + + fn unexpected(&self) -> Unexpected { + match self { + Value::Bool(b) => Unexpected::Bool(*b), + Value::Number(n) => Unexpected::Signed(*n), + Value::String(s) => Unexpected::Str(s), + Value::List(_) => Unexpected::Seq, + Value::Map(_) => Unexpected::Map, + Value::Record(_) => Unexpected::Other("record"), + Value::BigInt(_) => Unexpected::Other("bigint"), + Value::Tuple(_) => Unexpected::Other("tuple"), + Value::Set(_) => Unexpected::Other("set"), + Value::Unserializable(_) => Unexpected::Other("unserializable"), + } + } +} + +macro_rules! deserialize_number { + ($ty:ty, $to:ident, $visit:ident, $method:ident) => { + fn $method(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::Number(n) => { + let num = <$ty>::try_from(n).map_err(|_| { + serde::de::Error::invalid_type(Unexpected::Signed(n), &stringify!($ty)) + })?; + + visitor.$visit(num) + } + _ => Err(self.invalid_type(&visitor)), + } + } + }; +} + +impl<'de> IntoDeserializer<'de, Error> for Value { + type Deserializer = Self; + + fn into_deserializer(self) -> Self::Deserializer { + self + } +} + +impl<'de> Deserializer<'de> for Value { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::Bool(v) => visitor.visit_bool(v), + Value::Number(v) => visitor.visit_i64(v), + Value::String(v) => visitor.visit_string(v), + Value::BigInt(v) => visit_bigint(v, visitor), + Value::List(v) => visit_list(v, visitor), + Value::Tuple(v) => visit_tuple(v, visitor), + Value::Set(v) => visit_set(v, visitor), + Value::Record(v) => visit_record(v, visitor), + Value::Map(v) => visit_map(v, visitor), + Value::Unserializable(_) => Err(Error::UnsupportedType("unserializable")), + } + } + + deserialize_number!(i8, to_i8, visit_i8, deserialize_i8); + deserialize_number!(i16, to_i16, visit_i16, deserialize_i16); + deserialize_number!(i32, to_i32, visit_i32, deserialize_i32); + deserialize_number!(i64, to_i64, visit_i64, deserialize_i64); + deserialize_number!(i128, to_i128, visit_i128, deserialize_i128); + deserialize_number!(u8, to_u8, visit_u8, deserialize_u8); + deserialize_number!(u16, to_u16, visit_u16, deserialize_u16); + deserialize_number!(u32, to_u32, visit_u32, deserialize_u32); + deserialize_number!(u64, to_u64, visit_u64, deserialize_u64); + deserialize_number!(u128, to_u128, visit_u128, deserialize_u128); + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_bool(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::Bool(v) => visitor.visit_bool(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_string(visitor) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_string(visitor) + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::String(v) => visitor.visit_string(v), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_byte_buf(visitor) + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::String(v) => visitor.visit_string(v), + Value::List(v) => visit_list(v, visitor), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(self.invalid_type(&visitor)) + } + + fn deserialize_f32(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(self.invalid_type(&visitor)) + } + + fn deserialize_f64(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(self.invalid_type(&visitor)) + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::List(v) => visit_list(v, visitor), + Value::Tuple(v) => visit_tuple(v, visitor), + Value::Set(v) => visit_set(v, visitor), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::BigInt(v) => visit_bigint(v, visitor), + _ => self.deserialize_seq(visitor), + } + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self { + Value::Map(v) => visit_map(v, visitor), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + match self { + Value::Record(v) => visit_record(v, visitor), + _ => Err(self.invalid_type(&visitor)), + } + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + let (variant, value) = match self { + Value::Record(value) => { + let mut iter = value.into_iter(); + let (variant, value) = match iter.next() { + Some(v) => v, + None => { + return Err(serde::de::Error::invalid_value( + Unexpected::Map, + &"map with a single key", + )); + } + }; + if iter.next().is_some() { + return Err(serde::de::Error::invalid_value( + Unexpected::Map, + &"map with a single key", + )); + } + (variant, Some(value)) + } + Value::String(variant) => (variant, None), + other => { + return Err(serde::de::Error::invalid_type( + other.unexpected(), + &"string or map", + )); + } + }; + + visitor.visit_enum(EnumDeserializer { variant, value }) + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_string(visitor) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + drop(self); + visitor.visit_unit() + } +} + +fn visit_bigint<'de, V>(v: BigInt, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let (sign, digits) = v.into_inner().to_u32_digits(); + + let sign_value = match sign { + num_bigint::Sign::Minus => -1, + num_bigint::Sign::NoSign => 0, + num_bigint::Sign::Plus => 1, + }; + + let digit_value = digits + .into_iter() + .map(i64::from) + .map(Value::Number) + .collect(); + + let serialized = [Value::Number(sign_value), Value::List(digit_value)]; + + let deserializer = SeqDeserializer::new(serialized.into_iter()); + visitor.visit_seq(deserializer) +} + +fn visit_map<'de, V>(v: Map, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let mut deserializer = MapDeserializer::new(v.into_iter()); + let map = visitor.visit_map(&mut deserializer)?; + Ok(map) +} + +fn visit_record<'de, V>(record: Map, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let mut deserializer = MapDeserializer::new(record.into_iter()); + let map = visitor.visit_map(&mut deserializer)?; + Ok(map) +} + +fn visit_set<'de, V>(v: Set, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let mut deserializer = SeqDeserializer::new(v.into_iter()); + let seq = visitor.visit_seq(&mut deserializer)?; + Ok(seq) +} + +fn visit_tuple<'de, V>(v: Tuple, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let mut deserializer = SeqDeserializer::new(v.into_iter()); + let seq = visitor.visit_seq(&mut deserializer)?; + Ok(seq) +} + +fn visit_list<'de, V>(v: Vec, visitor: V) -> Result +where + V: Visitor<'de>, +{ + let mut deserializer = SeqDeserializer::new(v.into_iter()); + let seq = visitor.visit_seq(&mut deserializer)?; + Ok(seq) +} + +struct EnumDeserializer { + variant: String, + value: Option, +} + +impl<'de> EnumAccess<'de> for EnumDeserializer { + type Error = Error; + type Variant = VariantDeserializer; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Error> + where + V: DeserializeSeed<'de>, + { + let variant = self.variant.into_deserializer(); + let visitor = VariantDeserializer { value: self.value }; + seed.deserialize(variant).map(|v| (v, visitor)) + } +} + +struct VariantDeserializer { + value: Option, +} + +impl<'de> VariantAccess<'de> for VariantDeserializer { + type Error = Error; + + fn unit_variant(self) -> Result<(), Error> { + match self.value { + Some(value) => Deserialize::deserialize(value), + None => Ok(()), + } + } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: DeserializeSeed<'de>, + { + match self.value { + Some(value) => seed.deserialize(value), + None => Err(serde::de::Error::invalid_type( + Unexpected::UnitVariant, + &"newtype variant", + )), + } + } + + fn tuple_variant(self, _len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + match self.value { + Some(Value::Tuple(v)) => { + if v.is_empty() { + visitor.visit_unit() + } else { + visit_tuple(v, visitor) + } + } + // Some(Value::List(v)) => { + // if v.is_empty() { + // visitor.visit_unit() + // } else { + // visit_list(v, visitor) + // } + // } + Some(other) => Err(serde::de::Error::invalid_type( + other.unexpected(), + &"tuple variant", + )), + None => Err(serde::de::Error::invalid_type( + Unexpected::UnitVariant, + &"tuple variant", + )), + } + } + + fn struct_variant( + self, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + match self.value { + Some(Value::Record(v)) => visit_record(v, visitor), + Some(other) => Err(serde::de::Error::invalid_type( + other.unexpected(), + &"struct variant", + )), + None => Err(serde::de::Error::invalid_type( + Unexpected::UnitVariant, + &"struct variant", + )), + } + } +} diff --git a/itf/src/de/error.rs b/itf/src/de/error.rs new file mode 100644 index 0000000..0f4dd49 --- /dev/null +++ b/itf/src/de/error.rs @@ -0,0 +1,27 @@ +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Custom(String), + UnsupportedType(&'static str), +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Custom(msg) => msg.fmt(f), + Error::UnsupportedType(ty) => write!(f, "unsupported type: {ty}"), + } + } +} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Self::Custom(msg.to_string()) + } +} diff --git a/itf/src/de/helpers.rs b/itf/src/de/helpers.rs new file mode 100644 index 0000000..20dc369 --- /dev/null +++ b/itf/src/de/helpers.rs @@ -0,0 +1,73 @@ +use num_bigint::BigInt; + +pub use serde_with::As; + +/// Helper for `serde` to deserialize a `BigInt` to +/// any type which implements `TryFrom`. +/// +/// To be used in conjunction with [`As`]. +/// +/// ## Example +/// +/// ```rust +/// use std::collections::HashMap; +/// +/// use num_bigint::BigInt; +/// use serde::Deserialize; +/// +/// use itf::Trace; +/// use itf::de::{As, Integer}; +/// +/// let json = serde_json::json!([ +/// { +/// "_foo": {"#map": [[{"#bigint": "1"}, {"#bigint": "2"}]]}, +/// "typ": "Foo", +/// }, +/// { +/// "_bar": [[[{"#bigint": "1"}, {"#bigint": "2"}]]], +/// "typ": "Bar", +/// } +/// ]); +/// +/// // Deserialize as `num_bigint::BigInt` +/// #[derive(Deserialize, Debug)] +/// #[serde(tag = "typ")] +/// enum FooBarBigInt { +/// Foo { _foo: HashMap }, +/// Bar { _bar: Vec> }, +/// } +/// itf::from_value::>(json.clone()).unwrap(); +/// +/// // Deserialize as `i64` +/// #[derive(Deserialize, Debug)] +/// #[serde(tag = "typ")] +/// enum FooBarInt { +/// // try to deserialize _foo as i64, instead of BigInt +/// Foo { +/// #[serde(with = "As::>")] +/// _foo: HashMap, +/// }, +/// Bar { +/// #[serde(with = "As::>>")] +/// _bar: Vec>, +/// }, +/// } +/// itf::from_value::>(json.clone()).unwrap(); +/// +/// // Deserialize as a mix +/// #[derive(Deserialize, Debug)] +/// #[serde(tag = "typ")] +/// enum FooBarMixInt { +/// // try to deserialize _foo as i64, instead of BigInt +/// Foo { +/// #[serde(with = "As::>")] +/// _foo: HashMap, +/// }, +/// Bar { +/// #[serde(with = "As::>>")] +/// _bar: Vec>, +/// }, +/// } +/// itf::from_value::>(json.clone()).unwrap(); +/// ``` +pub type Integer = serde_with::TryFromInto; diff --git a/itf/src/error.rs b/itf/src/error.rs new file mode 100644 index 0000000..0183f35 --- /dev/null +++ b/itf/src/error.rs @@ -0,0 +1,43 @@ +/// Error type for the library. +#[derive(Debug)] +pub enum Error { + /// An error occured when deserializing the ITF-encoded JSON + Json(serde_json::Error), + + /// An error occured when decoding an ITF value into a Rust value + Decode(crate::de::Error), +} + +impl From for Error { + fn from(v: serde_json::Error) -> Self { + Self::Json(v) + } +} + +impl From for Error { + fn from(v: crate::de::Error) -> Self { + Self::Decode(v) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::Json(e) => write!(f, "JSON error: {}", e), + Error::Decode(e) => write!(f, "decoding error: {}", e), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Json(e) => Some(e), + Error::Decode(e) => Some(e), + } + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + self.source() + } +} diff --git a/itf/src/itf.rs b/itf/src/itf.rs deleted file mode 100644 index 1597ca3..0000000 --- a/itf/src/itf.rs +++ /dev/null @@ -1,423 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt, - hash::Hash, - ops::{Deref, DerefMut}, -}; - -use num_bigint::BigInt; -use serde::{de::DeserializeOwned, Deserialize}; - -pub type ItfMap = Itf>; -pub type ItfSet = Itf>; -pub type ItfTuple = Itf; -pub type ItfBigInt = Itf; -pub type ItfInt = i64; -pub type ItfBool = bool; -pub type ItfString = String; - -#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Itf(T); - -impl fmt::Debug for Itf -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Itf -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl Itf { - pub fn value(self) -> T { - self.0 - } -} - -impl Deref for Itf { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Itf { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl<'de, T> Deserialize<'de> for Itf> -where - T: Eq + Hash + Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - pub struct Set { - #[serde(rename = "#set")] - set: Vec, - } - - let set = Set::::deserialize(deserializer)?; - Ok(Self(set.set.into_iter().collect())) - } -} - -impl<'de, K, V> Deserialize<'de> for Itf> -where - K: Eq + Hash + DeserializeOwned, - V: Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - pub struct Map { - #[serde(rename = "#map")] - elements: Vec<(K, V)>, - } - - let map = Map::::deserialize(deserializer)?; - Ok(Self(map.elements.into_iter().collect())) - } -} - -impl<'de> Deserialize<'de> for Itf { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct BI { - #[serde(rename = "#bigint", with = "crate::util::serde::display_from_str")] - value: num_bigint::BigInt, - } - - #[derive(Deserialize)] - #[serde(untagged)] - enum IntOrBigInt { - Int(i64), - BigInt(BI), - } - - IntOrBigInt::deserialize(deserializer) - .map(|ib| match ib { - IntOrBigInt::Int(n) => BigInt::from(n), - IntOrBigInt::BigInt(b) => b.value, - }) - .map(Itf) - } -} - -#[derive(Deserialize)] -struct Tup { - #[serde(rename = "#tup")] - elements: Vec, -} - -macro_rules! deserialize_itf_tuple { - ($len:literal, $($n:literal $ty:ident)+) => { - impl<'de, $($ty ,)+> Deserialize<'de> for Itf<($($ty ,)+)> - where - $($ty: DeserializeOwned,)+ - { - #[allow(non_snake_case)] - fn deserialize(deserializer: De) -> Result - where - De: serde::Deserializer<'de>, - { - let mut elements = Tup::deserialize(deserializer).map(|t| t.elements)?; - - if elements.len() != $len { - return Err(serde::de::Error::custom(format_args!( - "expected tuple with {} elements but found {}", $len, elements.len() - ))); - } - - $( - let $ty: $ty = serde_json::from_value(std::mem::take(&mut elements[$n])) - .map_err(|e| serde::de::Error::custom(e))?; - )+ - - Ok(Itf(($($ty,)+))) - } - } - }; -} - -deserialize_itf_tuple!(2, 0 A 1 B); -deserialize_itf_tuple!(3, 0 A 1 B 2 C); -deserialize_itf_tuple!(4, 0 A 1 B 2 C 3 D); -deserialize_itf_tuple!(5, 0 A 1 B 2 C 3 D 4 E); -deserialize_itf_tuple!(6, 0 A 1 B 2 C 3 D 4 E 5 F); -deserialize_itf_tuple!(7, 0 A 1 B 2 C 3 D 4 E 5 F 6 G); -// deserialize_itf_tuple!(8, 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H); -// deserialize_itf_tuple!(9, 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I); -// deserialize_itf_tuple!(10, 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J); -// deserialize_itf_tuple!(11, 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J 10 K); -// deserialize_itf_tuple!(12, 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J 10 K 11 L); - -impl From for ItfBigInt -where - BigInt: From, -{ - fn from(t: T) -> Self { - Itf(BigInt::from(t)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use serde_json::json; - - #[test] - fn deserialize_set() { - let json = json!({ - "#set": [1, 2, 3, 4] - }); - - let set: ItfSet = serde_json::from_value(json).unwrap(); - let elems = [1_i64, 2, 3, 4].into_iter().collect::>(); - - assert_eq!(set.0, elems); - } - - #[test] - fn deserialize_map() { - let json = json!({ - "#map": [["hello", 1], ["world", 2]] - }); - - let set: ItfMap = serde_json::from_value(json).unwrap(); - let elems = [("hello".to_string(), 1), ("world".to_string(), 2)] - .into_iter() - .collect::>(); - - assert_eq!(set.0, elems); - } - - #[test] - fn deserialize_bigint_int() { - let json = json!(1024); - - let bigint: ItfBigInt = serde_json::from_value(json).unwrap(); - assert_eq!(bigint.0, BigInt::from(1024)); - } - - #[test] - fn deserialize_bigint() { - let json = json!({ - "#bigint": "1234567891011121314151617181920" - }); - - let bigint: ItfBigInt = serde_json::from_value(json).unwrap(); - assert_eq!(bigint.0, "1234567891011121314151617181920".parse().unwrap()); - } - - #[test] - #[should_panic(expected = "expected tuple with 3 elements but found 2")] - fn deserialize_tuple_wrong_cardinality() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - ] - }); - - let _tuple: ItfTuple<(ItfBigInt, ItfInt, ItfString)> = - serde_json::from_value(json).unwrap(); - } - - #[test] - fn deserialize_tuple_2() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - ] - }); - - let mut tuple: ItfTuple<(ItfBigInt, ItfInt)> = serde_json::from_value(json).unwrap(); - - assert_eq!( - tuple.deref().0, - Itf("1234567891011121314151617181920".parse().unwrap()), - ); - - assert_eq!(tuple.deref().1, 1234); - assert_eq!(tuple.deref_mut().1, 1234); - } - - #[test] - fn deserialize_tuple3() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - "Hello world", - ] - }); - - let tuple: ItfTuple<(ItfBigInt, ItfInt, ItfString)> = serde_json::from_value(json).unwrap(); - let tuple = tuple.value(); - - assert_eq!( - tuple, - ( - Itf("1234567891011121314151617181920".parse().unwrap()), - 1234, - "Hello world".to_string(), - ) - ); - } - - #[test] - fn deserialize_tuple4() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - "Hello world", - true - ] - }); - - let tuple: ItfTuple<(ItfBigInt, ItfInt, ItfString, ItfBool)> = - serde_json::from_value(json).unwrap(); - - assert_eq!( - tuple.0, - ( - Itf("1234567891011121314151617181920".parse().unwrap()), - 1234, - "Hello world".to_string(), - true - ) - ); - } - - #[test] - fn deserialize_tuple5() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - "Hello world", - true, - { "#set": [1, 2, 3] } - ] - }); - - let tuple: ItfTuple<(ItfBigInt, ItfInt, ItfString, ItfBool, ItfSet)> = - serde_json::from_value(json).unwrap(); - - assert_eq!( - tuple.0, - ( - Itf("1234567891011121314151617181920".parse().unwrap()), - 1234, - "Hello world".to_string(), - true, - Itf([1, 2, 3].into_iter().collect()) - ) - ); - } - - #[test] - #[allow(clippy::type_complexity)] - fn deserialize_tuple6() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - "Hello world", - true, - { "#set": [1, 2, 3] }, - { "#map": [[1, true], [2, false], [3, true]] } - ] - }); - - let tuple: ItfTuple<( - ItfBigInt, - ItfInt, - ItfString, - ItfBool, - ItfSet, - ItfMap, - )> = serde_json::from_value(json).unwrap(); - - assert_eq!( - tuple.0, - ( - Itf("1234567891011121314151617181920".parse().unwrap()), - 1234, - "Hello world".to_string(), - true, - Itf([1, 2, 3].into_iter().collect()), - Itf([(1, true), (2, false), (3, true)].into_iter().collect()) - ) - ); - } - - #[test] - #[allow(clippy::type_complexity)] - fn deserialize_tuple7() { - let json = json!({ - "#tup": [ - { "#bigint": "1234567891011121314151617181920" }, - 1234, - "Hello world", - true, - { "#set": [ 1, 2, 3 ] }, - { "#map": [ [1, true], [2, false], [3, true] ] }, - { "#tup": [ { "#bigint": "1" }, "hello"] }, - ] - }); - - let tuple: ItfTuple<( - ItfBigInt, - ItfInt, - ItfString, - ItfBool, - ItfSet, - ItfMap, - ItfTuple<(ItfBigInt, ItfString)>, - )> = serde_json::from_value(json).unwrap(); - - assert_eq!( - tuple.0, - ( - Itf("1234567891011121314151617181920".parse().unwrap()), - 1234, - "Hello world".to_string(), - true, - Itf([1, 2, 3].into_iter().collect()), - Itf([(1, true), (2, false), (3, true)].into_iter().collect()), - Itf((Itf(BigInt::from(1)), "hello".to_string())) - ) - ); - } - - #[test] - fn display() { - let s = "1234567891011121314151617181920"; - let itf: ItfBigInt = Itf(s.parse().unwrap()); - assert_eq!(format!("{}", itf), s.to_string()); - } -} diff --git a/itf/src/lib.rs b/itf/src/lib.rs index c179544..3f2ae27 100644 --- a/itf/src/lib.rs +++ b/itf/src/lib.rs @@ -1,306 +1,57 @@ -//! Library for consuming [Apalache ITF Traces](https://apalache.informal.systems/docs/adr/015adr-trace.html). -//! -//! ## Example -//! -//! **Trace:** [`MissionariesAndCannibals.itf.json`](./apalache-itf/tests/fixtures/MissionariesAndCannibals.itf.json) -//! -//! ```rust -//! use serde::Deserialize; -//! -//! use itf::{trace_from_str, ItfMap, ItfSet}; -//! -//! #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] -//! enum Bank { -//! #[serde(rename = "N")] -//! North, -//! -//! #[serde(rename = "W")] -//! West, -//! -//! #[serde(rename = "E")] -//! East, -//! -//! #[serde(rename = "S")] -//! South, -//! } -//! -//! #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] -//! enum Person { -//! #[serde(rename = "c1_OF_PERSON")] -//! Cannibal1, -//! -//! #[serde(rename = "c2_OF_PERSON")] -//! Cannibal2, -//! -//! #[serde(rename = "m1_OF_PERSON")] -//! Missionary1, -//! -//! #[serde(rename = "m2_OF_PERSON")] -//! Missionary2, -//! } -//! -//! #[derive(Clone, Debug, Deserialize)] -//! struct State { -//! pub bank_of_boat: Bank, -//! pub who_is_on_bank: ItfMap>, -//! } -//! -//! let data = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); -//! let trace: Trace = trace_from_str(data).unwrap(); -//! -//! dbg!(trace); -//! ``` -//! -//! **Output:** -//! -//! ```rust -//! trace = Trace { -//! meta: TraceMeta { -//! description: None, -//! source: Some( -//! "MC_MissionariesAndCannibalsTyped.tla", -//! ), -//! var_types: { -//! "bank_of_boat": "Str", -//! "who_is_on_bank": "Str -> Set(PERSON)", -//! }, -//! format: None, -//! format_description: None, -//! other: {}, -//! }, -//! params: [], -//! vars: [ -//! "bank_of_boat", -//! "who_is_on_bank", -//! ], -//! loop_index: None, -//! states: [ -//! State { -//! meta: StateMeta { -//! index: Some( -//! 0, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: East, -//! who_is_on_bank: { -//! West: {}, -//! East: { -//! Missionary2, -//! Cannibal1, -//! Cannibal2, -//! Missionary1, -//! }, -//! }, -//! }, -//! }, -//! State { -//! meta: StateMeta { -//! index: Some( -//! 1, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: West, -//! who_is_on_bank: { -//! West: { -//! Missionary2, -//! Cannibal2, -//! }, -//! East: { -//! Missionary1, -//! Cannibal1, -//! }, -//! }, -//! }, -//! }, -//! State { -//! meta: StateMeta { -//! index: Some( -//! 2, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: East, -//! who_is_on_bank: { -//! West: { -//! Cannibal2, -//! }, -//! East: { -//! Missionary2, -//! Cannibal1, -//! Missionary1, -//! }, -//! }, -//! }, -//! }, -//! State { -//! meta: StateMeta { -//! index: Some( -//! 3, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: West, -//! who_is_on_bank: { -//! West: { -//! Missionary1, -//! Cannibal2, -//! Missionary2, -//! }, -//! East: { -//! Cannibal1, -//! }, -//! }, -//! }, -//! }, -//! State { -//! meta: StateMeta { -//! index: Some( -//! 4, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: East, -//! who_is_on_bank: { -//! East: { -//! Cannibal2, -//! Cannibal1, -//! }, -//! West: { -//! Missionary1, -//! Missionary2, -//! }, -//! }, -//! }, -//! }, -//! State { -//! meta: StateMeta { -//! index: Some( -//! 5, -//! ), -//! other: {}, -//! }, -//! value: State { -//! bank_of_boat: West, -//! who_is_on_bank: { -//! East: {}, -//! West: { -//! Cannibal1, -//! Cannibal2, -//! Missionary1, -//! Missionary2, -//! }, -//! }, -//! }, -//! }, -//! ], -//! } -//! ``` +#![doc = include_str!("../../README.md")] -mod util; +use serde::de::DeserializeOwned; +use serde::Deserialize; -mod meta; -pub use meta::*; +pub mod de; +pub mod error; +pub mod state; +pub mod trace; -mod itf; -pub use itf::*; +pub use error::Error; +pub use state::State; +pub use trace::Trace; -mod trace; -use serde::{de::DeserializeOwned, Deserialize}; -pub use trace::*; +#[doc(hidden)] +pub mod value; -use serde_json::Result; +#[doc(hidden)] +pub use value::Value; -pub fn trace_from_str<'a, State>(s: &'a str) -> Result> +/// Deserialize a [`Trace`] over states `S` from an ITF JSON string. +pub fn trace_from_str(str: &str) -> Result, Error> where - State: Deserialize<'a>, + S: for<'de> Deserialize<'de>, { - serde_json::from_str(s) + let trace_value: Trace = serde_json::from_str(str)?; + trace_value.decode() } -pub fn trace_from_slice<'a, State>(s: &'a [u8]) -> Result> +/// Deserialize a [`Trace`] over states `S` from an ITF JSON [`serde_json::Value`]. +pub fn trace_from_value(value: serde_json::Value) -> Result, Error> where - State: Deserialize<'a>, + S: DeserializeOwned, { - serde_json::from_slice(s) + let trace_value: Trace = serde_json::from_value(value)?; + trace_value.decode() } -pub fn trace_from_value(v: serde_json::Value) -> Result> +/// Deserialize an ITF-encoded expression `S` from an ITF JSON string. +pub fn from_str(str: &str) -> Result where - State: DeserializeOwned, + S: for<'de> Deserialize<'de>, { - serde_json::from_value(v) + let value: Value = serde_json::from_str(str)?; + let data = S::deserialize(value)?; + Ok(data) } -pub fn trace_from_reader(r: R) -> Result> +/// Deserialize an ITF-encoded expression `S` from an ITF JSON [`serde_json::Value`]. +pub fn from_value(value: serde_json::Value) -> Result where - State: DeserializeOwned, - R: std::io::Read, + S: DeserializeOwned, { - serde_json::from_reader(r) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] - enum Bank { - #[serde(rename = "N")] - North, - #[serde(rename = "W")] - West, - #[serde(rename = "E")] - East, - #[serde(rename = "S")] - South, - } - - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] - enum Person { - #[serde(rename = "c1_OF_PERSON")] - Cannibal1, - #[serde(rename = "c2_OF_PERSON")] - Cannibal2, - #[serde(rename = "m1_OF_PERSON")] - Missionary1, - #[serde(rename = "m2_OF_PERSON")] - Missionary2, - } - - #[derive(Clone, Debug, Deserialize)] - #[allow(dead_code)] - struct State { - pub bank_of_boat: Bank, - pub who_is_on_bank: ItfMap>, - } - - const DATA: &str = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); - - #[test] - fn from_str() { - let _trace = trace_from_str::(DATA).unwrap(); - } - - #[test] - fn from_slice() { - let _trace = trace_from_slice::(DATA.as_bytes()).unwrap(); - } - - #[test] - fn from_value() { - let value = serde_json::from_str(DATA).unwrap(); - let _trace = trace_from_value::(value).unwrap(); - } - - #[test] - fn from_reader() { - let _trace = trace_from_reader::(DATA.as_bytes()).unwrap(); - } + let trace_value: Value = serde_json::from_value(value)?; + let s = S::deserialize(trace_value)?; + Ok(s) } diff --git a/itf/src/meta.rs b/itf/src/meta.rs deleted file mode 100644 index 144b6d0..0000000 --- a/itf/src/meta.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct TraceMeta { - #[serde(default)] - pub format: Option, - - #[serde(rename = "format-description")] - pub format_description: Option, - - #[serde(default)] - pub source: Option, - - #[serde(default)] - pub description: Option, - - #[serde(default, rename = "varTypes")] - pub var_types: HashMap, - - #[serde(default)] - pub timestamp: Option, - - #[serde(flatten)] - pub other: HashMap, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct StateMeta { - #[serde(default)] - pub index: Option, - - #[serde(flatten)] - pub other: HashMap, -} diff --git a/itf/src/state.rs b/itf/src/state.rs new file mode 100644 index 0000000..205a97d --- /dev/null +++ b/itf/src/state.rs @@ -0,0 +1,90 @@ +use std::collections::BTreeMap; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::error::Error; +use crate::value::Value; + +/// Metada for an ITF [`State`]. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Meta { + #[serde(default)] + pub index: Option, + + #[serde(flatten)] + pub other: BTreeMap, +} + +/// An ITF state of type `S`. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct State { + #[serde(rename = "#meta")] + pub meta: Meta, + + #[serde(flatten)] + pub value: S, +} + +impl State { + pub fn decode(self) -> Result, Error> + where + S: DeserializeOwned, + { + let value: Value = serde_json::from_value(self.value)?; + let inner: S = crate::de::decode_value(value)?; + + Ok(State { + meta: self.meta, + value: inner, + }) + } +} + +impl State { + pub fn decode(self) -> Result, Error> + where + S: DeserializeOwned, + { + let inner: S = crate::de::decode_value(self.value)?; + + Ok(State { + meta: self.meta, + value: inner, + }) + } +} + +// use serde_json::value::RawValue; +// +// impl State> { +// pub fn decode(self) -> Result, Error> +// where +// S: DeserializeOwned, +// { +// let value: Value = serde_json::from_str(self.value.get())?; +// dbg!(&value); +// let inner: S = crate::de::decode_value(value)?; +// +// Ok(State { +// meta: self.meta, +// value: inner, +// }) +// } +// } +// +// impl<'a> State<&'a RawValue> { +// pub fn decode(self) -> Result, Error> +// where +// S: DeserializeOwned, +// { +// let value: Value = serde_json::from_str(self.value.get())?; +// dbg!(&value); +// let inner: S = crate::de::decode_value(value)?; +// +// Ok(State { +// meta: self.meta, +// value: inner, +// }) +// } +// } diff --git a/itf/src/trace.rs b/itf/src/trace.rs index 21a5a65..caf63f7 100644 --- a/itf/src/trace.rs +++ b/itf/src/trace.rs @@ -1,20 +1,42 @@ -use serde::Deserialize; +use std::collections::BTreeMap; -use crate::{StateMeta, TraceMeta}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; -#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)] -pub struct State { - #[serde(rename = "#meta")] - pub meta: StateMeta, +use crate::error::Error; +use crate::state::State; +use crate::value::Value; + +/// Metadata for an ITF [`Trace`]. +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Meta { + #[serde(default)] + pub format: Option, + + #[serde(rename = "format-description")] + pub format_description: Option, + + #[serde(default)] + pub source: Option, + + #[serde(default)] + pub description: Option, + + #[serde(default, rename = "varTypes")] + pub var_types: BTreeMap, + + #[serde(default)] + pub timestamp: Option, #[serde(flatten)] - pub value: S, + pub other: BTreeMap, } -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +/// An ITF trace over states of type `S`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Trace { #[serde(rename = "#meta")] - pub meta: TraceMeta, + pub meta: Meta, #[serde(default)] pub params: Vec, @@ -28,32 +50,88 @@ pub struct Trace { pub states: Vec>, } -impl Default for Trace { - fn default() -> Self { - Self { - meta: Default::default(), - params: Default::default(), - vars: Default::default(), - loop_index: Default::default(), - states: Default::default(), - } +impl Trace { + pub fn decode(self) -> Result, Error> + where + S: DeserializeOwned, + { + let states = self + .states + .into_iter() + .map(|state| state.decode()) + .collect::, _>>()?; + + Ok(Trace { + meta: self.meta, + params: self.params, + vars: self.vars, + loop_index: self.loop_index, + states, + }) } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn trace_default() { - #[derive(Debug, PartialEq, Eq)] - struct S; // no need for `Default derive` - - let t: Trace = Trace::default(); - assert_eq!(t.meta, TraceMeta::default()); - assert_eq!(t.params, Vec::::new()); - assert_eq!(t.vars, Vec::::new()); - assert_eq!(t.loop_index, None); - assert_eq!(t.states, Vec::>::new()); +impl Trace { + pub fn decode(self) -> Result, Error> + where + S: DeserializeOwned, + { + let states = self + .states + .into_iter() + .map(|state| state.decode()) + .collect::, _>>()?; + + Ok(Trace { + meta: self.meta, + params: self.params, + vars: self.vars, + loop_index: self.loop_index, + states, + }) } } + +// use serde_json::value::RawValue; +// +// impl Trace> { +// pub fn decode(self) -> Result, Error> +// where +// S: DeserializeOwned, +// { +// let states = self +// .states +// .into_iter() +// .map(|state| state.decode()) +// .collect::, _>>()?; +// +// Ok(Trace { +// meta: self.meta, +// params: self.params, +// vars: self.vars, +// loop_index: self.loop_index, +// states, +// }) +// } +// } +// +// impl<'a> Trace<&'a RawValue> { +// pub fn decode(self) -> Result, Error> +// where +// S: DeserializeOwned, +// { +// let states = self +// .states +// .into_iter() +// .map(|state| state.decode()) +// .collect::, _>>()?; +// +// Ok(Trace { +// meta: self.meta, +// params: self.params, +// vars: self.vars, +// loop_index: self.loop_index, +// states, +// }) +// } +// } diff --git a/itf/src/util.rs b/itf/src/util.rs deleted file mode 100644 index f1c1f03..0000000 --- a/itf/src/util.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod serde { - pub mod display_from_str { - use std::{fmt::Display, str::FromStr}; - - use serde::{de, Deserialize, Deserializer}; - - pub fn deserialize<'de, D, T, E>(deserializer: D) -> Result - where - D: Deserializer<'de>, - T: FromStr, - E: Display, - { - let s = String::deserialize(deserializer)?; - FromStr::from_str(&s).map_err(de::Error::custom) - } - } -} diff --git a/itf/src/value.rs b/itf/src/value.rs new file mode 100644 index 0000000..174a163 --- /dev/null +++ b/itf/src/value.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; + +mod bigint; +mod map; +mod set; +mod tuple; +mod unserializable; + +pub use bigint::BigInt; +pub use map::Map; +pub use set::Set; +pub use tuple::Tuple; +pub use unserializable::Unserializable; + +/// An ITF value, as per the [Apalache ITF format][itf-spec] specification. +/// +/// This enum is hidden from the documentation, as it is not meant to be used directly +/// because of pitfalls documented in [this PR][pitfalls]. +/// +/// [itf-spec]: https://apalache.informal.systems/docs/adr/015adr-trace.html +/// [pitfalls]: https://github.com/informalsystems/itf-rs/pull/6#issuecomment-1817860601 +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(untagged)] +#[doc(hidden)] +pub enum Value { + /// A JSON Boolean: either `false` or `true`. + Bool(bool), + + /// A JSON number literal, e.g., `42`. + Number(i64), + + /// A JSON string literal, e.g., `"hello"`. + /// + /// TLA+ strings are written as strings in this format. + String(String), + + /// A big integer of the following form: `{ "#bigint": "[-][0-9]+" }`. + /// + /// We are using this format, as many JSON parsers impose limits + /// on integer values, see RFC7159. + /// + /// Big and small integers must be written in this format. + BigInt(BigInt), + + /// A list of the form `[ , ..., ]`. + /// + /// A list is just a JSON array. + /// TLA+ sequences are written as lists in this format. + List(Vec), + + /// A tuple of the form `{ "#tup": [ , ..., ] }`. + /// + /// There is no strict rule about when to use sequences or tuples. + /// Apalache differentiates between tuples and sequences, and it may produce both forms of expressions. + Tuple(Tuple), + + /// A set of the form `{ "#set": [ , ..., ] }`. + /// + /// A set is different from a list in that it does not assume any ordering of its elements. + /// However, it is only a syntax form in our format. + /// Apalache distinguishes between sets and lists and thus it will output sets in the set form. + /// Other tools may interpret sets as lists. + Set(Set), + + /// A map of the form `{ "#map": [ [ , ], ..., [ , ] ] }`. + /// + /// That is, a map holds a JSON array of two-element arrays. + /// Each two-element array p is interpreted as follows: + /// `p[0]` is the map key and `p[1]` is the map value. + /// + /// Importantly, a key may be an arbitrary expression. + /// It does not have to be a string or an integer. + /// TLA+ functions are written as maps in this format. + Map(Map), + + /// A record of the form `{ "field1": , ..., "fieldN": }`. + /// + /// A record is just a JSON object. Field names should not start with `#` and + /// hence should not pose any collision with other constructs. + /// TLA+ records are written as records in this format. + Record(Map), + + /// An expression that cannot be serialized: `{ "#unserializable": "" }`. + /// + /// For instance, the set of all integers is represented with `{ "#unserializable": "Int" }`. + /// This should be a very rare expression, which should not occur in normal traces. + /// Usually, it indicates some form of an error. + Unserializable(Unserializable), +} diff --git a/itf/src/value/bigint.rs b/itf/src/value/bigint.rs new file mode 100644 index 0000000..9ca483b --- /dev/null +++ b/itf/src/value/bigint.rs @@ -0,0 +1,67 @@ +use core::fmt; + +use serde::ser::SerializeStruct; +use serde::{Deserialize, Serialize}; + +/// A big integer of the following form: `{ "#bigint": "[-][0-9]+" }`. +/// +/// We are using this format, as many JSON parsers impose limits +/// on integer values, see RFC7159. +/// +/// Big and small integers must be written in this format. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BigInt(num_bigint::BigInt); + +impl BigInt { + pub fn new(value: impl Into) -> Self { + Self(value.into()) + } + + pub fn get(&self) -> &num_bigint::BigInt { + &self.0 + } + + pub fn into_inner(self) -> num_bigint::BigInt { + self.0 + } +} + +impl fmt::Debug for BigInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for BigInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Serialize for BigInt { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("BigInt", 1)?; + s.serialize_field("#bigint", &self.to_string())?; + s.end() + } +} + +impl<'de> Deserialize<'de> for BigInt { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct BigInt { + #[serde(rename = "#bigint")] + bigint: String, + } + + let inner = BigInt::deserialize(deserializer)?; + let bigint = inner.bigint.parse().map_err(serde::de::Error::custom)?; + Ok(Self(bigint)) + } +} diff --git a/itf/src/value/map.rs b/itf/src/value/map.rs new file mode 100644 index 0000000..593ac96 --- /dev/null +++ b/itf/src/value/map.rs @@ -0,0 +1,134 @@ +use core::fmt; +use std::collections::BTreeMap; + +/// A map of the form `{ "#map": [ [ , ], ..., [ , ] ] }`. +/// +/// That is, a map holds a JSON array of two-element arrays. +/// Each two-element array p is interpreted as follows: +/// - `p[0]` is the map key +/// - `p[1]` is the map value +/// +/// Importantly, a key may be an arbitrary expression. +/// It does not have to be a string or an integer. +/// +/// TLA+ functions are written as maps in this format. +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Map { + map: BTreeMap, +} + +impl Map { + pub fn new(map: BTreeMap) -> Self { + Self { map } + } + + pub fn iter(&self) -> impl Iterator { + self.map.iter() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + pub fn len(&self) -> usize { + self.map.len() + } +} + +impl IntoIterator for Map { + type Item = (K, V); + type IntoIter = std::collections::btree_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.map.into_iter() + } +} + +impl fmt::Debug for Map +where + K: fmt::Debug, + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.map.fmt(f) + } +} + +use serde::ser::{Serialize, SerializeMap, Serializer}; +use serde::Deserialize; + +use crate::value::Value; + +/// Serialize into a JSON object of this form: +/// +///```ignore +/// { +/// "#map": [ +/// [ , ], +/// ..., +/// [ , ] +/// ] +/// } +/// ``` +impl Serialize for Map +where + K: Serialize, + V: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // let mut pairs = serializer.serialize_seq(Some(self.map.len()))?; + // + // for (key, value) in &self.map { + // let mut pair = serializer.serialize_tuple(2)?; + // pair.serialize_element(key)?; + // pair.serialize_element(value); + // } + // + // let pairs = pairs.end()?; + + let pairs = self.map.iter().collect::>(); + + let mut object = serializer.serialize_map(Some(1))?; + object.serialize_entry("#map", &pairs)?; + object.end() + } +} + +impl<'de, V> Deserialize<'de> for Map +where + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerMap { + #[serde(rename = "#map")] + map: Vec<(Value, V)>, + } + + let map = InnerMap::deserialize(deserializer)? + .map + .into_iter() + .collect(); + + Ok(Map { map }) + } +} + +impl<'de, V> Deserialize<'de> for Map +where + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let map = BTreeMap::::deserialize(deserializer)?; + Ok(Map { map }) + } +} diff --git a/itf/src/value/set.rs b/itf/src/value/set.rs new file mode 100644 index 0000000..0242a02 --- /dev/null +++ b/itf/src/value/set.rs @@ -0,0 +1,82 @@ +use core::fmt; +use std::collections::BTreeSet; + +/// A set of the form `{ "#set": [ , ..., ] }`. +/// +/// A set is different from a list in that it does not assume any ordering of its elements. +/// However, it is only a syntax form in our format. +/// Apalache distinguishes between sets and lists and thus it will output sets in the set form. +/// Other tools may interpret sets as lists. +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Set { + set: BTreeSet, +} + +impl Set { + pub fn new() -> Self { + Self { + set: BTreeSet::new(), + } + } + + pub fn iter(&self) -> impl Iterator { + self.set.iter() + } +} + +impl IntoIterator for Set { + type Item = V; + type IntoIter = std::collections::btree_set::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.set.into_iter() + } +} + +impl fmt::Debug for Set +where + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.set.fmt(f) + } +} + +use serde::ser::{Serialize, SerializeMap, Serializer}; +use serde::Deserialize; + +impl Serialize for Set +where + V: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let elements = self.set.iter().collect::>(); + + let mut object = serializer.serialize_map(Some(1))?; + object.serialize_entry("#set", &elements)?; + object.end() + } +} + +impl<'de, V> Deserialize<'de> for Set +where + V: Deserialize<'de> + Ord, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerSet { + #[serde(rename = "#set")] + set: BTreeSet, + } + + let set = InnerSet::deserialize(deserializer)?.set; + + Ok(Set { set }) + } +} diff --git a/itf/src/value/tuple.rs b/itf/src/value/tuple.rs new file mode 100644 index 0000000..22fbb3b --- /dev/null +++ b/itf/src/value/tuple.rs @@ -0,0 +1,87 @@ +use core::fmt; + +/// A tuple of the form `{ "#tup": [ , ..., ] }`. +/// +/// There is no strict rule about when to use sequences or tuples. +/// Apalache differentiates between tuples and sequences, and it may produce both forms of expressions. +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Tuple { + elements: Vec, +} + +impl Tuple { + pub fn new() -> Self { + Self { + elements: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.elements.len() + } + + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.elements.iter() + } +} + +impl IntoIterator for Tuple { + type Item = V; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.elements.into_iter() + } +} + +impl fmt::Debug for Tuple +where + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.elements.fmt(f) + } +} + +use serde::ser::{Serialize, SerializeMap, Serializer}; +use serde::Deserialize; + +impl Serialize for Tuple +where + V: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let elements = self.elements.iter().collect::>(); + + let mut object = serializer.serialize_map(Some(1))?; + object.serialize_entry("#tup", &elements)?; + object.end() + } +} + +impl<'de, V> Deserialize<'de> for Tuple +where + V: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerTuple { + #[serde(rename = "#tup")] + elements: Vec, + } + + let elements = InnerTuple::deserialize(deserializer)?.elements; + + Ok(Tuple { elements }) + } +} diff --git a/itf/src/value/unserializable.rs b/itf/src/value/unserializable.rs new file mode 100644 index 0000000..625bab6 --- /dev/null +++ b/itf/src/value/unserializable.rs @@ -0,0 +1,38 @@ +/// An expression that cannot be serialized: `{ "#unserializable": "" }`. +/// +/// For instance, the set of all integers is represented with `{ "#unserializable": "Int" }`. +/// This should be a very rare expression, which should not occur in normal traces. +/// Usually, it indicates some form of an error. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Unserializable(String); + +use serde::ser::{Serialize, SerializeMap, Serializer}; +use serde::Deserialize; + +impl Serialize for Unserializable { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut object = serializer.serialize_map(Some(1))?; + object.serialize_entry("#unserializable", &self.0)?; + object.end() + } +} + +impl<'de> Deserialize<'de> for Unserializable { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + #[serde(rename = "#unserializable")] + string: String, + } + + let inner = Inner::deserialize(deserializer)?.string; + + Ok(Self(inner)) + } +} diff --git a/itf/tests/decide_non_proposer.rs b/itf/tests/decide_non_proposer.rs new file mode 100644 index 0000000..0470344 --- /dev/null +++ b/itf/tests/decide_non_proposer.rs @@ -0,0 +1,235 @@ +#![allow(dead_code)] + +use std::collections::BTreeMap; +use std::result::Result as StdResult; + +use serde::de::IntoDeserializer; +use serde::Deserialize; + +type Address = String; +type Value = String; +type Step = String; +type Round = i64; +type Height = i64; + +#[derive(Clone, Debug, Deserialize)] +enum Timeout { + #[serde(rename = "timeoutPrevote")] + Prevote, + + #[serde(rename = "timeoutPrecommit")] + Precommit, + + #[serde(rename = "timeoutPropose")] + Propose, +} + +#[derive(Clone, Debug, Deserialize)] +struct State { + system: System, + + #[serde(rename = "_Event")] + event: Event, + + #[serde(rename = "_Result")] + result: Result, +} + +#[derive(Clone, Debug, Deserialize)] +struct System(BTreeMap); + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "name")] +enum Event { + Initial, + NewRound { + height: Height, + round: Round, + }, + Proposal { + height: Height, + round: Round, + value: Value, + }, + ProposalAndPolkaAndValid { + height: Height, + round: Round, + value: Value, + }, + ProposalAndCommitAndValid { + height: Height, + round: Round, + value: Value, + }, + NewHeight { + height: Height, + round: Round, + }, + NewRoundProposer { + height: Height, + round: Round, + value: Value, + }, + PolkaNil { + height: Height, + round: Round, + value: Value, + }, + PolkaAny { + height: Height, + round: Round, + value: Value, + }, + PrecommitAny { + height: Height, + round: Round, + value: Value, + }, + TimeoutPrevote { + height: Height, + round: Round, + }, + TimeoutPrecommit { + height: Height, + round: Round, + value: Value, + }, + TimeoutPropose { + height: Height, + round: Round, + value: Value, + }, + ProposalInvalid { + height: Height, + round: Round, + }, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Result { + name: String, + #[serde(deserialize_with = "proposal_or_none")] + proposal: Option, + #[serde(deserialize_with = "vote_message_or_none")] + vote_message: Option, + #[serde(deserialize_with = "empty_string_as_none")] + timeout: Option, + #[serde(deserialize_with = "empty_string_as_none")] + decided: Option, + #[serde(deserialize_with = "minus_one_as_none")] + skip_round: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Proposal { + src: Address, + height: Height, + round: Round, + proposal: Value, + valid_round: Round, +} + +impl Proposal { + fn is_empty(&self) -> bool { + self.src.is_empty() + && self.proposal.is_empty() + && self.height == -1 + && self.round == -1 + && self.valid_round == -1 + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct VoteMessage { + src: Address, + height: Height, + round: Round, + step: Step, + id: Value, +} + +impl VoteMessage { + fn is_empty(&self) -> bool { + self.src.is_empty() + && self.id.is_empty() + && self.height == -1 + && self.round == -1 + && self.step.is_empty() + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConsensusState { + p: Address, + height: Height, + round: Round, + step: Step, + + #[serde(deserialize_with = "minus_one_as_none")] + locked_round: Option, + #[serde(deserialize_with = "empty_string_as_none")] + locked_value: Option, + #[serde(deserialize_with = "minus_one_as_none")] + valid_round: Option, + #[serde(deserialize_with = "empty_string_as_none")] + valid_value: Option, +} + +fn empty_string_as_none<'de, D, T>(de: D) -> StdResult, D::Error> +where + D: serde::Deserializer<'de>, + T: serde::Deserialize<'de>, +{ + let opt = Option::::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => T::deserialize(s.into_deserializer()).map(Some), + } +} + +fn minus_one_as_none<'de, D, T>(de: D) -> StdResult, D::Error> +where + D: serde::Deserializer<'de>, + T: serde::Deserialize<'de>, +{ + let opt = Option::::deserialize(de)?; + match opt { + None | Some(-1) => Ok(None), + Some(i) => T::deserialize(i.into_deserializer()).map(Some), + } +} + +fn proposal_or_none<'de, D>(de: D) -> StdResult, D::Error> +where + D: serde::Deserializer<'de>, +{ + let proposal = Proposal::deserialize(de)?; + if proposal.is_empty() { + Ok(None) + } else { + Ok(Some(proposal)) + } +} + +fn vote_message_or_none<'de, D>(de: D) -> StdResult, D::Error> +where + D: serde::Deserializer<'de>, +{ + let vote_message = VoteMessage::deserialize(de)?; + if vote_message.is_empty() { + Ok(None) + } else { + Ok(Some(vote_message)) + } +} + +#[test] +fn deserialize() { + let data = include_str!("../tests/fixtures/DecideNonProposerTest0.itf.json"); + let trace = itf::trace_from_str::(data).unwrap(); + dbg!(trace); +} diff --git a/itf/tests/fixtures/DecideNonProposerTest0.itf.json b/itf/tests/fixtures/DecideNonProposerTest0.itf.json new file mode 100644 index 0000000..0ecfbc5 --- /dev/null +++ b/itf/tests/fixtures/DecideNonProposerTest0.itf.json @@ -0,0 +1,1781 @@ +{ + "#meta": { + "format": "ITF", + "format-description": "https://apalache.informal.systems/docs/adr/015adr-trace.html", + "source": "consensus.qnt", + "status": "passed", + "description": "Created by Quint on Wed Oct 25 2023 15:38:28 GMT+0200 (Central European Summer Time)", + "timestamp": 1698241108633 + }, + "vars": [ + "system", + "_Event", + "_Result" + ], + "states": [ + { + "#meta": { + "index": 0 + }, + "_Event": { + "height": -1, + "name": "Initial", + "round": -1, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "newRound", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 1 + }, + "_Event": { + "height": 1, + "name": "NewRound", + "round": 0, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 2 + }, + "_Event": { + "height": 1, + "name": "NewRound", + "round": 0, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 3 + }, + "_Event": { + "height": 1, + "name": "Proposal", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 1, + "id": "block", + "round": 0, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 4 + }, + "_Event": { + "height": 1, + "name": "Proposal", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 1, + "id": "block", + "round": 0, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 5 + }, + "_Event": { + "height": 1, + "name": "ProposalAndPolkaAndValid", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 1, + "id": "block", + "round": 0, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": 0, + "lockedValue": "block", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": 0, + "validValue": "block" + } + ] + ] + } + }, + { + "#meta": { + "index": 6 + }, + "_Event": { + "height": 1, + "name": "ProposalAndPolkaAndValid", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 1, + "id": "block", + "round": 0, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": 0, + "lockedValue": "block", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": 0, + "validValue": "block" + } + ] + ] + } + }, + { + "#meta": { + "index": 7 + }, + "_Event": { + "height": 1, + "name": "ProposalAndCommitAndValid", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "block", + "name": "decided", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": 0, + "lockedValue": "block", + "p": "Josef", + "round": 0, + "step": "decided", + "validRound": 0, + "validValue": "block" + } + ] + ] + } + }, + { + "#meta": { + "index": 8 + }, + "_Event": { + "height": 1, + "name": "ProposalAndCommitAndValid", + "round": 0, + "value": "block", + "vr": -1 + }, + "_Result": { + "decided": "block", + "name": "decided", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 1, + "lockedRound": 0, + "lockedValue": "block", + "p": "Josef", + "round": 0, + "step": "decided", + "validRound": 0, + "validValue": "block" + } + ] + ] + } + }, + { + "#meta": { + "index": 9 + }, + "_Event": { + "height": 2, + "name": "NewHeight", + "round": 0, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "newRound", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 10 + }, + "_Event": { + "height": 2, + "name": "NewHeight", + "round": 0, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "newRound", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 11 + }, + "_Event": { + "height": 2, + "name": "NewRoundProposer", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "proposal", + "proposal": { + "height": 2, + "proposal": "nextBlock", + "round": 0, + "src": "Josef", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 12 + }, + "_Event": { + "height": 2, + "name": "NewRoundProposer", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "proposal", + "proposal": { + "height": 2, + "proposal": "nextBlock", + "round": 0, + "src": "Josef", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 13 + }, + "_Event": { + "height": 2, + "name": "Proposal", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nextBlock", + "round": 0, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 14 + }, + "_Event": { + "height": 2, + "name": "Proposal", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nextBlock", + "round": 0, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 15 + }, + "_Event": { + "height": 2, + "name": "PolkaAny", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrevote", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 16 + }, + "_Event": { + "height": 2, + "name": "PolkaAny", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrevote", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 17 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrevote", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 0, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 18 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrevote", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 0, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 19 + }, + "_Event": { + "height": 2, + "name": "PrecommitAny", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrecommit", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 20 + }, + "_Event": { + "height": 2, + "name": "PrecommitAny", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrecommit", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 21 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrecommit", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "skipRound", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": 1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 22 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrecommit", + "round": 0, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "skipRound", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": 1, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 0, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 23 + }, + "_Event": { + "height": 2, + "name": "NewRound", + "round": 1, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 24 + }, + "_Event": { + "height": 2, + "name": "NewRound", + "round": 1, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 25 + }, + "_Event": { + "height": 2, + "name": "TimeoutPropose", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 1, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 26 + }, + "_Event": { + "height": 2, + "name": "TimeoutPropose", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 1, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "prevote", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 27 + }, + "_Event": { + "height": 2, + "name": "PolkaNil", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 1, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 28 + }, + "_Event": { + "height": 2, + "name": "PolkaNil", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 1, + "src": "Josef", + "step": "precommit" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 29 + }, + "_Event": { + "height": 2, + "name": "PrecommitAny", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrecommit", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 30 + }, + "_Event": { + "height": 2, + "name": "PrecommitAny", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPrecommit", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 31 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrecommit", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "skipRound", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": 2, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 32 + }, + "_Event": { + "height": 2, + "name": "TimeoutPrecommit", + "round": 1, + "value": "nextBlock", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "skipRound", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": 2, + "timeout": "", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 1, + "step": "precommit", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 33 + }, + "_Event": { + "height": 2, + "name": "NewRound", + "round": 2, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 2, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 34 + }, + "_Event": { + "height": 2, + "name": "NewRound", + "round": 2, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "timeout", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": -1, + "timeout": "timeoutPropose", + "voteMessage": { + "height": -1, + "id": "", + "round": -1, + "src": "", + "step": "" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 2, + "step": "propose", + "validRound": -1, + "validValue": "nil" + } + ] + ] + } + }, + { + "#meta": { + "index": 35 + }, + "_Event": { + "height": 2, + "name": "ProposalInvalid", + "round": 2, + "value": "", + "vr": -1 + }, + "_Result": { + "decided": "", + "name": "votemessage", + "proposal": { + "height": -1, + "proposal": "", + "round": -1, + "src": "", + "validRound": -1 + }, + "skipRound": 42, + "timeout": "", + "voteMessage": { + "height": 2, + "id": "nil", + "round": 2, + "src": "Josef", + "step": "prevote" + } + }, + "system": { + "#map": [ + [ + "Josef", + { + "height": 2, + "lockedRound": -1, + "lockedValue": "nil", + "p": "Josef", + "round": 2, + "step": "prevote", + "validRound": 200, + "validValue": "nil" + } + ] + ] + } + } + ] +} diff --git a/itf/tests/fixtures/TestInsufficientSuccess9.itf.json b/itf/tests/fixtures/TestInsufficientSuccess9.itf.json index 2e8d813..0e86dac 100644 --- a/itf/tests/fixtures/TestInsufficientSuccess9.itf.json +++ b/itf/tests/fixtures/TestInsufficientSuccess9.itf.json @@ -4,12 +4,7 @@ "format-description": "https://apalache.informal.systems/docs/adr/015adr-trace.html", "description": "Created by Apalache on Sat Sep 24 20:45:34 CEST 2022" }, - "vars": [ - "outcome", - "balances", - "action", - "step" - ], + "vars": ["outcome", "balances", "action", "step"], "states": [ { "#meta": { @@ -22,18 +17,9 @@ "Carol", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ], @@ -41,18 +27,9 @@ "Eve", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ], @@ -110,18 +87,9 @@ "Dave", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ] @@ -135,18 +103,9 @@ "Carol", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ], @@ -154,18 +113,9 @@ "Eve", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ], @@ -223,18 +173,9 @@ "Dave", { "#map": [ - [ - "atom", - 0 - ], - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ] + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }] ] } ] @@ -256,7 +197,7 @@ "denom": "muon" }, { - "amount": 2, + "amount": { "#bigint": "2" }, "denom": "atom" }, { @@ -276,18 +217,9 @@ "Carol", { "#map": [ - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ], - [ - "atom", - 0 - ] + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }] ] } ], @@ -295,10 +227,7 @@ "Eve", { "#map": [ - [ - "atom", - 2 - ], + ["atom", { "#bigint": "2" }], [ "muon", { @@ -318,14 +247,8 @@ "Alice", { "#map": [ - [ - "gluon", - 2 - ], - [ - "muon", - 2 - ], + ["gluon", { "#bigint": "2" }], + ["muon", { "#bigint": "2" }], [ "atom", { @@ -364,18 +287,9 @@ "Dave", { "#map": [ - [ - "gluon", - 0 - ], - [ - "muon", - 0 - ], - [ - "atom", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }] ] } ] @@ -391,11 +305,11 @@ "action": { "coins": [ { - "amount": 1, + "amount": { "#bigint": "1" }, "denom": "gluon" }, { - "amount": 0, + "amount": { "#bigint": "0" }, "denom": "gluon" } ], @@ -409,18 +323,9 @@ "Carol", { "#map": [ - [ - "muon", - 0 - ], - [ - "gluon", - 0 - ], - [ - "atom", - 0 - ] + ["muon", { "#bigint": "0" }], + ["gluon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }] ] } ], @@ -440,10 +345,7 @@ "#bigint": "57896044618658097711785492504343953926634992332820282019728792003956564819965" } ], - [ - "atom", - 2 - ] + ["atom", { "#bigint": "2" }] ] } ], @@ -451,14 +353,8 @@ "Alice", { "#map": [ - [ - "muon", - 2 - ], - [ - "gluon", - 2 - ], + ["muon", { "#bigint": "2" }], + ["gluon", { "#bigint": "2" }], [ "atom", { @@ -497,18 +393,9 @@ "Dave", { "#map": [ - [ - "gluon", - 0 - ], - [ - "atom", - 0 - ], - [ - "muon", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }] ] } ] @@ -530,7 +417,7 @@ "denom": "muon" }, { - "amount": 3, + "amount": { "#bigint": "3" }, "denom": "atom" } ], @@ -544,18 +431,9 @@ "Carol", { "#map": [ - [ - "gluon", - 0 - ], - [ - "atom", - 0 - ], - [ - "muon", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }] ] } ], @@ -575,10 +453,7 @@ "#bigint": "57896044618658097711785492504343953926634992332820282019728792003956564819965" } ], - [ - "atom", - 2 - ] + ["atom", { "#bigint": "2" }] ] } ], @@ -586,20 +461,14 @@ "Alice", { "#map": [ - [ - "gluon", - 2 - ], + ["gluon", { "#bigint": "2" }], [ "atom", { "#bigint": "57896044618658097711785492504343953926634992332820282019728792003956564819965" } ], - [ - "muon", - 2 - ] + ["muon", { "#bigint": "2" }] ] } ], @@ -632,18 +501,9 @@ "Dave", { "#map": [ - [ - "gluon", - 0 - ], - [ - "muon", - 0 - ], - [ - "atom", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }] ] } ] @@ -659,15 +519,15 @@ "action": { "coins": [ { - "amount": 1, + "amount": { "#bigint": "1" }, "denom": "gluon" }, { - "amount": 1, + "amount": { "#bigint": "1" }, "denom": "muon" }, { - "amount": 1, + "amount": { "#bigint": "1" }, "denom": "atom" } ], @@ -681,18 +541,9 @@ "Carol", { "#map": [ - [ - "gluon", - 0 - ], - [ - "atom", - 0 - ], - [ - "muon", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }] ] } ], @@ -712,10 +563,7 @@ "#bigint": "57896044618658097711785492504343953926634992332820282019728792003956564819965" } ], - [ - "atom", - 2 - ] + ["atom", { "#bigint": "2" }] ] } ], @@ -723,20 +571,14 @@ "Alice", { "#map": [ - [ - "gluon", - 2 - ], + ["gluon", { "#bigint": "2" }], [ "atom", { "#bigint": "57896044618658097711785492504343953926634992332820282019728792003956564819965" } ], - [ - "muon", - 2 - ] + ["muon", { "#bigint": "2" }] ] } ], @@ -769,18 +611,9 @@ "Dave", { "#map": [ - [ - "gluon", - 0 - ], - [ - "muon", - 0 - ], - [ - "atom", - 0 - ] + ["gluon", { "#bigint": "0" }], + ["muon", { "#bigint": "0" }], + ["atom", { "#bigint": "0" }] ] } ] @@ -791,4 +624,3 @@ } ] } - diff --git a/itf/tests/insufficient_success.rs b/itf/tests/insufficient_success.rs new file mode 100644 index 0000000..0ea3178 --- /dev/null +++ b/itf/tests/insufficient_success.rs @@ -0,0 +1,57 @@ +#![allow(dead_code)] + +use std::collections::HashMap; + +use num_bigint::BigInt; +use serde::Deserialize; + +type Balance = HashMap; +type Balances = HashMap; + +#[derive(Copy, Clone, Debug, Deserialize)] +enum Outcome { + #[serde(rename = "")] + None, + #[serde(rename = "SUCCESS")] + Success, + #[serde(rename = "DUPLICATE_DENOM")] + DuplicateDenom, + #[serde(rename = "INSUFFICIENT_FUNDS")] + InsufficientFunds, +} + +#[derive(Clone, Debug, Deserialize)] +struct Coin { + amount: BigInt, + denom: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "tag")] +enum Action { + #[serde(rename = "init")] + Init { balances: Balances }, + + #[serde(rename = "send")] + Send { + receiver: String, + sender: String, + coins: Vec, + }, +} + +#[derive(Clone, Debug, Deserialize)] +struct State { + action: Action, + outcome: Outcome, + balances: Balances, + step: i64, +} + +#[test] +fn deserialize() { + let data = include_str!("../tests/fixtures/TestInsufficientSuccess9.itf.json"); + let trace = itf::trace_from_str::(data).unwrap(); + + dbg!(trace); +} diff --git a/itf/tests/missionaries_and_cannibals.rs b/itf/tests/missionaries_and_cannibals.rs new file mode 100644 index 0000000..a99b479 --- /dev/null +++ b/itf/tests/missionaries_and_cannibals.rs @@ -0,0 +1,43 @@ +#![allow(dead_code)] + +use std::collections::{BTreeSet, HashMap}; + +use serde::Deserialize; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +enum Bank { + #[serde(rename = "N")] + North, + #[serde(rename = "W")] + West, + #[serde(rename = "E")] + East, + #[serde(rename = "S")] + South, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +enum Person { + #[serde(rename = "c1_OF_PERSON")] + Cannibal1, + #[serde(rename = "c2_OF_PERSON")] + Cannibal2, + #[serde(rename = "m1_OF_PERSON")] + Missionary1, + #[serde(rename = "m2_OF_PERSON")] + Missionary2, +} + +#[derive(Clone, Debug, Deserialize)] +struct State { + pub bank_of_boat: Bank, + pub who_is_on_bank: HashMap>, +} + +#[test] +fn cannibals() { + let data = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); + let trace = itf::trace_from_str::(data).unwrap(); + + dbg!(trace); +} diff --git a/itf/tests/parse_trace.rs b/itf/tests/parse_trace.rs deleted file mode 100644 index 5b99b8c..0000000 --- a/itf/tests/parse_trace.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::collections::HashMap; - -use num_bigint::BigInt; -use serde::Deserialize; - -use itf::{trace_from_str, Itf, ItfMap, ItfSet}; - -#[test] -fn cannibals() { - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] - enum Bank { - #[serde(rename = "N")] - North, - #[serde(rename = "W")] - West, - #[serde(rename = "E")] - East, - #[serde(rename = "S")] - South, - } - - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] - enum Person { - #[serde(rename = "c1_OF_PERSON")] - Cannibal1, - #[serde(rename = "c2_OF_PERSON")] - Cannibal2, - #[serde(rename = "m1_OF_PERSON")] - Missionary1, - #[serde(rename = "m2_OF_PERSON")] - Missionary2, - } - - #[derive(Clone, Debug, Deserialize)] - #[allow(dead_code)] - struct State { - pub bank_of_boat: Bank, - pub who_is_on_bank: ItfMap>, - } - - let data = include_str!("../tests/fixtures/MissionariesAndCannibals.itf.json"); - let trace = trace_from_str::(data).unwrap(); - - dbg!(trace); -} - -#[test] -fn insufficent_success_9() { - type Balance = Itf>>; - type Balances = Itf>; - - #[derive(Copy, Clone, Debug, Deserialize)] - enum Outcome { - #[serde(rename = "")] - None, - #[serde(rename = "SUCCESS")] - Success, - #[serde(rename = "DUPLICATE_DENOM")] - DuplicateDenom, - #[serde(rename = "INSUFFICIENT_FUNDS")] - InsufficientFunds, - } - - #[derive(Clone, Debug, Deserialize)] - #[allow(dead_code)] - struct Coin { - amount: Itf, - denom: String, - } - - #[derive(Clone, Debug, Deserialize)] - #[allow(dead_code)] - #[serde(tag = "tag")] - enum Action { - #[serde(rename = "init")] - Init { balances: Balances }, - - #[serde(rename = "send")] - Send { - receiver: String, - sender: String, - coins: Vec, - }, - } - - #[derive(Clone, Debug, Deserialize)] - #[allow(dead_code)] - struct State { - action: Action, - outcome: Outcome, - balances: Balances, - step: i64, - } - - let data = include_str!("../tests/fixtures/TestInsufficientSuccess9.itf.json"); - let trace = trace_from_str::(data).unwrap(); - - dbg!(trace); -} diff --git a/itf/tests/regression.rs b/itf/tests/regression.rs new file mode 100644 index 0000000..86d1431 --- /dev/null +++ b/itf/tests/regression.rs @@ -0,0 +1,253 @@ +use serde::Deserialize; + +#[test] +fn test_tuple() { + let itf = serde_json::json!({"#tup": [1, 2, 3]}); + + let _: [u8; 3] = itf::from_value(itf.clone()).unwrap(); + let _: (u8, u8, u8) = itf::from_value(itf.clone()).unwrap(); + let _: Vec = itf::from_value(itf.clone()).unwrap(); + let _: std::collections::HashSet = itf::from_value(itf.clone()).unwrap(); + let _: std::collections::BTreeSet = itf::from_value(itf.clone()).unwrap(); +} + +#[test] +fn test_set() { + let itf = serde_json::json!({"#set": [1, 2, 3]}); + + let _: [u8; 3] = itf::from_value(itf.clone()).unwrap(); + let _: (u8, u8, u8) = itf::from_value(itf.clone()).unwrap(); + let _: Vec = itf::from_value(itf.clone()).unwrap(); + let _: std::collections::HashSet = itf::from_value(itf.clone()).unwrap(); + let _: std::collections::BTreeSet = itf::from_value(itf.clone()).unwrap(); + let _: serde_json::Value = itf::from_value(itf.clone()).unwrap(); +} + +#[test] +fn test_num_bigint() { + let itf = serde_json::json!([-1, [99]]); + + // successful case; only BigInt + assert_eq!( + num_bigint::BigInt::from(-99), + itf::from_value::(itf.clone()).unwrap() + ); + + // unsuccessful cases + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(!matches!( + itf::from_value::(itf.clone()).unwrap(), + itf::Value::BigInt(_), + )); +} + +#[test] +fn test_bigint_deser() { + let itf = serde_json::json!({"#bigint": "-99"}); + + // successful case; only BigInt + assert_eq!( + num_bigint::BigInt::from(-99), + itf::from_value(itf.clone()).unwrap() + ); + + // unsuccessful cases + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(!matches!( + itf::from_value::(itf.clone()).unwrap(), + itf::Value::BigInt(_), + )); +} + +#[test] +fn test_biguint_deser() { + let itf = serde_json::json!({"#bigint": "99"}); + + // successful case; only BigInt + assert_eq!( + num_bigint::BigInt::from(99), + itf::from_value(itf.clone()).unwrap() + ); + + // unsuccessful cases + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(!matches!( + itf::from_value::(itf.clone()).unwrap(), + itf::Value::BigInt(_), + )); +} + +#[test] +fn test_itf_value_equivalent() { + let itf = serde_json::json!({ + "bool": true, + "number": -99, + "str": "hello", + "list": [1, 2, 3], + "record": {"a": 1, "b": 2, "c": 3}, + }); + + let value = serde_json::from_value::(itf.clone()).unwrap(); + assert_eq!(value.clone(), itf::Value::deserialize(value).unwrap()); +} + +#[test] +#[should_panic] +fn test_itf_value_noneq() { + // Deserialized Value loses the type information + let itf = serde_json::json!({ + "bigint": {"#bigint": "-999"}, + "tuple": {"#tup": [1, 2, 3]}, + "set": {"#set": [1, 2, 3]}, + "map": {"#map": [["1", 3], ["2", 4]]}, + }); + + let value = serde_json::from_value::(itf.clone()).unwrap(); + assert_eq!(value.clone(), itf::Value::deserialize(value).unwrap()); +} + +#[test] +#[should_panic] +fn test_map_with_non_str_key() { + // MapSerializer accepts only string keys + let itf = serde_json::json!({ + "map": {"#map": [[1, 3], [2, 4]]}, + }); + + let value = serde_json::from_value::(itf).unwrap(); + itf::Value::deserialize(value).unwrap(); +} + +#[test] +fn test_bigint_to_int() { + let itf = serde_json::json!({ + // i64::MIN - 1 + "#bigint": "-9223372036854775809", + }); + + assert!(itf::from_value::(itf.clone()).is_err()); + assert!(itf::from_value::(itf).is_ok()); +} + +#[test] +fn test_deserialize_any() { + use itf::de::{As, Integer}; + use num_bigint::BigInt; + use std::collections::HashMap; + + let itf = serde_json::json!([{ + "_foo": {"#map": [[{"#bigint": "1"}, {"#bigint": "2"}]]}, + "typ": "Foo", + }, + { + "_bar": [[[{"#bigint": "1"}, {"#bigint": "2"}]]], + "typ": "Bar", + } + + ]); + + // deserialize as bigints + #[derive(Deserialize, Debug)] + #[serde(tag = "typ")] + enum FooBarBigInt { + Foo { _foo: HashMap }, + Bar { _bar: Vec> }, + } + itf::from_value::>(itf.clone()).unwrap(); + + // deserialize as i64 + #[derive(Deserialize, Debug)] + #[serde(tag = "typ")] + enum FooBarInt { + // try to deserialize _foo as i64, instead of BigInt + Foo { + #[serde(with = "As::>")] + _foo: HashMap, + }, + Bar { + #[serde(with = "As::>>")] + _bar: Vec>, + }, + } + itf::from_value::>(itf.clone()).unwrap(); + + // deserialize as mix + #[derive(Deserialize, Debug)] + #[serde(tag = "typ")] + enum FooBarMixInt { + // try to deserialize _foo as i64, instead of BigInt + Foo { + #[serde(with = "As::>")] + _foo: HashMap, + }, + Bar { + #[serde(with = "As::>>")] + _bar: Vec>, + }, + } + itf::from_value::>(itf.clone()).unwrap(); +} + +#[test] +fn test_failed_bare_bigint_to_int() { + use itf::de::Integer; + use serde_with::de::DeserializeAsWrap; + + let itf = serde_json::json!({ + "#bigint": "12", + }); + + let itf_value = serde_json::from_value::(itf.clone()).unwrap(); + + assert!(i64::deserialize(itf_value.clone()).is_err()); + + assert!(DeserializeAsWrap::::deserialize(itf_value).is_ok()); +} + +#[test] +fn test_complete() { + use std::collections::{BTreeSet, HashMap, HashSet}; + + #[derive(Deserialize, Debug)] + #[serde(untagged)] + enum RecordEnum { + One(i64, String), + Two { _foo: String, _bar: i64 }, + } + + #[derive(Deserialize, Debug)] + struct Complete { + _bool: bool, + _number: i64, + _str: String, + _bigint: num_bigint::BigInt, + _list: Vec, + _tuple: (String, num_bigint::BigInt), + _set: HashSet, + _map: HashMap, num_bigint::BigInt>, + _enum: Vec, + } + + let itf = serde_json::json!({ + "_bool": true, + "_number": -99, + "_str": "hello", + "_bigint": {"#bigint": "-999"}, + "_int_from_bigint": {"#bigint": "-999"}, + "_bigint_from_int": -999, + "_list": [{"#bigint": "1"}, {"#bigint": "2"}, {"#bigint": "3"}], + "_tuple": {"#tup": ["hello", {"#bigint": "999"}]}, + "_set": {"#set": [{"#bigint": "1"}, {"#bigint": "2"}, {"#bigint": "3"}]}, + "_map": {"#map": [[{"#set": [{"#bigint": "1"}, {"#bigint": "2"}]}, {"#bigint": "3"}], [{"#set": [{"#bigint": "2"}, {"#bigint": "3"}]}, {"#bigint": "4"}]]}, + "_enum": [{"#tup": [1, "hello"]}, {"_foo": "hello", "_bar": 1}], + }); + + let _: Complete = itf::from_value(itf).unwrap(); +}