From cbbc63583bc7fcb70d30d314b8fe82405bd4b296 Mon Sep 17 00:00:00 2001 From: aritkulova Date: Tue, 10 Nov 2026 22:21:10 +0200 Subject: [PATCH] added bit and conversion functions for u4 --- CHANGELOG.md | 2 +- README.md | 3 +- docs/stdlib.json | 77 ++++++++++++++++ simf/lib/u4/bit.simf | 27 ++++++ simf/lib/u4/convert.simf | 61 +++++++++++++ simf/tests/u4/bit.simf | 33 +++++++ simf/tests/u4/convert.simf | 109 +++++++++++++++++++++++ simf/tests/utxo/op_return.simf | 9 +- tests/stdlib/main.rs | 1 + tests/stdlib/u4/bit.rs | 107 ++++++++++++++++++++++ tests/stdlib/u4/convert.rs | 157 +++++++++++++++++++++++++++++++++ tests/stdlib/u4/mod.rs | 2 + 12 files changed, 578 insertions(+), 10 deletions(-) create mode 100644 simf/lib/u4/bit.simf create mode 100644 simf/lib/u4/convert.simf create mode 100644 simf/tests/u4/bit.simf create mode 100644 simf/tests/u4/convert.simf create mode 100644 tests/stdlib/u4/bit.rs create mode 100644 tests/stdlib/u4/convert.rs create mode 100644 tests/stdlib/u4/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9cbad..3fd7eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - Extended functions for retrieving information from `OP_RETURN`. -- Conversion and bit functions for `u2`. +- Conversion and bit functions for `u2` and `u4`. ### Changed - Bumped Simplex to 0.2.0. diff --git a/README.md b/README.md index f3a9c31..c8e12e9 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ simf/lib │ └── convert.simf │ └── Conversions from `u1` to other uint types and `bool`. ├── u2 +├── u4 │ ├── bit.simf │ │ └── Basic bitwise operations (`and`, `or`, `eq`). │ └── convert.simf -│ └── Conversions between `u2` and other uint types. +│ └── Conversions between `u2`, `u4` and other uint types. ├── u8 ├── u16 ├── u32 diff --git a/docs/stdlib.json b/docs/stdlib.json index 38d1a21..f72a997 100644 --- a/docs/stdlib.json +++ b/docs/stdlib.json @@ -825,6 +825,83 @@ "output_type": "u1", "description": "Narrow a `u2` value to a `u1` value.\n\n## Panics\nPanics if the value does not fit in `u1`." }, + { + "simplicityhl_name": "and_4", + "section": "`u4` bit logic", + "input_type": "u4, u4", + "output_type": "u4", + "description": "Bitwise AND of two `u4` values." + }, + { + "simplicityhl_name": "or_4", + "section": "`u4` bit logic", + "input_type": "u4, u4", + "output_type": "u4", + "description": "Bitwise OR of two `u4` values." + }, + { + "simplicityhl_name": "eq_4", + "section": "`u4` bit logic", + "input_type": "u4, u4", + "output_type": "bool", + "description": "Check whether two `u4` values are equal." + }, + { + "simplicityhl_name": "u4_to_u8", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u8", + "description": "Widen a `u4` value to a `u8` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "u4_to_u16", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u16", + "description": "Widen a `u4` value to a `u16` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "u4_to_u32", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u32", + "description": "Widen a `u4` value to a `u32` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "u4_to_u64", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u64", + "description": "Widen a `u4` value to a `u64` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "u4_to_u128", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u128", + "description": "Widen a `u4` value to a `u128` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "u4_to_u256", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u256", + "description": "Widen a `u4` value to a `u256` value, zero-extending the high bits." + }, + { + "simplicityhl_name": "split_u4_into_u1", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "(u1, u1, u1, u1)", + "description": "Split a `u4` value into four `u1` words, most-significant first." + }, + { + "simplicityhl_name": "safe_u4_to_u1", + "section": "`u4` conversions", + "input_type": "u4", + "output_type": "u1", + "description": "Narrow a `u4` value to a `u1` value.\n\n## Panics\nPanics if the value does not fit in `u1`." + }, { "simplicityhl_name": "checked_add_8", "section": "`u8` arithmetic", diff --git a/simf/lib/u4/bit.simf b/simf/lib/u4/bit.simf new file mode 100644 index 0000000..2bb28c1 --- /dev/null +++ b/simf/lib/u4/bit.simf @@ -0,0 +1,27 @@ +use crate::lib::u4::convert::u4_to_u8; + +/// Bitwise AND of two 4-bit values +pub fn and_4(a: u4, b: u4) -> u4 { + let a_u8: u8 = u4_to_u8(a); + let b_u8: u8 = u4_to_u8(b); + + let (_, result): (u4, u4) = ::into(jet::and_8(a_u8, b_u8)); + result +} + +/// Bitwise OR of two 4-bit values +pub fn or_4(a: u4, b: u4) -> u4 { + let a_u8: u8 = u4_to_u8(a); + let b_u8: u8 = u4_to_u8(b); + + let (_, result): (u4, u4) = ::into(jet::or_8(a_u8, b_u8)); + result +} + +/// Checks if two 4-bit values are equal +pub fn eq_4(a: u4, b: u4) -> bool { + let a_u8: u8 = u4_to_u8(a); + let b_u8: u8 = u4_to_u8(b); + + jet::eq_8(a_u8, b_u8) +} diff --git a/simf/lib/u4/convert.simf b/simf/lib/u4/convert.simf new file mode 100644 index 0000000..b3c7ba2 --- /dev/null +++ b/simf/lib/u4/convert.simf @@ -0,0 +1,61 @@ +/// Widening uint conversions + +/// Converts u4 to u8 +pub fn u4_to_u8(a: u4) -> u8 { + <(u4, u4)>::into((0, a)) +} + +/// Converts u4 to u16 +pub fn u4_to_u16(a: u4) -> u16 { + let a_u8: u8 = u4_to_u8(a); + + jet::left_pad_low_8_16(a_u8) +} + +/// Converts u4 to u32 +pub fn u4_to_u32(a: u4) -> u32 { + let a_u8: u8 = u4_to_u8(a); + + jet::left_pad_low_8_32(a_u8) +} + +/// Converts u4 to u64 +pub fn u4_to_u64(a: u4) -> u64 { + let a_u8: u8 = u4_to_u8(a); + + jet::left_pad_low_8_64(a_u8) +} + +/// Converts u4 to u128 +pub fn u4_to_u128(a: u4) -> u128 { + let a_u64: u64 = u4_to_u64(a); + + <(u64, u64)>::into((0, a_u64)) +} + +/// Converts u4 to u256 +pub fn u4_to_u256(a: u4) -> u256 { + let a_u128: u128 = u4_to_u128(a); + + <(u128, u128)>::into((0, a_u128)) +} + +/// Splitting uint conversions + +/// Splits u4 into four u1 +pub fn split_u4_into_u1(a: u4) -> (u1, u1, u1, u1) { + ::into(a) +} + +/// Narrowing uint conversions + +/// Converts u4 into u1. +/// Panics if the value does not fit in u1 +pub fn safe_u4_to_u1(a: u4) -> u1 { + let u1_max: u8 = jet::left_pad_low_1_8(jet::high_1()); + let a_u8: u8 = u4_to_u8(a); + + assert!(jet::le_8(a_u8, u1_max)); + + jet::rightmost_8_1(a_u8) +} diff --git a/simf/tests/u4/bit.simf b/simf/tests/u4/bit.simf new file mode 100644 index 0000000..9dd9435 --- /dev/null +++ b/simf/tests/u4/bit.simf @@ -0,0 +1,33 @@ +use crate::lib::u4::bit::{and_4, or_4, eq_4}; + +use crate::tests::support::dispatch::is_selected; +use crate::tests::support::expect::assert_bool; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let a: u4 = witness::FIRST_ARG; + let b: u4 = witness::SECOND_ARG; + let expected: Option = witness::EXPECTED; + + let expected_bool: bool = witness::EXPECTED_BOOL; + + match is_selected(0, fn_idx) { + true => { + assert!(eq_4(and_4(a, b), unwrap(expected))); + }, + false => (), + }; + match is_selected(1, fn_idx) { + true => { + assert!(eq_4(or_4(a, b), unwrap(expected))); + }, + false => (), + }; + match is_selected(2, fn_idx) { + true => { + assert_bool(eq_4(a, b), expected_bool); + }, + false => (), + }; +} diff --git a/simf/tests/u4/convert.simf b/simf/tests/u4/convert.simf new file mode 100644 index 0000000..1765d30 --- /dev/null +++ b/simf/tests/u4/convert.simf @@ -0,0 +1,109 @@ +use crate::lib::u4::convert::{ + u4_to_u8, + u4_to_u16, + u4_to_u32, + u4_to_u64, + u4_to_u128, + u4_to_u256, + split_u4_into_u1, + safe_u4_to_u1 +}; +use crate::lib::asserts::{ + assert_eq_1, + assert_eq_8, + assert_eq_16, + assert_eq_32, + assert_eq_64, + assert_eq_128, + assert_eq_256 +}; + +use crate::tests::support::dispatch::is_selected; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let a: u4 = witness::FIRST_ARG; + + let expected: u256 = witness::EXPECTED; + let (_, _, _, expected_64): (u64, u64, u64, u64) = ::into(expected); + + match is_selected(0, fn_idx) { + true => { + let expected: u8 = jet::rightmost_64_8(expected_64); + + assert_eq_8(u4_to_u8(a), expected); + }, + false => (), + }; + + match is_selected(1, fn_idx) { + true => { + let expected: u16 = jet::rightmost_64_16(expected_64); + + assert_eq_16(u4_to_u16(a), expected); + }, + false => (), + }; + + match is_selected(2, fn_idx) { + true => { + let expected: u32 = jet::rightmost_64_32(expected_64); + + assert_eq_32(u4_to_u32(a), expected); + }, + false => (), + }; + + match is_selected(3, fn_idx) { + true => { + assert_eq_64(u4_to_u64(a), expected_64); + }, + false => (), + }; + + match is_selected(4, fn_idx) { + true => { + let (_, expected): (u128, u128) = ::into(expected); + + assert_eq_128(u4_to_u128(a), expected); + }, + false => (), + }; + + match is_selected(5, fn_idx) { + true => { + assert_eq_256(u4_to_u256(a), expected); + }, + false => (), + }; + + match is_selected(6, fn_idx) { + true => { + let (a3, a2, a1, a0): (u1, u1, u1, u1) = split_u4_into_u1(a); + + let expected: u4 = jet::rightmost_64_4(expected_64); + let ( + expected3, + expected2, + expected1, + expected0 + ): (u1, u1, u1, u1) = ::into(expected); + + assert_eq_1(a3, expected3); + assert_eq_1(a2, expected2); + assert_eq_1(a1, expected1); + assert_eq_1(a0, expected0); + }, + false => (), + }; + + match is_selected(7, fn_idx) { + true => { + let expected: u1 = jet::rightmost_64_1(expected_64); + + assert_eq_1(safe_u4_to_u1(a), expected); + }, + false => (), + }; +} diff --git a/simf/tests/utxo/op_return.simf b/simf/tests/utxo/op_return.simf index 94e83a9..0c41af6 100644 --- a/simf/tests/utxo/op_return.simf +++ b/simf/tests/utxo/op_return.simf @@ -15,20 +15,13 @@ use crate::lib::utxo::op_return::{ }; use crate::lib::u2::bit::eq_2; +use crate::lib::u4::bit::eq_4; use crate::lib::asserts::assert_eq_8; use crate::lib::u8::convert::safe_u8_to_u1; use crate::tests::support::dispatch::is_selected; use crate::tests::support::expect::assert_bool; -// todo: move to lib -fn eq_4(a: u4, b: u4) -> bool { - let a_u8: u8 = <(u4, u4)>::into((0, a)); - let b_u8: u8 = <(u4, u4)>::into((0, b)); - - jet::eq_8(a_u8, b_u8) -} - fn eq_pushed_data(a: (u2, u256), b: (u2, u256)) -> bool { let (indicator_a, hash_a): (u2, u256) = a; let (indicator_b, hash_b): (u2, u256) = b; diff --git a/tests/stdlib/main.rs b/tests/stdlib/main.rs index b91a90a..a5c2be3 100644 --- a/tests/stdlib/main.rs +++ b/tests/stdlib/main.rs @@ -9,6 +9,7 @@ mod u16; mod u2; mod u256; mod u32; +mod u4; mod u64; mod u8; mod utxo; diff --git a/tests/stdlib/u4/bit.rs b/tests/stdlib/u4/bit.rs new file mode 100644 index 0000000..772ac1c --- /dev/null +++ b/tests/stdlib/u4/bit.rs @@ -0,0 +1,107 @@ +use rand::Rng; + +use crate::common::core::{Expect, run}; + +use simplicityhl_std::artifacts::tests::u4::bit::BitProgram as U4TestBitProgram; +use simplicityhl_std::artifacts::tests::u4::bit::derived_bit::{ + BitArguments as U4TestBitArguments, BitWitness as U4TestBitWitness, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + And4, + Or4, + Eq4, +} + +const U4_MAX: u8 = 15; + +fn program() -> U4TestBitProgram { + U4TestBitProgram::new(&U4TestBitArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads. +struct Case { + witness: U4TestBitWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: U4TestBitWitness { + function_index: function as u8, + first_arg: 0, + second_arg: 0, + expected: None, + expected_bool: false, + }, + } +} + +impl Case { + /// The two operands, `first_arg` and `second_arg`. + fn args(mut self, a: u8, b: u8) -> Self { + self.witness.first_arg = a; + self.witness.second_arg = b; + self + } + + /// The u4 value the arm should return + fn expect(mut self, expected: u8) -> Self { + self.witness.expected = Some(expected); + self + } + + /// `expected_bool`: the boolean the arm should report. + fn expected_bool(mut self, expected_bool: bool) -> Self { + self.witness.expected_bool = expected_bool; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + run(context, program(), self.witness, expect) + } +} + +#[simplex::test] +fn and_4(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + let b = rand::thread_rng().gen_range(0..=U4_MAX); + let result = a & b; + + case(And4).args(a, b).expect(result).run(&context) +} + +#[simplex::test] +fn or_4(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + let b = rand::thread_rng().gen_range(0..=U4_MAX); + let result = a | b; + + case(Or4).args(a, b).expect(result).run(&context) +} + +#[simplex::test] +fn eq_4_true(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(Eq4).args(a, a).expected_bool(true).run(&context) +} + +#[simplex::test] +fn eq_4_false(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + let mut b = rand::thread_rng().gen_range(0..=U4_MAX); + + while a == b { + b = rand::thread_rng().gen_range(0..=U4_MAX); + } + + case(Eq4).args(a, b).expected_bool(false).run(&context) +} diff --git a/tests/stdlib/u4/convert.rs b/tests/stdlib/u4/convert.rs new file mode 100644 index 0000000..e929302 --- /dev/null +++ b/tests/stdlib/u4/convert.rs @@ -0,0 +1,157 @@ +use primitive_types::U256; +use rand::Rng; + +use crate::common::core::{Expect, run}; + +use simplicityhl_std::artifacts::tests::u4::convert::ConvertProgram as U4ConvertTestProgram; +use simplicityhl_std::artifacts::tests::u4::convert::derived_convert::{ + ConvertArguments as U4ConvertTestArguments, ConvertWitness as U4ConvertTestWitness, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + U4ToU8, + U4ToU16, + U4ToU32, + U4ToU64, + U4ToU128, + U4ToU256, + SplitU4IntoU1, + SafeU4ToU1, +} + +const U4_MAX: u8 = 15; + +fn program() -> U4ConvertTestProgram { + U4ConvertTestProgram::new(&U4ConvertTestArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads. +struct Case { + witness: U4ConvertTestWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: U4ConvertTestWitness { + function_index: function as u8, + first_arg: 0, + expected: [0; 32], + }, + } +} + +impl Case { + /// The operand, `first_arg`. + fn arg(mut self, a: u8) -> Self { + self.witness.first_arg = a; + self + } + + /// `expected`: the value the arm should produce. + fn expect(mut self, expected: [u8; 32]) -> Self { + self.witness.expected = expected; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + run(context, program(), self.witness, expect) + } +} + +#[simplex::test] +fn u4_to_u8(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU8) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn u4_to_u16(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU16) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn u4_to_u32(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU32) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn u4_to_u64(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU64) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn u4_to_u128(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU128) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn u4_to_u256(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(U4ToU256) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn split_u4_into_u1(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=U4_MAX); + + case(SplitU4IntoU1) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn safe_u4_to_u1(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(0..=1); + + case(SafeU4ToU1) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .run(&context) +} + +#[simplex::test] +fn safe_u4_to_u1_overflow(context: simplex::TestContext) -> anyhow::Result<()> { + let a = rand::thread_rng().gen_range(2..=U4_MAX); + + case(SafeU4ToU1) + .arg(a) + .expect(U256::from(a).to_big_endian()) + .expecting(&context, Expect::AssertFailed) +} diff --git a/tests/stdlib/u4/mod.rs b/tests/stdlib/u4/mod.rs new file mode 100644 index 0000000..61da102 --- /dev/null +++ b/tests/stdlib/u4/mod.rs @@ -0,0 +1,2 @@ +mod bit; +mod convert;