Skip to content

Commit 6dde184

Browse files
giladchaseGilad Chase
andauthored
feat(byte_array): add get(usize) and index(usize) to ByteSpan (#8427)
Co-authored-by: Gilad Chase <[email protected]>
1 parent f158822 commit 6dde184

File tree

3 files changed

+168
-8
lines changed

3 files changed

+168
-8
lines changed

corelib/src/byte_array.cairo

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -839,8 +839,7 @@ pub impl ByteSpanImpl of ByteSpanTrait {
839839
}
840840

841841
/// Gets the element(s) at the given index.
842-
/// Accepts ranges (returns Option<ByteSpan>), and (to-be-implemented) single indices (returns
843-
/// Option<u8>).
842+
/// Accepts ranges (returns Option<ByteSpan>), and single indices (returns Option<u8>).
844843
#[feature("corelib-get-trait")]
845844
fn get<I, impl TGet: crate::ops::Get<ByteSpan, I>, +Drop<I>>(
846845
self: @ByteSpan, index: I,
@@ -909,6 +908,16 @@ impl ByteSpanGetRangeInclusive of crate::ops::Get<ByteSpan, crate::ops::RangeInc
909908
}
910909
}
911910

911+
impl ByteSpanGetUsize of crate::ops::Get<ByteSpan, usize> {
912+
type Output = u8;
913+
914+
/// Returns the byte at the given index.
915+
/// If out of bounds: returns `None`.
916+
fn get(self: @ByteSpan, index: usize) -> Option<u8> {
917+
helpers::byte_at(self, index)
918+
}
919+
}
920+
912921
#[feature("byte-span")]
913922
impl ByteSpanIndexViewRange of crate::ops::IndexView<ByteSpan, crate::ops::Range<usize>> {
914923
type Target = ByteSpan;
@@ -929,6 +938,14 @@ impl ByteSpanIndexViewRangeInclusive of crate::ops::IndexView<
929938
}
930939
}
931940

941+
impl ByteSpanIndex of core::ops::index::IndexView<ByteSpan, usize> {
942+
type Target = u8;
943+
944+
fn index(self: @ByteSpan, index: usize) -> u8 {
945+
ByteSpanTrait::get(self, index).expect('Index out of bounds')
946+
}
947+
}
948+
932949
/// Trait for types that can be converted into a `ByteSpan`.
933950
#[unstable(feature: "byte-span")]
934951
pub trait ToByteSpanTrait<C> {
@@ -973,18 +990,21 @@ fn shift_right(word: felt252, word_len: usize, n_bytes: usize) -> felt252 {
973990

974991
mod helpers {
975992
use core::num::traits::Bounded;
976-
use crate::bytes_31::BYTES_IN_BYTES31;
993+
use crate::bytes_31::{BYTES_IN_BYTES31, Bytes31Trait, u8_at_u256};
977994
#[feature("bounded-int-utils")]
978995
use crate::internal::bounded_int::{
979-
self, AddHelper, BoundedInt, ConstrainHelper, MulHelper, SubHelper, UnitInt, downcast,
980-
upcast,
996+
self, AddHelper, BoundedInt, ConstrainHelper, DivRemHelper, MulHelper, SubHelper, UnitInt,
997+
downcast, upcast,
981998
};
982999
use super::{BYTES_IN_BYTES31_MINUS_ONE, ByteSpan, Bytes31Index};
9831000

9841001
type BytesInBytes31Typed = UnitInt<{ BYTES_IN_BYTES31.into() }>;
9851002

9861003
const U32_MAX_TIMES_B31: felt252 = Bounded::<u32>::MAX.into() * BYTES_IN_BYTES31.into();
9871004
const BYTES_IN_BYTES31_UNIT_INT: BytesInBytes31Typed = downcast(BYTES_IN_BYTES31).unwrap();
1005+
const NZ_BYTES_IN_BYTES31: NonZero<BytesInBytes31Typed> = 31;
1006+
const BYTES_IN_BYTES31_MINUS_ONE_TYPED: UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }> = 30;
1007+
const ONE_TYPED: UnitInt<1> = 1;
9881008

9891009
impl U32ByB31 of MulHelper<u32, BytesInBytes31Typed> {
9901010
type Result = BoundedInt<0, U32_MAX_TIMES_B31>;
@@ -1002,6 +1022,50 @@ mod helpers {
10021022
>;
10031023
}
10041024

1025+
// For byte_at: usize + BoundedInt<0,30>
1026+
impl UsizeAddBytes31Index of AddHelper<usize, Bytes31Index> {
1027+
type Result =
1028+
BoundedInt<0, { Bounded::<usize>::MAX.into() + BYTES_IN_BYTES31_MINUS_ONE.into() }>;
1029+
}
1030+
1031+
// For byte_at: div_rem of (usize + BoundedInt<0,30>) by 31
1032+
const USIZE_PLUS_30_DIV_31: felt252 = (Bounded::<usize>::MAX / 31 + 1).into();
1033+
impl UsizePlusBytes31IndexDivRemB31 of DivRemHelper<
1034+
UsizeAddBytes31Index::Result, BytesInBytes31Typed,
1035+
> {
1036+
type DivT = BoundedInt<0, USIZE_PLUS_30_DIV_31>;
1037+
type RemT = Bytes31Index;
1038+
}
1039+
1040+
// For byte_at: 30 - BoundedInt<0,30>
1041+
impl B30SubBytes31Index of SubHelper<
1042+
UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }>, Bytes31Index,
1043+
> {
1044+
type Result = Bytes31Index;
1045+
}
1046+
1047+
// For byte_at: BoundedInt<0,30> - 1
1048+
impl Bytes31IndexSub1 of SubHelper<Bytes31Index, UnitInt<1>> {
1049+
type Result = BoundedInt<-1, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1050+
}
1051+
1052+
// For byte_at: (BoundedInt<0,30> - 1) - BoundedInt<0,30>
1053+
impl Bytes31IndexMinus1SubBytes31Index of SubHelper<Bytes31IndexSub1::Result, Bytes31Index> {
1054+
type Result =
1055+
BoundedInt<
1056+
{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1057+
{ BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1058+
>;
1059+
}
1060+
1061+
// For byte_at: split BoundedInt<-31, 29> at 0.
1062+
impl ConstrainRemainderIndexAt0 of bounded_int::ConstrainHelper<
1063+
Bytes31IndexMinus1SubBytes31Index::Result, 0,
1064+
> {
1065+
type LowT = BoundedInt<{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }, -1>;
1066+
type HighT = BoundedInt<0, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1067+
}
1068+
10051069
/// Calculates the length of a `ByteSpan` in bytes.
10061070
pub fn calc_bytespan_len(span: ByteSpan) -> usize {
10071071
let data_bytes = bounded_int::mul(span.data.len(), BYTES_IN_BYTES31_UNIT_INT);
@@ -1091,5 +1155,39 @@ mod helpers {
10911155
pub fn length_minus_one(len: BoundedInt<1, 31>) -> Bytes31Index {
10921156
bounded_int::sub(len, 1)
10931157
}
1158+
/// Returns the byte at the given index in the ByteSpan.
1159+
/// If out of bounds: returns `None`.
1160+
pub fn byte_at(self: @ByteSpan, index: usize) -> Option<u8> {
1161+
let absolute_index = bounded_int::add(index, *self.first_char_start_offset);
1162+
let (word_index_bounded, msb_index) = bounded_int::div_rem(
1163+
absolute_index, NZ_BYTES_IN_BYTES31,
1164+
);
1165+
1166+
let word_index = upcast(word_index_bounded);
1167+
match self.data.get(word_index) {
1168+
Some(word) => {
1169+
// Convert from MSB to LSB indexing.
1170+
let lsb_index = bounded_int::sub(BYTES_IN_BYTES31_MINUS_ONE_TYPED, msb_index);
1171+
Some(word.at(upcast(lsb_index)))
1172+
},
1173+
None => {
1174+
// Word index must equal data.len() for remainder word.
1175+
if word_index != self.data.len() {
1176+
return None;
1177+
}
1178+
1179+
// Compute LSB index: remainder_len - 1 - msb_index.
1180+
let lsb_index_bounded = bounded_int::sub(
1181+
bounded_int::sub(*self.remainder_len, ONE_TYPED), msb_index,
1182+
);
1183+
1184+
// Check if in bounds and extract non-negative index.
1185+
let Err(lsb_index) = bounded_int::constrain::<_, 0>(lsb_index_bounded) else {
1186+
return None; // Out of bounds: index >= remainder_len.
1187+
};
1188+
Some(u8_at_u256((*self.remainder_word).into(), upcast(lsb_index)))
1189+
},
1190+
}
1191+
}
10941192
}
10951193
pub(crate) use helpers::len_parts;

corelib/src/ops/get.cairo

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
/// let ba: ByteArray = "hello";
2121
/// let span = ba.span();
2222
///
23+
/// // Using usize.
24+
/// let byte = span.get(1).unwrap();
25+
/// assert!(byte == 'e');
26+
///
2327
/// // Using Range<usize>.
2428
/// let slice = span.get(1..4).unwrap();
2529
/// assert_eq!(slice.to_byte_array(), "ell");
@@ -29,6 +33,7 @@
2933
/// assert_eq!(slice.to_byte_array(), "ell");
3034
///
3135
/// // Out of bounds returns None.
36+
/// assert!(span.get(10).is_none());
3237
/// assert!(span.get(10..20).is_none());
3338
/// ```
3439
// TODO(giladchase): add examples for `usize` once supported.

corelib/src/test/byte_array_test.cairo

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#[feature("byte-span")]
2-
use crate::byte_array::{ByteSpan, ByteSpanTrait, ToByteSpanTrait};
3-
use crate::num::traits::Bounded;
4-
use crate::test::test_utils::{assert_eq, assert_ne};
2+
use core::byte_array::{ByteSpan, ByteSpanTrait, ToByteSpanTrait};
3+
use core::num::traits::Bounded;
4+
use core::test::test_utils::{assert_eq, assert_ne};
55

66
#[test]
77
fn test_append_byte() {
@@ -681,3 +681,60 @@ fn test_span_multiple_start_offset_slicing() {
681681
assert_eq!(slice2_inc.map(tba), Some("cdef"));
682682
assert_eq!(slice3_inc.map(tba), Some("def"));
683683
}
684+
685+
#[test]
686+
fn test_span_at_and_index() {
687+
// Test simple access.
688+
let ba: ByteArray = "AB";
689+
let span = ba.span();
690+
assert_eq!(span[0_usize], 'A');
691+
assert_eq!(span.get(1_usize), Some('B'));
692+
assert_eq!(span.get(2_usize), None);
693+
694+
// Test with offset and two words.
695+
let ba_33: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg";
696+
let mut span = ba_33.span();
697+
span = span.get(1..33).unwrap();
698+
assert_eq!(span.get(0_usize), Some('B'));
699+
assert_eq!(span.get(30_usize), Some('f'));
700+
assert_eq!(span[31_usize], 'g');
701+
assert_eq!(span.get(32_usize), None);
702+
703+
// Test with offset and two words.
704+
// 64 bytes: 31 + 31 + 2.
705+
let ba_64: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$";
706+
let mut span = ba_64.span();
707+
span = span.get(1..64).unwrap();
708+
assert_eq!(span.get(30_usize), Some('f'));
709+
assert_eq!(span[31_usize], 'g');
710+
assert_eq!(span.get(60_usize), Some('9'));
711+
assert_eq!(span[61_usize], '#');
712+
assert_eq!(span.get(62_usize), Some('$'));
713+
assert_eq!(span.get(63_usize), None);
714+
715+
// Test empty span.
716+
let empty: ByteArray = Default::default();
717+
let empty_span = empty.span();
718+
assert_eq!(empty_span.get(0_usize), None);
719+
}
720+
721+
#[test]
722+
#[should_panic(expected: ('Index out of bounds',))]
723+
fn test_span_index_out_of_bounds() {
724+
let ba: ByteArray = "AB";
725+
let span = ba.span();
726+
let _x = span[2_usize]; // Should panic
727+
}
728+
729+
#[test]
730+
fn test_span_at_overflows() {
731+
// Test overflow protection with large indices.
732+
let ba: ByteArray = "test";
733+
let span = ba.span();
734+
735+
assert_eq!(span.get(Bounded::<usize>::MAX), None);
736+
737+
let sliced = ba.span().get(1..3).unwrap();
738+
assert_eq!(sliced.get(Bounded::<usize>::MAX - 1), None);
739+
assert_eq!(sliced.get(Bounded::<usize>::MAX), None);
740+
}

0 commit comments

Comments
 (0)