diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 10ac08974..240169a75 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -27,6 +27,8 @@ jobs: run: make -C pykwasm pyupgrade - name: 'Run unit tests' run: make -C pykwasm cov-unit + - name: 'Run Rust unit-tests' + run: make rust-tests conformance-tests: name: 'Conformance Tests' diff --git a/Makefile b/Makefile index 88a0c44a9..1e087535d 100644 --- a/Makefile +++ b/Makefile @@ -206,6 +206,14 @@ proof_tests:=$(wildcard tests/proofs/*-spec.k) test-prove: $(proof_tests:=.prove) +### Rust tests + +rust-tests: erc20-rust-tests +.PHONY: rust-tests + +erc20-rust-tests: tests/ulm/erc20/rust/src/*.rs tests/ulm/erc20/rust/Cargo.* + cd tests/ulm/erc20/rust && RUST_BACKTRACE=1 cargo test +.PHONY: erc20-rust-tests # Analysis # -------- diff --git a/tests/ulm/erc20/erc20.wast b/tests/ulm/erc20/erc20.wast new file mode 100644 index 000000000..e69de29bb diff --git a/tests/ulm/erc20/rust/.gitignore b/tests/ulm/erc20/rust/.gitignore new file mode 100644 index 000000000..9f970225a --- /dev/null +++ b/tests/ulm/erc20/rust/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/tests/ulm/erc20/rust/Cargo.lock b/tests/ulm/erc20/rust/Cargo.lock new file mode 100644 index 000000000..16338b513 --- /dev/null +++ b/tests/ulm/erc20/rust/Cargo.lock @@ -0,0 +1,124 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "erc20" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasm-bindgen" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" diff --git a/tests/ulm/erc20/rust/Cargo.toml b/tests/ulm/erc20/rust/Cargo.toml new file mode 100644 index 000000000..a0661b15d --- /dev/null +++ b/tests/ulm/erc20/rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "erc20" +version = "0.1.0" +edition = "2021" + +#[profile.dev] +#panic = "abort" + +#[profile.release] +#panic = "abort" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" diff --git a/tests/ulm/erc20/rust/src/assertions.rs b/tests/ulm/erc20/rust/src/assertions.rs new file mode 100644 index 000000000..753c2a928 --- /dev/null +++ b/tests/ulm/erc20/rust/src/assertions.rs @@ -0,0 +1,24 @@ + +#[cfg(not(test))] +use core::panic::PanicInfo; + +use crate::ulm_hooks; + +pub fn fail(msg: &str) -> ! { + ulm_hooks::failWrapper(msg); +} + +#[cfg(not(test))] +#[panic_handler] +pub fn panic(_info: &PanicInfo) -> ! { + fail("Panic") +} + +#[macro_export] +macro_rules! require { + ( $cond:expr , $msg:expr ) => { + if ! $cond { + fail($msg); + } + } +} diff --git a/tests/ulm/erc20/rust/src/lib.rs b/tests/ulm/erc20/rust/src/lib.rs new file mode 100644 index 000000000..e486868be --- /dev/null +++ b/tests/ulm/erc20/rust/src/lib.rs @@ -0,0 +1,7 @@ +#![cfg_attr(not(test), no_std)] + +mod assertions; +mod unsigned; +mod ulm_hooks; + +mod unsigned_tests; \ No newline at end of file diff --git a/tests/ulm/erc20/rust/src/ulm_hooks.rs b/tests/ulm/erc20/rust/src/ulm_hooks.rs new file mode 100644 index 000000000..b8a5ecd6e --- /dev/null +++ b/tests/ulm/erc20/rust/src/ulm_hooks.rs @@ -0,0 +1,25 @@ +extern "C" { + #[allow(dead_code)] + pub fn fail(msg: *const u8, msg_len: usize) -> !; +} + +#[cfg(test)] +pub mod overrides { + #[no_mangle] + pub extern "C" fn fail(_msg: *const u8, _msg_len: usize) -> ! { + panic!("fail called"); + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +pub fn failWrapper(msg: &str) -> ! { + panic!("{}", msg); +} + +#[cfg(not(test))] +#[allow(non_snake_case)] +pub fn failWrapper(msg: &str) -> ! { + let msg_bytes = msg.as_bytes(); + unsafe { fail(msg_bytes.as_ptr(), msg_bytes.len()); } +} diff --git a/tests/ulm/erc20/rust/src/unsigned.rs b/tests/ulm/erc20/rust/src/unsigned.rs new file mode 100644 index 000000000..704227d7a --- /dev/null +++ b/tests/ulm/erc20/rust/src/unsigned.rs @@ -0,0 +1,181 @@ +// This is a suboptimal implementation of an unsigned int, which is small and +// therefore useful for testing the wasm semantics. A proper implementation +// should probably use something like ruint2::Uint<..., ...> or uint256::Uint256. + +use core::cmp::Ordering; +use core::ops::Add; +use core::ops::Sub; + +use crate::assertions::fail; +use crate::require; + +#[derive(Debug)] +pub struct Unsigned { + chunks: [u8; N], +} + +// pub type U72 = Unsigned<9>; +// pub type U80 = Unsigned<10>; +// pub type U88 = Unsigned<11>; +// pub type U96 = Unsigned<12>; +// pub type U104 = Unsigned<13>; +// pub type U112 = Unsigned<14>; +// pub type U120 = Unsigned<15>; +// pub type U128 = Unsigned<16>; +// pub type U136 = Unsigned<17>; +// pub type U144 = Unsigned<18>; +// pub type U152 = Unsigned<19>; +pub type U160 = Unsigned<20>; +// pub type U168 = Unsigned<21>; +// pub type U176 = Unsigned<22>; +// pub type U184 = Unsigned<23>; +// pub type U192 = Unsigned<24>; +// pub type U200 = Unsigned<25>; +// pub type U208 = Unsigned<26>; +// pub type U216 = Unsigned<27>; +// pub type U224 = Unsigned<28>; +// pub type U232 = Unsigned<29>; +// pub type U240 = Unsigned<30>; +// pub type U248 = Unsigned<31>; +pub type U256 = Unsigned<32>; + +impl Unsigned { + pub fn from_unsigned(value: &Unsigned) -> Unsigned { + let mut chunks = [0_u8; N]; + if M <= N { + for i in 0 .. M { + chunks[i] = value.chunks[i]; + } + } else { + for i in 0 .. N { + chunks[i] = value.chunks[i]; + } + for i in N .. M { + require!(value.chunks[i] == 0, "Value too large to cast"); + } + } + Unsigned { chunks } + } + + pub fn from_u64(value: u64) -> Unsigned { + if 8 <= N { + let mut chunks = [0_u8; N]; + let mut to_process = value; + for i in 0 .. 8 { + chunks[i] = (to_process & 0xff) as u8; + to_process = to_process >> 8; + } + require!(to_process == 0, "Unprocessed bits in value."); + Unsigned { chunks } + } else { + Unsigned::from_unsigned(&Unsigned::<8>::from_u64(value)) + } + } +} + +impl Add for &Unsigned { + type Output = Unsigned; + fn add(self, other: &Unsigned) -> Self::Output { + let mut chunks = [0_u8; N]; + let mut carry = 0_u16; + for i in 0..N { + let value = (self.chunks[i] as u16) + (other.chunks[i] as u16) + carry; + carry = value >> 8; + chunks[i] = (value & 0xff) as u8; + } + require!(carry == 0, "Addition overflow"); + Unsigned { chunks } + } +} +impl Add for Unsigned { + type Output = Unsigned; + fn add(self, other: Unsigned) -> Self::Output { + &self + &other + } +} +impl Add> for &Unsigned { + type Output = Unsigned; + fn add(self, other: Unsigned) -> Self::Output { + self + &other + } +} +impl Add<&Unsigned> for Unsigned { + type Output = Unsigned; + fn add(self, other: &Unsigned) -> Self::Output { + &self + other + } +} + +impl Sub for &Unsigned { + type Output = Unsigned; + fn sub(self, other: &Unsigned) -> Self::Output { + let mut chunks = [0_u8; N]; + let mut carry = 0_u16; + for i in 0..N { + let self_chunk = self.chunks[i] as u16; + let to_remove = other.chunks[i] as u16 + carry; + let remove_from = + if self_chunk >= to_remove { + carry = 0; + self_chunk + } else { + carry = 1; + self_chunk + 0x100 + }; + require!(remove_from >= to_remove, "Unexpected value in subtraction"); + let result = remove_from - to_remove; + require!(result <= 0xff, "Unexpected value in subtraction"); + chunks[i] = result as u8; + } + require!(carry == 0, "Subtraction overflow"); + Unsigned { chunks } + } +} +impl Sub for Unsigned { + type Output = Unsigned; + fn sub(self, other: Unsigned) -> Self::Output { + &self - &other + } +} +impl Sub> for &Unsigned { + type Output = Unsigned; + fn sub(self, other: Unsigned) -> Self::Output { + self - &other + } +} +impl Sub<&Unsigned> for Unsigned { + type Output = Unsigned; + fn sub(self, other: &Unsigned) -> Self::Output { + &self - other + } +} + +impl Ord for Unsigned { + fn cmp(&self, other: &Self) -> Ordering { + for i in (0..N).rev() { + if self.chunks[i] < other.chunks[i] { + return Ordering::Less; + } + if self.chunks[i] > other.chunks[i] { + return Ordering::Greater; + } + } + Ordering::Equal + } +} +impl PartialOrd for Unsigned { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl PartialEq for Unsigned { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} +impl Eq for Unsigned {} +impl Clone for Unsigned { + fn clone(&self) -> Self { + Unsigned { chunks: self.chunks.clone() } + } +} diff --git a/tests/ulm/erc20/rust/src/unsigned_tests.rs b/tests/ulm/erc20/rust/src/unsigned_tests.rs new file mode 100644 index 000000000..25aa95fc9 --- /dev/null +++ b/tests/ulm/erc20/rust/src/unsigned_tests.rs @@ -0,0 +1,76 @@ + +#[cfg(test)] +mod unsigned_tests { + use crate::unsigned::*; + + #[test] + fn simple_addition() { + let first = Unsigned::<1>::from_u64(2); + let second = Unsigned::<1>::from_u64(3); + let result: Unsigned<1> = first + second; + assert_eq!(result, Unsigned::from_u64(5)); + } + + #[test] + fn addition_byte_overflow() { + let result: Unsigned<2> = Unsigned::from_u64(129) + Unsigned::from_u64(128); + assert_eq!(result, Unsigned::from_u64(257)); + } + + #[test] + fn simple_subtraction() { + let result: Unsigned<1> = Unsigned::from_u64(5) - Unsigned::from_u64(3); + assert_eq!(result, Unsigned::from_u64(2)); + } + + #[test] + fn subtraction_byte_overflow() { + let result: Unsigned<2> = Unsigned::from_u64(257) - Unsigned::from_u64(128); + assert_eq!(result, Unsigned::from_u64(129)); + } + + #[test] + fn eq() { + assert_eq!(Unsigned::<1>::from_u64(5), Unsigned::from_u64(5)); + assert_ne!(Unsigned::<1>::from_u64(8), Unsigned::from_u64(5)); + } + + #[test] + fn lt() { + assert!(Unsigned::<1>::from_u64(5) < Unsigned::from_u64(8)); + assert!(!(Unsigned::<1>::from_u64(8) < Unsigned::from_u64(5))); + assert!(Unsigned::<2>::from_u64(0x105) < Unsigned::from_u64(0x108)); + assert!(!(Unsigned::<2>::from_u64(0x108) < Unsigned::from_u64(0x105))); + } + + #[test] + fn le() { + assert!(Unsigned::<1>::from_u64(5) <= Unsigned::from_u64(8)); + assert!(!(Unsigned::<1>::from_u64(8) <= Unsigned::from_u64(5))); + assert!(Unsigned::<2>::from_u64(0x105) <= Unsigned::from_u64(0x108)); + assert!(!(Unsigned::<2>::from_u64(0x108) <= Unsigned::from_u64(0x105))); + assert!(Unsigned::<2>::from_u64(0x105) <= Unsigned::from_u64(0x105)); + } + + #[test] + fn gt() { + assert!(Unsigned::<1>::from_u64(8) > Unsigned::from_u64(5)); + assert!(!(Unsigned::<1>::from_u64(5) > Unsigned::from_u64(8))); + assert!(Unsigned::<2>::from_u64(0x108) > Unsigned::from_u64(0x105)); + assert!(!(Unsigned::<2>::from_u64(0x105) > Unsigned::from_u64(0x108))); + } + + #[test] + fn ge() { + assert!(Unsigned::<1>::from_u64(8) >= Unsigned::from_u64(5)); + assert!(!(Unsigned::<1>::from_u64(5) >= Unsigned::from_u64(8))); + assert!(Unsigned::<2>::from_u64(0x108) >= Unsigned::from_u64(0x105)); + assert!(!(Unsigned::<2>::from_u64(0x105) >= Unsigned::from_u64(0x108))); + assert!(Unsigned::<2>::from_u64(0x105) >= Unsigned::from_u64(0x105)); + } + + #[test] + fn from_unsigned() { + assert_eq!(U256::from_u64(5), U256::from_unsigned(&U160::from_u64(5))); + } +}