Skip to content

Commit 2280b52

Browse files
author
Gilad Chase
committed
feat(byte_array): add get(usize) and index(usize) to ByteSpan
1 parent fdf41e3 commit 2280b52

File tree

3 files changed

+169
-5
lines changed

3 files changed

+169
-5
lines changed

corelib/src/byte_array.cairo

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

842842
/// Gets the element(s) at the given index.
843-
/// Accepts ranges (returns Option<ByteSpan>), and (to-be-implemented) single indices (returns
844-
/// Option<u8>).
843+
/// Accepts ranges (returns Option<ByteSpan>), and single indices (returns Option<u8>).
845844
#[feature("get-trait")]
846845
fn get<I, impl TGet: crate::ops::Get<ByteSpan, I>, +Drop<I>>(
847846
self: @ByteSpan, index: I,
@@ -913,6 +912,24 @@ impl ByteSpanGetRangeInclusive of crate::ops::Get<ByteSpan, crate::ops::RangeInc
913912
}
914913
}
915914

915+
impl ByteSpanGetUsize of crate::ops::Get<ByteSpan, usize> {
916+
type Output = u8;
917+
918+
/// Returns the byte at the given index.
919+
/// If out of bounds: returns `None`.
920+
fn get(self: @ByteSpan, index: usize) -> Option<u8> {
921+
helpers::byte_at(self, index)
922+
}
923+
}
924+
925+
impl ByteSpanIndex of core::ops::index::IndexView<ByteSpan, usize> {
926+
type Target = u8;
927+
928+
fn index(self: @ByteSpan, index: usize) -> u8 {
929+
ByteSpanTrait::get(self, index).expect('Index out of bounds')
930+
}
931+
}
932+
916933
/// Trait for types that can be converted into a `ByteSpan`.
917934
#[unstable(feature: "byte-span")]
918935
pub trait ToByteSpanTrait<C> {
@@ -957,18 +974,21 @@ fn shift_right(word: felt252, word_len: usize, n_bytes: usize) -> felt252 {
957974

958975
mod helpers {
959976
use core::num::traits::Bounded;
960-
use crate::bytes_31::BYTES_IN_BYTES31;
977+
use crate::bytes_31::{BYTES_IN_BYTES31, Bytes31Trait, u8_at_u256};
961978
#[feature("bounded-int-utils")]
962979
use crate::internal::bounded_int::{
963-
self, AddHelper, BoundedInt, ConstrainHelper, MulHelper, SubHelper, UnitInt, downcast,
964-
upcast,
980+
self, AddHelper, BoundedInt, ConstrainHelper, DivRemHelper, MulHelper, SubHelper, UnitInt,
981+
downcast, upcast,
965982
};
966983
use super::{BYTES_IN_BYTES31_MINUS_ONE, ByteSpan, Bytes31Index};
967984

968985
type BytesInBytes31Typed = UnitInt<{ BYTES_IN_BYTES31.into() }>;
969986

970987
const U32_MAX_TIMES_B31: felt252 = Bounded::<u32>::MAX.into() * BYTES_IN_BYTES31.into();
971988
const BYTES_IN_BYTES31_UNIT_INT: BytesInBytes31Typed = downcast(BYTES_IN_BYTES31).unwrap();
989+
const NZ_BYTES_IN_BYTES31: NonZero<BytesInBytes31Typed> = 31;
990+
const BYTES_IN_BYTES31_MINUS_ONE_TYPED: UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }> = 30;
991+
const ONE_TYPED: UnitInt<1> = 1;
972992

973993
impl U32ByB31 of MulHelper<u32, BytesInBytes31Typed> {
974994
type Result = BoundedInt<0, U32_MAX_TIMES_B31>;
@@ -986,6 +1006,50 @@ mod helpers {
9861006
>;
9871007
}
9881008

1009+
// For byte_at: usize + BoundedInt<0,30>
1010+
impl UsizeAddBytes31Index of AddHelper<usize, Bytes31Index> {
1011+
type Result =
1012+
BoundedInt<0, { Bounded::<usize>::MAX.into() + BYTES_IN_BYTES31_MINUS_ONE.into() }>;
1013+
}
1014+
1015+
// For byte_at: div_rem of (usize + BoundedInt<0,30>) by 31
1016+
const USIZE_PLUS_30_DIV_31: felt252 = 0x8421085; // (usize::MAX + 30) / 31 = 138547333
1017+
impl UsizePlusBytes31IndexDivRemB31 of DivRemHelper<
1018+
UsizeAddBytes31Index::Result, BytesInBytes31Typed,
1019+
> {
1020+
type DivT = BoundedInt<0, USIZE_PLUS_30_DIV_31>;
1021+
type RemT = Bytes31Index;
1022+
}
1023+
1024+
// For byte_at: 30 - BoundedInt<0,30>
1025+
impl B30SubBytes31Index of SubHelper<
1026+
UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }>, Bytes31Index,
1027+
> {
1028+
type Result = Bytes31Index;
1029+
}
1030+
1031+
// For byte_at: BoundedInt<0,30> - 1
1032+
impl Bytes31IndexSub1 of SubHelper<Bytes31Index, UnitInt<1>> {
1033+
type Result = BoundedInt<-1, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1034+
}
1035+
1036+
// For byte_at: (BoundedInt<0,30> - 1) - BoundedInt<0,30>
1037+
impl Bytes31IndexMinus1SubBytes31Index of SubHelper<Bytes31IndexSub1::Result, Bytes31Index> {
1038+
type Result =
1039+
BoundedInt<
1040+
{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1041+
{ BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1042+
>;
1043+
}
1044+
1045+
// For byte_at: split BoundedInt<-31, 29> at 0.
1046+
impl ConstrainRemainderIndexAt0 of bounded_int::ConstrainHelper<
1047+
Bytes31IndexMinus1SubBytes31Index::Result, 0,
1048+
> {
1049+
type LowT = BoundedInt<{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }, -1>;
1050+
type HighT = BoundedInt<0, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1051+
}
1052+
9891053
/// Calculates the length of a `ByteSpan` in bytes.
9901054
pub fn calc_bytespan_len(span: ByteSpan) -> usize {
9911055
let data_bytes = bounded_int::mul(span.data.len(), BYTES_IN_BYTES31_UNIT_INT);
@@ -1075,5 +1139,39 @@ mod helpers {
10751139
pub fn length_minus_one(len: BoundedInt<1, 31>) -> Bytes31Index {
10761140
bounded_int::sub(len, 1)
10771141
}
1142+
/// Returns the byte at the given index in the ByteSpan.
1143+
/// If out of bounds: returns `None`.
1144+
pub fn byte_at(self: @ByteSpan, index: usize) -> Option<u8> {
1145+
let absolute_index = bounded_int::add(index, *self.first_char_start_offset);
1146+
let (word_index_bounded, msb_index) = bounded_int::div_rem(
1147+
absolute_index, NZ_BYTES_IN_BYTES31,
1148+
);
1149+
1150+
let word_index = upcast(word_index_bounded);
1151+
match self.data.get(word_index) {
1152+
Some(word) => {
1153+
// Convert from MSB to LSB indexing.
1154+
let lsb_index = bounded_int::sub(BYTES_IN_BYTES31_MINUS_ONE_TYPED, msb_index);
1155+
Some(word.at(upcast(lsb_index)))
1156+
},
1157+
None => {
1158+
// Word index must equal data.len() for remainder word.
1159+
if word_index != self.data.len() {
1160+
return None;
1161+
}
1162+
1163+
// Compute LSB index: remainder_len - 1 - msb_index.
1164+
let lsb_index_bounded = bounded_int::sub(
1165+
bounded_int::sub(*self.remainder_len, ONE_TYPED), msb_index,
1166+
);
1167+
1168+
// Check if in bounds and extract non-negative index.
1169+
let Err(lsb_index) = bounded_int::constrain::<_, 0>(lsb_index_bounded) else {
1170+
return None; // Out of bounds: index >= remainder_len.
1171+
};
1172+
Some(u8_at_u256((*self.remainder_word).into(), upcast(lsb_index)))
1173+
},
1174+
}
1175+
}
10781176
}
10791177
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,3 +789,64 @@ fn test_span_multiple_start_offset_slicing() {
789789
result3 = slice3_inc.to_byte_array();
790790
assert_eq!(result3, "def", "third slice (inclusive)");
791791
}
792+
793+
#[test]
794+
fn test_span_at_and_index() {
795+
// Test simple access.
796+
let ba: ByteArray = "AB";
797+
let span = ba.span();
798+
assert_eq!(span.get(0_usize), Some('A'));
799+
assert_eq!(span.get(1_usize), Some('B'));
800+
assert_eq!(span.get(2_usize), None);
801+
802+
// Test index operator on same span.
803+
assert_eq!(span[0], 'A');
804+
assert_eq!(span[1], 'B');
805+
806+
// Test with offset and two words.
807+
let ba_33: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg";
808+
let mut span = ba_33.span();
809+
span = span.get(1..33).unwrap();
810+
assert_eq!(span.get(0_usize), Some('B'));
811+
assert_eq!(span.get(30_usize), Some('f'));
812+
assert_eq!(span.get(31_usize), Some('g'));
813+
assert_eq!(span.get(32_usize), None);
814+
815+
// Test with offset and two words.
816+
// 64 bytes: 31 + 31 + 2.
817+
let ba_64: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$";
818+
let mut span = ba_64.span();
819+
span = span.get(1..64).unwrap();
820+
assert_eq!(span.get(30_usize), Some('f'), "byte 30 - last of 1nd word");
821+
assert_eq!(span.get(31_usize), Some('g'), "byte 31 - first of 2nd word");
822+
assert_eq!(span.get(60_usize), Some('9'), "byte 60 - last of 2nd word");
823+
assert_eq!(span.get(61_usize), Some('#'), "byte 61 - first in last_word");
824+
assert_eq!(span.get(62_usize), Some('$'), "byte 62 - last in last_word");
825+
assert_eq!(span.get(63_usize), None);
826+
827+
// Test empty span.
828+
let empty: ByteArray = Default::default();
829+
let empty_span = empty.span();
830+
assert_eq!(empty_span.get(0_usize), None);
831+
}
832+
833+
#[test]
834+
#[should_panic(expected: ('Index out of bounds',))]
835+
fn test_span_index_out_of_bounds() {
836+
let ba: ByteArray = "AB";
837+
let span = ba.span();
838+
let _x = span[2]; // Should panic
839+
}
840+
841+
#[test]
842+
fn test_span_at_overflows() {
843+
// Test overflow protection with large indices.
844+
let ba: ByteArray = "test";
845+
let span = ba.span();
846+
847+
assert_eq!(span.get(Bounded::<usize>::MAX), None);
848+
849+
let sliced = ba.span().get(1..3).unwrap();
850+
assert_eq!(sliced.get(Bounded::<usize>::MAX - 1), None);
851+
assert_eq!(sliced.get(Bounded::<usize>::MAX), None);
852+
}

0 commit comments

Comments
 (0)