Skip to content

Commit 0a835f7

Browse files
authored
Add support for signed arbitrary-ints (danlehmann#90)
1 parent cc4158f commit 0a835f7

File tree

8 files changed

+165
-49
lines changed

8 files changed

+165
-49
lines changed

bitbybit-tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99

1010
[dependencies]
1111
bitbybit = { path = "../bitbybit" }
12-
arbitrary-int = "1.3"
12+
arbitrary-int = "2.0"
1313
defmt = "1"
1414

1515
[features]

bitbybit-tests/src/bitfield_tests.rs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
use arbitrary_int::u6;
2-
use arbitrary_int::Number;
3-
use std::fmt::Debug;
4-
5-
use arbitrary_int::{u1, u12, u13, u14, u2, u24, u3, u30, u4, u48, u5, u57, u7};
1+
use arbitrary_int::prelude::*;
62
use bitbybit::bitenum;
73
use bitbybit::bitfield;
4+
use std::fmt::Debug;
85

96
#[test]
107
fn test_construction() {
@@ -69,6 +66,58 @@ fn test_getter_and_with() {
6966
assert_eq!(0x0122, t.raw_value);
7067
}
7168

69+
#[test]
70+
fn test_getter_and_with_signed() {
71+
#[bitfield(u128, default = 0)]
72+
struct Test2 {
73+
#[bits(98..=127, rw)]
74+
val30: i30,
75+
76+
#[bits(41..=97, rw)]
77+
val57: i57,
78+
79+
#[bits(28..=40, rw)]
80+
val13: i13,
81+
#[bits(12..=27, rw)]
82+
val16: i16,
83+
84+
#[bits(4..=11, rw)]
85+
baudrate: i8,
86+
87+
#[bits(0..=3, rw)]
88+
some_other_bits: i4,
89+
}
90+
91+
let t = Test2::new_with_raw_value(0xAE42_315A_2134_FE06_3412_345A_2134_FE06);
92+
assert_eq!(i30::from_bits(0x2B908C56), t.val30());
93+
assert_eq!(i57::from_bits(0x0110_9A7F_031A_091A), t.val57());
94+
assert_eq!(i13::new(0x5A2), t.val13());
95+
assert_eq!(0x134F, t.val16());
96+
assert_eq!(0xE0u8 as i8, t.baudrate());
97+
assert_eq!(i4::new(0x6), t.some_other_bits());
98+
99+
let t = Test2::DEFAULT
100+
.with_baudrate(0x12)
101+
.with_some_other_bits(i4::new(2));
102+
assert_eq!(0x12, t.baudrate());
103+
assert_eq!(i4::new(2), t.some_other_bits());
104+
assert_eq!(0x0122, t.raw_value);
105+
106+
// Ensure lower fields aren't accidentally sign extending into higher ones
107+
assert_eq!(
108+
0,
109+
Test2::DEFAULT
110+
.with_some_other_bits(i4::from_bits(0b1111))
111+
.baudrate()
112+
);
113+
114+
assert_eq!(0, {
115+
let mut a = Test2::DEFAULT;
116+
a.set_some_other_bits(i4::from_bits(0b1111));
117+
a.baudrate()
118+
});
119+
}
120+
72121
#[test]
73122
fn test_getter_and_with_arbitrary_uint() {
74123
#[bitfield(u128, default = 0)]
@@ -193,6 +242,42 @@ fn test_u1() {
193242
assert_eq!(t3.raw_value, 0b10);
194243
}
195244

245+
#[test]
246+
fn test_i1() {
247+
#[bitfield(u16, default = 0)]
248+
struct Test {
249+
#[bit(0, rw)]
250+
bit0: i1,
251+
252+
#[bit(1, rw)]
253+
bit1: i1,
254+
255+
#[bits(2..=9, r)]
256+
n: u8,
257+
258+
#[bits(10..=12, w)]
259+
m: i3,
260+
261+
#[bits(13..=15, r)]
262+
o: i3,
263+
}
264+
265+
let t = Test::DEFAULT;
266+
assert_eq!(t.bit0(), i1::new(0));
267+
assert_eq!(t.bit1(), i1::new(0));
268+
assert_eq!(t.raw_value, 0b00);
269+
270+
let t2 = t.with_bit0(i1::from_bits(1));
271+
assert_eq!(t2.bit0(), i1::from_bits(1));
272+
assert_eq!(t2.bit1(), i1::from_bits(0));
273+
assert_eq!(t2.raw_value, 0b01);
274+
275+
let t3 = t.with_bit1(i1::new(-1));
276+
assert_eq!(t3.bit0(), i1::from_bits(0));
277+
assert_eq!(t3.bit1(), i1::from_bits(1));
278+
assert_eq!(t3.raw_value, 0b10);
279+
}
280+
196281
#[test]
197282
fn signed_vs_unsigned() {
198283
#[bitfield(u32)]

bitbybit/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## bitbybit 2.0.0
4+
5+
This version expects arbitrary-int 2.x.
6+
7+
### Added
8+
9+
- Support for signed arbitrary-int integers as field types, e.g. `i3`, `i24`, etc.
10+
311
## bitbybit 1.4.0
412

513
This is the final version to support arbitrary-int 1.x. Future versions will require arbitrary-int 2.x.

bitbybit/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "bitbybit"
3-
version = "1.4.0"
3+
version = "2.0.0"
44
authors = ["Daniel Lehmann <danlehmannmuc@gmail.com>"]
55
edition = "2021"
66
description = "Efficient implementation of bit-fields where several numbers are packed within a larger number and bit-enums. Useful for drivers, so it works in no_std environments"
@@ -23,7 +23,7 @@ examples = ["arbitrary-int/defmt"]
2323
syn = { version = "2.0", features = ["full"] }
2424
quote = "1.0"
2525
proc-macro2 = "1.0"
26-
arbitrary-int = "1.3.0"
26+
arbitrary-int = "2.0"
2727

2828
[dev-dependencies]
2929
defmt = "1"

bitbybit/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,11 @@ struct GICD_TYPER {
284284

285285
## Dependencies
286286

287-
Arbitrary bit widths like u5 or u67 do not exist in Rust at the moment. Therefore, the following dependency is required:
287+
Arbitrary bit widths like u5, u67 or i81 do not exist in Rust at the moment. Therefore, the following dependency is
288+
required:
288289

289290
```toml
290-
arbitrary-int = "1.3.0"
291+
arbitrary-int = "2.0"
291292
```
292293

293294
## Usage

bitbybit/src/bitfield/codegen.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,14 @@ pub fn generate(
134134
quote! { field_value }
135135
}
136136
} else {
137-
// Once signed arbitrary-ints (e.g. i7) are a thing, we'll need to pay special attention to sign extension here
138-
quote! { field_value.value() }
137+
// For arbitrary-ints, signed numbers provide to_bits() which return an
138+
// unsigned, non-sign extended number. Using value() would be incorrect
139+
// here as the sign bit would pollute fields defined higher up.
140+
if field_definition.is_signed {
141+
quote! { field_value.to_bits() }
142+
} else {
143+
quote! { field_value.value() }
144+
}
139145
}
140146
}
141147
CustomType::Yes(_) => {
@@ -260,8 +266,9 @@ fn extracted_bits(
260266
quote! { #packed as #primitive_type }
261267
}
262268
} else {
269+
let prefix = if field_definition.is_signed { "i" } else { "u" };
263270
let custom_type =
264-
TokenStream2::from_str(format!("arbitrary_int::u{}", total_number_bits).as_str())
271+
TokenStream2::from_str(format!("arbitrary_int::{prefix}{total_number_bits}").as_str())
265272
.unwrap();
266273
let extract =
267274
TokenStream2::from_str(format!("extract_u{}", base_data_size.internal).as_str())

bitbybit/src/bitfield/mod.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,32 @@ const fn is_int_size_regular_type(size: usize) -> bool {
2323
size == BITCOUNT_BOOL || size == 8 || size == 16 || size == 32 || size == 64 || size == 128
2424
}
2525

26-
fn try_parse_arbitrary_int_type(s: &str) -> Option<usize> {
27-
if !s.starts_with('u') || s.len() < 2 {
26+
/// Parses an integer string, like "u8" or "i24".
27+
/// Allows "i" types if allow_signed is true.
28+
///
29+
/// Returns Some((size, is_signed)) if successful, None if not.
30+
fn try_parse_arbitrary_int_type(s: &str, allow_signed: bool) -> Option<(usize, bool)> {
31+
if s.len() < 2 {
2832
return None;
2933
}
3034

31-
let size = usize::from_str(s.split_at(1).1);
32-
match size {
33-
Ok(size) => {
34-
if (1..128).contains(&size) && !is_int_size_regular_type(size) {
35-
Some(size)
36-
} else {
37-
None
35+
let is_unsigned = s.starts_with('u');
36+
let is_signed = s.starts_with('i');
37+
38+
if is_unsigned || (is_signed && allow_signed) {
39+
let size = usize::from_str(s.split_at(1).1);
40+
match size {
41+
Ok(size) => {
42+
if (1..128).contains(&size) && !is_int_size_regular_type(size) {
43+
Some((size, is_signed))
44+
} else {
45+
None
46+
}
3847
}
48+
Err(_) => None,
3949
}
40-
Err(_) => None,
50+
} else {
51+
None
4152
}
4253
}
4354

@@ -51,6 +62,7 @@ struct FieldDefinition {
5162
getter_type: Option<Type>,
5263
setter_type: Option<Type>,
5364
field_type_size_from_data_type: Option<usize>,
65+
is_signed: bool,
5466
/// If non-null: (count, stride)
5567
use_regular_int: bool,
5668
primitive_type: TokenStream2,
@@ -238,8 +250,8 @@ pub fn bitfield(args: TokenStream, input: TokenStream) -> TokenStream {
238250
"u32" => BaseDataSize::new(32),
239251
"u64" => BaseDataSize::new(64),
240252
"u128" => BaseDataSize::new(128),
241-
s if try_parse_arbitrary_int_type(s).is_some() => {
242-
BaseDataSize::new(try_parse_arbitrary_int_type(s).unwrap())
253+
s if try_parse_arbitrary_int_type(s, false).is_some() => {
254+
BaseDataSize::new(try_parse_arbitrary_int_type(s, false).unwrap().0)
243255
}
244256
_ => {
245257
return syn::Error::new_spanned(

bitbybit/src/bitfield/parsing.rs

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@ pub fn parse(fields: &Fields, base_data_size: BaseDataSize) -> Result<Vec<FieldD
2323
Ok(field_definitions)
2424
}
2525

26-
fn parse_scalar_field(ty: &Type) -> Result<(Option<usize>, bool)> {
26+
fn parse_scalar_field(ty: &Type) -> Result<Option<(usize, bool)>> {
2727
match ty {
2828
Type::Path(path) => {
2929
let type_str = path.to_token_stream().to_string();
3030
let result = match type_str.as_str() {
31-
"bool" => Some((Some(BITCOUNT_BOOL), false)),
32-
"u8" => Some((Some(8), false)),
33-
"i8" => Some((Some(8), true)),
34-
"u16" => Some((Some(16), false)),
35-
"i16" => Some((Some(16), true)),
36-
"u32" => Some((Some(32), false)),
37-
"i32" => Some((Some(32), true)),
38-
"u64" => Some((Some(64), false)),
39-
"i64" => Some((Some(64), true)),
40-
"u128" => Some((Some(128), false)),
41-
"i128" => Some((Some(128), true)),
31+
"bool" => Some(Some((BITCOUNT_BOOL, false))),
32+
"u8" => Some(Some((8, false))),
33+
"i8" => Some(Some((8, true))),
34+
"u16" => Some(Some((16, false))),
35+
"i16" => Some(Some((16, true))),
36+
"u32" => Some(Some((32, false))),
37+
"i32" => Some(Some((32, true))),
38+
"u64" => Some(Some((64, false))),
39+
"i64" => Some(Some((64, true))),
40+
"u128" => Some(Some((128, false))),
41+
"i128" => Some(Some((128, true))),
4242
_ => None,
4343
};
4444

@@ -47,15 +47,15 @@ fn parse_scalar_field(ty: &Type) -> Result<(Option<usize>, bool)> {
4747
}
4848

4949
if let Some(last_segment) = path.path.segments.last() {
50-
return Ok((try_parse_arbitrary_int_type(&last_segment.ident.to_string()), false));
50+
return Ok(try_parse_arbitrary_int_type(&last_segment.ident.to_string(), true));
5151
}
5252

5353
Err(Error::new(path.span(), "invalid path for bitfield field"))
5454
}
5555
_ => Err(Error::new(
5656
ty.span(),
5757
format!(
58-
"bitfield!: Field type {} not valid. Supported types: bool, u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, arbitrary int (e.g., u1, u3, u62). Their arrays are also supported.",
58+
"bitfield!: Field type {} not valid. Supported types: bool, u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, arbitrary int (e.g., u1, u3, u62, i81). Their arrays are also supported.",
5959
ty.into_token_stream()
6060
),
6161
)),
@@ -132,14 +132,16 @@ fn parse_field(base_data_size: usize, field: &Field) -> Result<FieldDefinition>
132132
_ => (&field.ty, None),
133133
}
134134
};
135-
let (field_type_size_from_data_type, field_type_is_signed) = parse_scalar_field(ty)?;
136-
let unsigned_field_type = if field_type_is_signed {
137-
Some(
138-
syn::parse_str::<Type>(
139-
format!("u{}", field_type_size_from_data_type.unwrap()).as_str(),
135+
let field_type_size_from_data_type = parse_scalar_field(ty)?;
136+
let unsigned_field_type = if let Some((bits, is_signed)) = field_type_size_from_data_type {
137+
if is_signed {
138+
Some(
139+
syn::parse_str::<Type>(format!("u{}", bits).as_str())
140+
.unwrap_or_else(|_| panic!("bitfield!: Error parsing unsigned_field_type")),
140141
)
141-
.unwrap_or_else(|_| panic!("bitfield!: Error parsing unsigned_field_type")),
142-
)
142+
} else {
143+
None
144+
}
143145
} else {
144146
None
145147
};
@@ -323,7 +325,7 @@ fn parse_field(base_data_size: usize, field: &Field) -> Result<FieldDefinition>
323325
panic!("bitfield!: number_of_bits is too large!")
324326
}
325327
}),
326-
Some(b) => (b, quote! { #ty }),
328+
Some((b, _is_signed)) => (b, quote! { #ty }),
327329
};
328330

329331
if field_type_size == BITCOUNT_BOOL {
@@ -401,7 +403,7 @@ fn parse_field(base_data_size: usize, field: &Field) -> Result<FieldDefinition>
401403
};
402404

403405
let use_regular_int = match field_type_size_from_data_type {
404-
Some(i) => is_int_size_regular_type(i),
406+
Some((i, _is_signed)) => is_int_size_regular_type(i),
405407
None => {
406408
// For CustomTypes (e.g. enums), prefer u1 over bool
407409
number_of_bits != 1 && is_int_size_regular_type(number_of_bits)
@@ -427,7 +429,8 @@ fn parse_field(base_data_size: usize, field: &Field) -> Result<FieldDefinition>
427429
custom_type,
428430
doc_comment,
429431
array: indexed_count.map(|count| (count, indexed_stride.unwrap())),
430-
field_type_size_from_data_type,
432+
field_type_size_from_data_type: field_type_size_from_data_type.map(|v| v.0),
433+
is_signed: field_type_size_from_data_type.map_or(false, |v| v.1),
431434
unsigned_field_type,
432435
})
433436
}

0 commit comments

Comments
 (0)