Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Slices

A subscript whose brackets contain a top-level : is a slice. It copies the selected elements into a new value and returns Result[_, AllocError], so the copy is visible as a ? (issue #579). A tuple slice allocates nothing and returns the tuple itself (see tuple slices):

def main() -> Result[(), AllocError | IoError]:
    xs = [0, 1, 2, 3, 4, 5]?
    print(xs[1:3]?)?
    print(xs[-2:]?)?
    print(xs[::2]?)?
    print(xs[::-1]?)?
    print("Aé🙂Z"[1:3]?)?
    Ok(())
[1, 2]
[4, 5]
[0, 2, 4]
[5, 4, 3, 2, 1, 0]
é🙂

Syntax

slice = primary "[" [start] ":" [stop] [":" [step]] "]"

xs[:], xs[::], xs[a:], xs[:b], xs[::c] and xs[a:b:c] are all slices. A slice has the precedence of indexing and the other postfix forms: xs[1:3]?[0] indexes the copy and s[1:]?.upper()? calls a method on it. A comma inside slice brackets is rejected: there are no multi-dimensional subscripts. The receiver, then each written bound, evaluate once from left to right. The receiver is only read, so a named receiver stays usable.

Bounds and steps

start and stop are i64 expressions or references to i64; other integer types need a cast such as i64(n). Positions count elements, Unicode scalars for str, bytes for Bytes and bytes, and rows for clist.

Out-of-range bounds clamp exactly as in Python. A negative bound counts from the end, and positions outside the receiver select nothing rather than trapping. Omitted bounds depend on the sign of the step: a positive step runs from the start to the end, and a negative step runs from the last element back to the first.

The step has type NonZero[i64] and defaults to 1, so a zero step cannot be written or computed. A nonzero integer literal converts implicitly, and a literal 0 is a compile error. A computed step needs an explicit conversion, which reports a zero step as RangeError:

def main() -> Result[(), AllocError | IoError | RangeError | OverflowError[i64] | ZeroDivisionError]:
    xs = [0, 1, 2, 3, 4, 5]?
    step = NonZero.try_from(len(xs) // 2)?
    print(xs[::step]?)?
    print(xs[100:]?)?
    print(xs[-100:2]?)?
    Ok(())
[0, 3]
[]
[0, 1]

Receivers and results

ReceiverResultElements
list[T]Result[list[T], AllocError]Duplicated with copy(); Shared[T] handles with share()
strResult[str, AllocError]Unicode scalars
std.bytes.BytesResult[std.bytes.Bytes, AllocError]Bytes
bytesResult[std.bytes.Bytes, AllocError]Bytes; no import needed
clist[T]Result[clist[T], AllocError]Rows copied column by column
TupleThe selected tuple, or (); no ResultComponents selected at compile time

A list slice of a nested list copies each selected inner list, so later changes to the source do not reach the slice. A step-1 slice of a list whose elements own no storage, such as list[i64], is one block copy. Every slice reserves its result storage once, before copying; allocation failure returns Err(AllocError.OutOfMemory) and leaves the receiver unchanged.

A list element type that copy() rejects, such as std.bytes.Bytes, is a compile error. Transfer such elements out of an owned temporary with the slice() method instead. A Shared handle in an element, as in list[(str, Shared[T])], gains another owner, as copy() does.

A bytes literal slice gives an owned std.bytes.Bytes. Slice syntax makes the Bytes type available without an import std.bytes line; naming the type in a signature still needs the import. The compiler loads std.bytes for this only when some module of the program could produce a bytes value: one with a b"..." literal, include_bytes, or the word bytes. A program that slices only lists or text does not load it.

Tuple slices

Tuple slice bounds and step must be integer literals, because the result type depends on them: (1, "two", 3.0)[1:] has type (str, f64). A slice that selects no component is (). A tuple slice copies components inline and allocates nothing, so it returns no Result and takes no ? (issue #625):

def main() -> Result[(), IoError]:
    t = (1, "two", 3.0)
    tail: (str, f64) = t[1:]
    print(tail, t[::-1])?
    Ok(())
("two", 3.0) (3.0, "two", 1)

A tuple whose selected components are affine is rejected for now; unpack it and copy the components instead.

A clist receiver must be a named place, and the slice is subject to the same borrow checks as other clist reads.

Slice views

&xs[a:b] borrows the selected elements in place, and &mut xs[a:b] borrows them exclusively (issue #581). A view copies nothing and allocates nothing, so it needs no ?. Use one to pass part of a list or buffer to a function:

def total(values: &[i64]) -> Result[i64, OverflowError[i64]]:
    mut sum = 0
    for x in values:
        sum += *x
    Ok(sum)

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    mut xs = [1, 2, 3, 4, 5]?
    print(total(&xs[1:4])?)?
    tail = &mut xs[-2:]
    tail[0] = 40
    for x in &mut xs[:2]:
        *x *= 10
    print(xs)?
    Ok(())
9
[10, 20, 3, 40, 5]

The bounds clamp as for a copying slice, so a view is empty rather than out of range. A view selects adjacent elements, so it takes no step; copy every k-th element with xs[a:b:k]?.

Viewed valueShared viewExclusive view
list[T]&[T]&mut [T]
std.bytes.Bytes&[u8]&mut [u8]
bytes&[u8]rejected: a literal is read-only
A viewA narrower viewA narrower view of an exclusive view
Shared[list[T]]&[T] of the contentrejected: the content is shared

&[T] and &mut [T] are types: parameters and local annotations may name them. A list of u8, a Bytes buffer, and a bytes literal all give &[u8], so one &[u8] parameter reads any of them. The viewed value must be a named place, such as xs, self.items, or rows[0], or a bytes or str literal, as in &b"GET /"[:3]. A shared view argument may also view a call result, as in w.write(&encode(x)?[2:]): the result lives until the end of the statement. Bind a call result to a name before binding a view of it to a name, and before passing an exclusive view of it.

OperationMeaning
len(v)Number of elements in the view
v[i]Element i of the view, with list index rules: a negative index counts from the end, and an index outside the view traps
v[i] = xReplace element i; needs an exclusive view in a mut binding
v.swap(i, j)Exchange elements i and j in place, with list.swap’s contract; needs an exclusive view
&v[i], &mut v[i]Reference to element i
&v[a:b], &mut v[a:b]Narrower view; bounds count from the start of v
for x in v&T per element, or &mut T for an exclusive view; reversed(v) and enumerate(v) also work
iter(v)A pipeline over the elements, with the items of for x in v
v == w, v != wEqual lengths and equal elements; both sides are views of one element type
print(v), str.repr(v), f"{v}"The elements as a list, [2, 3]; a &[u8] as a bytes literal, b"bc"

In --unchecked assume builds, view indexing inside a plain unchecked: suite assumes a nonnegative index below the view’s own length, with no bounds check or negative-index normalization. The view’s construction and swap method keep their normal contracts.

A view’s length never changes, so v[i] in for i in range(len(v)) or for i, x in enumerate(v) compiles without a bounds check, as described for lists. An in-place reversal, while lo < hi: starting with v.swap(lo, hi) or v[hi], checks its first pass’s indices once, before the loop, as described there.

def even(x: &i64) -> bool:
    two: NonZero[i64] = 2
    *x % two == 0

def main() -> Result[(), AllocError | IoError]:
    xs = [1, 2, 3, 4, 5]?
    middle = &xs[1:4]
    print(middle, iter(middle).filter(even).count())?
    data = b"abc"
    print(&data[1:])?
    Ok(())
[2, 3, 4] 2
b"bc"

A view prints as a reference does, through to its elements; a Bytes buffer itself keeps its own spelling. Other list operations, such as methods other than swap, in, list(v), and copying slices of a view, are rejected. Copy from the owner, xs[a:b]?, for an owned value.

v.swap(i, j) counts both indices within the view, so a negative index counts from the view’s end, and an index outside the view traps before either element moves. Both indices are evaluated before the view is borrowed, so they may read it, as len(v) - 1 does. Equal indices leave the view unchanged. It accepts every element type and allocates, copies, retains, and drops nothing. For numbers and bool it compiles to the inline bounds checks, loads, and stores of list.swap; other element types make one runtime call, which repairs inline payload addresses. A shared view is read-only and has no swap:

def reverse(v: &mut [i64]) -> Result[(), OverflowError[i64]]:
    mut lo = 0
    mut hi = len(v) - 1
    while lo < hi:
        v.swap(lo, hi)
        lo += 1
        hi -= 1
    Ok(())

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    mut xs = [1, 2, 3, 4, 5]?
    reverse(&mut xs[1:])?
    print(xs)?
    Ok(())
[1, 5, 4, 3, 2]

Views borrow their owner

A view holds a loan of its run of elements in its owner: a shared view a shared loan, and an exclusive view an exclusive one, under the borrowing rules. While a view is live, its owner cannot grow, shrink, move, or be reassigned, and len(xs) and &xs conflict with an exclusive view. Two views are disjoint when one stops where the other starts, at one literal or non-mut i64 name, so &mut xs[:m] and &mut xs[m:] can both be live. Overlapping or unprovable ranges, such as &mut xs[:3] with &xs[2:], conflict. Disjoint slice views states the full rule and why it is sound.

Where a view type is expected, a borrow of its owner converts to the whole view, as [:] would: &xs or &mut xs for a list, &buffer or a b"..." literal for &[u8], and &handle for a Shared list. A reference binding to an owner, such as an xs: &list[i64] parameter, converts too. The conversion copies nothing and allocates nothing. Expected view types are parameters, including a generic &[T], and annotated locals:

def total(values: &[i64]) -> Result[i64, OverflowError[i64]]:
    mut sum = 0
    for x in values:
        sum += *x
    Ok(sum)

def width(data: &[u8]) -> i64:
    len(data)

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    xs = [1, 2, 3]?
    whole: &[i64] = &xs
    print(total(&xs)?, total(&xs[1:])?, total(whole)?, width(b"abc"))?
    Ok(())
6 5 6 3

&xs[:] keeps working. An owned list, total(xs), is not a view and is rejected. A &T type parameter takes a reference to a whole value, not a view. Standard-library operations that read bytes take &[u8], so buffer.extend(&other[2:]) and writer.write(b"ok") pass a view. A function with a view parameter may return a reference into it: the first reference or view parameter is the origin of a returned reference. Views may be passed to scoped workers.

Views are local until stored references land (backlog B11). A view cannot be returned, stored in a class field, tuple, collection, or enum payload, or captured by an anonymous function. Return or store the bounds, or an owned copy.

Substring views

&s[a:b] on a str borrows the selected text in place as a &substr (issue #595). Like other views it copies nothing, allocates nothing, and takes no ?. Its bounds are those of the copying slice s[a:b]?: they count Unicode scalars, a negative bound counts from the end, and bounds outside the text clamp. A view therefore always starts and ends on a scalar boundary, and s[a:b]? == &s[a:b] for any a and b.

def vowels(text: &substr) -> Result[i64, OverflowError[i64]]:
    mut n = 0
    for c in text:
        if c in "aeiouéö":
            n += 1
    Ok(n)

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    s = "héllo, wörld"
    word = &s[7:]
    print(word, len(word), word.byte_len(), vowels(&s[:5])?)?
    print(word == "wörld", &s[:1] < word, "ör" in word, word.find("l"))?
    for c in reversed(&word[1:3]):
        print(c)?
    owned = word[:2]?
    print(owned)?
    Ok(())
wörld 5 6 2
True True True Option[i64].Some(3)
r
ö
wö

&substr is a separate type from &str. A &str refers to a whole str, whose representation stores its byte length and scalar count ahead of the text, and every str operation reads them. A run inside a string has no such prefix, so a view holds the run’s address and byte length instead. substr is only written as &substr; there is no owned substr and no &mut substr, since a str is immutable. A &substr parameter or annotated local also takes a whole string as &s, a &str binding, or a str literal, converted as &s[:] would be.

OperationMeaning
len(v)Number of scalars; scans the view’s text, as positions do
v.byte_len()Number of UTF-8 bytes; constant time
v[i]Scalar i as a one-scalar str; a negative index counts from the end, and an index outside the view traps
&v[a:b]Narrower view; bounds count scalars from the start of v
v[a:b]?An owned copy of part of the view, Result[str, AllocError]
for c in vEach scalar as a one-scalar str; reversed(v), enumerate(v), and iter(v) also work
v == w, v != w, <, <=, >, >=Compare by Unicode scalar value, as str does; either side may be a str or a &substr
needle in v, needle not in vSubstring membership; the needle is a str or a &substr
v.startswith(x), v.endswith(x)Exact prefix or suffix; x is a str or a &substr
v.find(x)First matching scalar position within the view, or Nothing
print(v), f"{v}"The text itself; str.repr(v) and f"{v!r}" quote it as a str

Each scalar a loop or index yields fits in a string’s inline form, so these operations allocate nothing. Finding a bound, len(v), v[i], and v.find(x) scan the text before the position, as they do on a str. str methods other than those listed are not available on a view; copy it with v[:]? to use one. A str method does not take a &substr argument, and v in s for a str s is written v in &s[:].

A substring view borrows its string as other views borrow their owner: while it is live, the string cannot be reassigned or moved, and the view stays local until stored references land (backlog B11).

Limits

SourceDiagnostic
ys: list[i64] = xs[1:]slice returns Result[list[i64], AllocError]; use `?` to propagate allocation failure
xs[::0]slice step cannot be zero
xs[::k] with k: i64slice step must be `NonZero[i64]`, not `i64`
&xs[::2]a slice view selects adjacent elements, so it takes no step
&mut s[1:3] on a stra `str` is immutable, so a substring view is shared only; write `&s[a:b]`
def f(xs: &list[T]) -> &[T]a function cannot return a slice view yet (backlog B11)
total(xs) for total(v: &[i64])this parameter takes a slice view: pass `&xs` for a whole list or buffer, or `&xs[a:b]` for part of it
x in v for a view vslice views do not support `in` yet
xs[1:3] = ysslice assignment is not supported; assign elements by index, or build a new list
m[1:2, 0]a slice cannot have several dimensions
t[n:] on a tupletuple slice bounds must be integer literals; the result type depends on them
t[1:]? on a tuplea tuple slice `t[a:b]` does not allocate, so it takes no `?`; write `t[a:b]`
d[1:] on a dictdict does not support slicing
class Window:
    rows: &[i64]

def main() -> ():
    pass
slice views cannot be stored in class fields yet (backlog B11)

First-class slice(...) values are not implemented. The slice syntax proposal records the design and alternatives.