Skip to content

Commit 9d086a3

Browse files
bobzhangclaude
andcommitted
feat(builtin): add clamped_view for FixedArray/ReadOnlyArray
Completes the total view variant across the whole family. `FixedArray` mirrors `Array::clamped_view` through `unsafe_cast_fixedarray_to_uninitializedarray`; `ReadOnlyArray` delegates to `FixedArray::clamped_view` via `unsafe_reinterpret_to_fixed_array()`, the same way its existing `view` and `get_view` delegate. Contract is unchanged: both offsets are clamped independently into `[0, length()]`, an inverted range yields an empty view at the clamped `start`, and the result aliases the backing storage. Codex CLI review: "No blocking issues in the uncommitted diff. `ReadOnlyArray` correctly forwards `start` in both arms. Omitting `end` produces exactly the same bounds as explicitly passing `length()` [...] Clamping guarantees `0 <= lo <= len` and `0 <= count <= len - lo`; subtraction cannot overflow." It also spotted a real gap: with the `end=None` arm exercised only at starts 0, -5 and 100, a `start * 2` mutation there survived the suite. The added test "clamped_view with `end` omitted forwards `start` unchanged" compares the omitted-`end` result against an explicit `end=5` across every start in -1..=6, including `start_offset()`; that mutation now fails 1 test. Signed-off-by: Codex CLI <codex@openai.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4N5Hc1TYuJW5oRDTY19Wf
1 parent 331c3e8 commit 9d086a3

4 files changed

Lines changed: 186 additions & 0 deletions

File tree

builtin/arrayview.mbt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,44 @@ pub fn[T] FixedArray::get_view(
636636
)
637637
}
638638

639+
///|
640+
/// Creates a view of a portion of the fixed array, clamping the requested
641+
/// range instead of rejecting it. Same contract as `Array::clamped_view`.
642+
///
643+
/// Example:
644+
///
645+
/// ```mbt check
646+
/// test {
647+
/// let arr : FixedArray[Int] = [1, 2, 3, 4, 5]
648+
/// debug_inspect(
649+
/// arr.clamped_view(start=3, end=10),
650+
/// content=(
651+
/// #|<ArrayView: [4, 5]>
652+
/// ),
653+
/// )
654+
/// debug_inspect(
655+
/// arr.clamped_view(start=4, end=1),
656+
/// content=(
657+
/// #|<ArrayView: []>
658+
/// ),
659+
/// )
660+
/// }
661+
/// ```
662+
pub fn[T] FixedArray::clamped_view(
663+
self : FixedArray[T],
664+
start? : Int = 0,
665+
end? : Int,
666+
) -> ArrayView[T] {
667+
let len = self.length()
668+
let lo = clamp_offset(start, len)
669+
let hi = match end {
670+
None => len
671+
Some(end) => clamp_offset(end, len)
672+
}
673+
let count = if hi > lo { hi - lo } else { 0 }
674+
ArrayView::make(unsafe_cast_fixedarray_to_uninitializedarray(self), lo, count)
675+
}
676+
639677
///|
640678
/// Return an iterator over suffix views of this array view.
641679
///

builtin/clamped_view_test.mbt

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,115 @@ test "clamped_view saturates at the extremes of Int" {
203203
inspect(bv.clamped_view(start=lo, end=hi) == bv, content="true")
204204
inspect(bv.clamped_view(start=hi, end=lo).start_offset(), content="4")
205205
}
206+
207+
///|
208+
test "FixedArray::clamped_view clamps out-of-range and inverted offsets" {
209+
let arr : FixedArray[Int] = [1, 2, 3, 4, 5]
210+
json_inspect(arr.clamped_view().to_owned(), content=[1, 2, 3, 4, 5])
211+
json_inspect(arr.clamped_view(start=1, end=4).to_owned(), content=[2, 3, 4])
212+
json_inspect(arr.clamped_view(end=100).to_owned(), content=[1, 2, 3, 4, 5])
213+
json_inspect(arr.clamped_view(start=-5).to_owned(), content=[1, 2, 3, 4, 5])
214+
json_inspect(arr.clamped_view(start=100).to_owned(), content=[])
215+
json_inspect(arr.clamped_view(start=4, end=1).to_owned(), content=[])
216+
json_inspect(arr.clamped_view(start=-9, end=-3).to_owned(), content=[])
217+
json_inspect(
218+
arr.clamped_view(start=@int.MIN_VALUE, end=@int.MAX_VALUE).to_owned(),
219+
content=[1, 2, 3, 4, 5],
220+
)
221+
json_inspect(
222+
arr.clamped_view(start=@int.MAX_VALUE, end=@int.MIN_VALUE).to_owned(),
223+
content=[],
224+
)
225+
json_inspect(([] : FixedArray[Int]).clamped_view(end=10).to_owned(), content=[])
226+
// empty results sit at the clamped start
227+
inspect(arr.clamped_view(start=4, end=1).start_offset(), content="4")
228+
inspect(arr.clamped_view(start=100).start_offset(), content="5")
229+
inspect(arr.clamped_view(start=-9, end=-3).start_offset(), content="0")
230+
}
231+
232+
///|
233+
test "FixedArray::clamped_view aliases the backing array" {
234+
let arr : FixedArray[Int] = [1, 2, 3, 4, 5]
235+
let v = arr.clamped_view(start=1, end=100)
236+
arr[1] = 20
237+
json_inspect(v.to_owned(), content=[20, 3, 4, 5])
238+
}
239+
240+
///|
241+
test "ReadOnlyArray::clamped_view clamps out-of-range and inverted offsets" {
242+
let arr : ReadOnlyArray[Int] = [1, 2, 3, 4, 5]
243+
json_inspect(arr.clamped_view().to_owned(), content=[1, 2, 3, 4, 5])
244+
json_inspect(arr.clamped_view(start=1, end=4).to_owned(), content=[2, 3, 4])
245+
json_inspect(arr.clamped_view(end=100).to_owned(), content=[1, 2, 3, 4, 5])
246+
json_inspect(arr.clamped_view(start=-5).to_owned(), content=[1, 2, 3, 4, 5])
247+
json_inspect(arr.clamped_view(start=100).to_owned(), content=[])
248+
json_inspect(arr.clamped_view(start=4, end=1).to_owned(), content=[])
249+
json_inspect(arr.clamped_view(start=-9, end=-3).to_owned(), content=[])
250+
json_inspect(
251+
arr.clamped_view(start=@int.MIN_VALUE, end=@int.MAX_VALUE).to_owned(),
252+
content=[1, 2, 3, 4, 5],
253+
)
254+
json_inspect(
255+
arr.clamped_view(start=@int.MAX_VALUE, end=@int.MIN_VALUE).to_owned(),
256+
content=[],
257+
)
258+
json_inspect(([] : ReadOnlyArray[Int]).clamped_view(end=10).to_owned(), content=[])
259+
// the `end=None` arm delegates separately; it must clamp `start` too
260+
inspect(arr.clamped_view(start=100).start_offset(), content="5")
261+
inspect(arr.clamped_view(start=4, end=1).start_offset(), content="4")
262+
}
263+
264+
///|
265+
test "clamped_view with `end` omitted forwards `start` unchanged" {
266+
// `ReadOnlyArray::clamped_view` handles `end=None` in a separate arm, so an
267+
// interior start is what catches a mangled `start` there: 0 / negative /
268+
// past-the-end starts all survive a doubled `start`.
269+
let ro : ReadOnlyArray[Int] = [1, 2, 3, 4, 5]
270+
let fixed : FixedArray[Int] = [1, 2, 3, 4, 5]
271+
json_inspect(ro.clamped_view(start=1).to_owned(), content=[2, 3, 4, 5])
272+
json_inspect(fixed.clamped_view(start=1).to_owned(), content=[2, 3, 4, 5])
273+
for start in -1..<=6 {
274+
// omitting `end` must equal passing the full length explicitly
275+
let ro_omitted = ro.clamped_view(start~)
276+
let ro_explicit = ro.clamped_view(start~, end=5)
277+
json_inspect(
278+
ro_omitted.to_owned(),
279+
content=ro_explicit.to_owned().to_json(),
280+
)
281+
inspect(
282+
ro_omitted.start_offset() == ro_explicit.start_offset(),
283+
content="true",
284+
)
285+
let fixed_omitted = fixed.clamped_view(start~)
286+
let fixed_explicit = fixed.clamped_view(start~, end=5)
287+
json_inspect(
288+
fixed_omitted.to_owned(),
289+
content=fixed_explicit.to_owned().to_json(),
290+
)
291+
inspect(
292+
fixed_omitted.start_offset() == fixed_explicit.start_offset(),
293+
content="true",
294+
)
295+
}
296+
}
297+
298+
///|
299+
test "FixedArray/ReadOnlyArray clamped_view agree with get_view" {
300+
let fixed : FixedArray[Int] = [1, 2, 3, 4, 5]
301+
let ro : ReadOnlyArray[Int] = [1, 2, 3, 4, 5]
302+
for start in -1..<=6 {
303+
for end in -1..<=6 {
304+
// get_view rejects out-of-range and inverted ranges; clamped_view
305+
// answers the same on every range get_view accepts.
306+
guard fixed.get_view(start~, end~) is Some(v) else { continue }
307+
json_inspect(
308+
fixed.clamped_view(start~, end~).to_owned(),
309+
content=v.to_owned().to_json(),
310+
)
311+
json_inspect(
312+
ro.clamped_view(start~, end~).to_owned(),
313+
content=v.to_owned().to_json(),
314+
)
315+
}
316+
}
317+
}

builtin/pkg.generated.mbti

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,7 @@ pub fn FixedArray::blit_from_bytes(Self[Byte], Int, Bytes, Int, Int) -> Unit
998998
pub fn FixedArray::blit_from_bytesview(Self[Byte], Int, BytesView) -> Unit
999999
pub fn FixedArray::blit_from_string(Self[Byte], Int, String, Int, Int) -> Unit
10001000
pub fn[A] FixedArray::blit_to(Self[A], Self[A], len~ : Int, src_offset? : Int, dst_offset? : Int) -> Unit
1001+
pub fn[T] FixedArray::clamped_view(Self[T], start? : Int, end? : Int) -> ArrayView[T]
10011002
pub fn[T : Compare] FixedArray::compare(Self[T], Self[T]) -> Int
10021003
pub fn[T : Eq] FixedArray::contains(Self[T], T) -> Bool
10031004
#alias(clone, deprecated)
@@ -1072,6 +1073,7 @@ pub fn[T : Compare] ReadOnlyArray::binary_search(Self[T], T) -> Result[Int, Int]
10721073
pub fn[T] ReadOnlyArray::binary_search_by(Self[T], (T) -> Int raise?) -> Result[Int, Int] raise?
10731074
pub fn[T] ReadOnlyArray::chunk_by(Self[T], (T, T) -> Bool raise?) -> Array[ArrayView[T]] raise?
10741075
pub fn[T] ReadOnlyArray::chunks(Self[T], Int) -> Array[ArrayView[T]]
1076+
pub fn[T] ReadOnlyArray::clamped_view(Self[T], start? : Int, end? : Int) -> ArrayView[T]
10751077
pub fn[T : Compare] ReadOnlyArray::compare(Self[T], Self[T]) -> Int
10761078
pub fn[T : Eq] ReadOnlyArray::contains(Self[T], T) -> Bool
10771079
pub fn[T] ReadOnlyArray::each(Self[T], (T) -> Unit raise?) -> Unit raise?

builtin/readonlyarray.mbt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,40 @@ pub fn[T] ReadOnlyArray::get_view(
913913
}
914914
}
915915

916+
///|
917+
/// Creates a view of a subarray, clamping the requested range instead of
918+
/// rejecting it. Same contract as `Array::clamped_view`.
919+
///
920+
/// # Example
921+
/// ```mbt check
922+
/// test {
923+
/// let arr : ReadOnlyArray[Int] = [1, 2, 3, 4, 5]
924+
/// debug_inspect(
925+
/// arr.clamped_view(start=3, end=10),
926+
/// content=(
927+
/// #|<ArrayView: [4, 5]>
928+
/// ),
929+
/// )
930+
/// debug_inspect(
931+
/// arr.clamped_view(start=4, end=1),
932+
/// content=(
933+
/// #|<ArrayView: []>
934+
/// ),
935+
/// )
936+
/// }
937+
/// ```
938+
pub fn[T] ReadOnlyArray::clamped_view(
939+
self : ReadOnlyArray[T],
940+
start? : Int = 0,
941+
end? : Int,
942+
) -> ArrayView[T] {
943+
let fixed = self.unsafe_reinterpret_to_fixed_array()
944+
match end {
945+
None => fixed.clamped_view(start~)
946+
Some(end) => fixed.clamped_view(start~, end~)
947+
}
948+
}
949+
916950
///|
917951
/// Joins the string-renderable elements with a separator.
918952
///

0 commit comments

Comments
 (0)