Slices

A slice is a non-owning borrowed view of a contiguous range of initialized elements. It lets code work with part or all of an existing sequence without taking ownership and without copying the elements.

let values: int[5] := [10, 20, 30, 40, 50]

let all := ref values[..]
let middle := ref values[1..<4]

The canonical slice types are:

ref T[]
ref mut T[]
Core rule: A slice borrows storage. It does not own elements, capacity, backing allocation or storage lifetime.

Borrowed View Model

A slice represents a contiguous unit-stride view into storage owned elsewhere.

A slice:

  • does not own its elements;
  • does not own collection capacity;
  • does not choose an allocator;
  • does not reclaim backing storage;
  • does not implicitly copy the represented elements;
  • cannot outlive the storage it borrows;
  • cannot survive an invalidating backing-storage transition.

A slice is therefore fundamentally different from an owning dynamic array T[].

T[]          // owns the dynamic sequence
ref T[]      // shared borrowed view
ref mut T[]  // mutable borrowed view

Slice Types

Shared slice

ref T[]

A shared slice provides read access to a contiguous element range.

Mutable slice

ref mut T[]

A mutable slice provides exclusive mutable access to the represented element range.

The element type T is part of slice type identity.

The runtime length is not encoded as a source-level generic or fixed dimension.

The physical slice representation is compiler-defined. Source code must not assume one particular descriptor layout.

Valid Slice Sources

A slice may borrow any source that guarantees:

  • contiguous storage;
  • unit-stride elements;
  • initialized elements;
  • the exact element type T;
  • a lifetime long enough for the borrow;
  • storage stability for the borrow duration.

Canonical Sec 0.1 slice sources include:

T[N]
T[]
list[T]
ref T[]
ref mut T[]

A slice is not a general borrowed view over every collection.

These are not ordinary slice sources:

map[K, V]
set[T]

Shaped multidimensional views use the shaped-type view model rather than pretending every shaped value is a one-dimensional slice.

Slice Creation Is Explicit

Create a shared slice with ref:

let view := ref values[..]

Create a mutable slice with ref mut:

let writable := ref mut values[..]

Create a shared subrange:

let middle := ref values[1..<4]

Or an inclusive subrange:

let middle := ref values[1..3]

Sec does not require helper methods such as:

AsSlice()
AsMutableSlice()
Slice()

The borrow is visible directly in source.

Rule: There is no general implicit owner-to-slice conversion. Borrowing is explicit unless another language rule specifically defines a borrowing conversion.

Shared Slices

let view: ref int[] := ref values[..]

A shared slice permits:

  • reading elements;
  • shared element borrowing;
  • re-slicing into another shared slice;
  • read-only observations such as Len and IsEmpty;
  • safe iteration that does not require mutable authority.

A shared slice does not permit mutation of its represented elements.

Shared slices may be created from mutable storage:

let mut values: int[4] := [1, 2, 3, 4]
let view := ref values[..]

Borrowing mutable storage as shared does not grant mutation through the resulting slice.

Mutable Slices

let writable: ref mut int[] := ref mut values[..]

A mutable slice requires mutable borrowing authority over the source range.

It may:

  • read represented elements;
  • replace represented elements;
  • take shared element borrows;
  • take mutable element borrows;
  • create shared reborrows;
  • create mutable reborrows;
  • use slice mutation operations such as Reverse() and Fill(value).

While the mutable slice is live, conflicting direct access through the owner or overlapping mutable borrows is forbidden by the ordinary borrow rules.

Slice Ranges

Slice ranges use Sec's ordinary range syntax.

Exclusive upper bound

source[start..<end]

Example:

let part := ref values[1..<4]

selects logical indices 1, 2 and 3.

Inclusive upper bound

source[start..end]

Example:

let part := ref values[1..3]

selects logical indices 1, 2 and 3.

For semantic analysis and borrow reasoning, valid ranges are normalized to:

[start, endExclusive)

Open Slice Ranges

Sec supports open slice forms:

source[..]
source[start..]
source[..end]
source[..<end]

Entire sequence

let all := ref values[..]

From an index to the end

let tail := ref values[2..]

From the beginning through an inclusive endpoint

let head := ref values[..3]

From the beginning to an exclusive endpoint

let head := ref values[..<3]

Open endpoints are resolved against the represented source range.

Empty Slices

An empty slice is valid.

For example, a valid empty range may produce:

Len == 0
IsEmpty == true

No element exists in the represented range.

The empty slice still has ordinary borrow/lifetime semantics. It is not an owning empty collection.

Slice Ranges Do Not Descend

Descending slice ranges are invalid.

Slicing selects a contiguous forward range from existing storage. It does not reverse the sequence.

To reverse represented elements in place through a mutable slice, use:

writable.Reverse()

A reversed traversal order is a different operation from selecting a contiguous slice.

Checked and Fallible Slicing

Slice bounds remain memory-safety boundaries.

A compile-time-known invalid constant range is a compile-time error.

A runtime-dependent range uses the normal checked slicing path. Fallible range access uses the canonical RangeError family where explicit fallible access is required.

Negative range endpoints are invalid.

Safe bounds checking is still required inside an unsafe block. unsafe does not turn safe slice syntax into unchecked pointer traversal.

Truly unchecked traversal belongs to raw-pointer operations and their own rules.

Reborrowing a Slice

A slice may itself be sliced again.

Shared reborrow

let part := ref view[2..<5]

Mutable reborrow

let part := ref mut writable[2..<5]

A mutable reborrow requires mutable authority in the source slice.

A mutable slice may also produce a shared reborrow when only shared access is needed:

let readOnly := ref writable[2..<5]

Reborrowing never creates a new owner. The new slice remains tied to the same ultimate storage.

Slice Lifetimes

A slice's lifetime is constrained by every storage layer it depends on.

A reborrow cannot outlive:

  • the source slice;
  • the ultimate storage owner;
  • the current backing/storage epoch.

The compiler tracks these constraints statically. The programmer does not write explicit lifetime annotations.

This is invalid in principle:

fn Invalid() ref int[] {
    let values: int[4] := [1, 2, 3, 4]
    return ref values[..]
}

The returned slice would refer to local storage destroyed when the function returns.

A returned slice must derive from storage whose lifetime remains valid after return, such as an allowed incoming borrow or another valid external owner.

Borrow Invalidation

A slice depends not only on the owner's existence but also on the continued validity of the referenced storage region.

Operations that may invalidate backing storage conflict with a live slice.

Examples include operations that may:

  • relocate dynamic backing;
  • replace the backing storage domain;
  • destroy the owner;
  • move an owner in a way that invalidates the referenced location;
  • perform structural mutation that changes the represented storage assumptions.

The compiler rejects conflicting operations while the slice remains live.

No runtime borrow counter or tracing collector is required for this model.

Slices of Dynamic Arrays

A slice into T[] depends on the dynamic array's current backing incarnation.

let mut values: int[]
try values.Append(10)
try values.Append(20)

let view := ref values[..]

A later structural operation that may grow and relocate values cannot proceed while view remains a conflicting live dependency.

For example:

let view := ref values[..]
try values.Append(30)

is invalid when Append may invalidate the backing storage required by view.

Ordinary in-place mutation of an element does not itself imply backing relocation, but the usual shared/mutable borrow conflicts still apply.

Slice Properties

Both shared and mutable slices expose these read-only properties:

Len
IsEmpty
Ptr
SizeOf

Mutable slices expose the same observations as shared slices.

Len

Len is the number of logical elements represented by the slice.

Its type is:

uint

Example:

let view := ref values[2..<5]
let count: uint := view.Len

Len never means byte size.

IsEmpty

IsEmpty is a read-only bool property equivalent to:

Len == 0

Example:

if view.IsEmpty {
    return
}

SizeOf

For a slice instance, SizeOf is the byte extent of the represented logical payload.

Conceptually:

payload byte size = Len * element layout stride

It is not the physical size of the compiler's internal slice descriptor.

For FFI wrappers, these two observations commonly form an explicit pair:

view.Ptr
view.SizeOf

Ptr

Ptr exposes the address of the represented contiguous element storage under the raw-pointer rules.

let pointer := view.Ptr

Ptr does not:

  • transfer element ownership;
  • extend the slice lifetime;
  • extend the owner's lifetime;
  • pin relocatable backing storage;
  • prevent invalidation;
  • produce a safe reference;
  • make the slice descriptor ABI-stable.

For an empty slice, the raw pointer representation is implementation-defined under the raw-pointer rules and must not be dereferenced.

ToString

Slices may expose the common formatting methods:

view.ToString()
view.ToString(format)

Ordinary no-argument formatting is type-oriented rather than an implicit dump of every represented element.

For example:

ref int[].ToString() -> "ref int[]"

Exact char[] and rune[] text conversions are special cases defined by the string conversion rules.

Formatting that reveals data borrows it; it does not consume the underlying owner.

Indexing a Slice

Slice indexing uses ordinary sequence indexing:

view[index]

Valid bounds are:

0 <= index < Len

Indexing a linear sequence produces an element place. It does not inherently decide whether the use copies, borrows or mutates the element.

Examples:

let value := view[index]
let element := ref view[index]

For a mutable slice:

writable[index] = replacement
let element := ref mut writable[index]

Complete indexing and checked-access semantics are covered in the next chapter.

Element Borrows

A shared slice can yield a shared reference to an element:

let element := ref view[index]

A mutable slice can yield either:

let readable := ref writable[index]
let writableElement := ref mut writable[index]

The resulting reference lifetime cannot exceed the slice or the ultimate backing storage.

Two runtime indexes are not assumed disjoint merely because they are written as different expressions.

Overlapping mutable borrows are rejected unless disjointness can be proven.

Element Replacement

A mutable slice may replace an existing element:

writable[index] = replacement

Replacement requires:

  • a valid index;
  • mutable access to the element place;
  • an assignable replacement value;
  • no conflicting borrow;
  • successful evaluation of the replacement before the old value is destroyed.

The old owned element is destroyed exactly once after replacement is ready and ownership transfer is valid.

Element replacement does not change slice Len.

Move-Only Elements

A slice does not own the represented elements.

Therefore ordinary indexing never transfers ownership out of a slice merely because the element type is move-only.

This is not an implicit extraction mechanism:

let resource := view[index]

when the operation would require moving a move-only value from borrowed storage.

Borrow the element instead:

let resource := ref view[index]

A slice has no RemoveAt operation because it cannot remove ownership from the owner or change the owner's collection length.

Iteration

Slices participate directly in for iteration:

for item in view {
    Process(item)
}

Plain iteration copies each element and therefore requires the element type to be copyable.

Shared borrowed iteration:

for ref item in view {
    Inspect(item)
}

Mutable borrowed iteration:

for ref mut item in writable {
    item.Reset()
}

Index plus element:

for index, ref item in view {
    Inspect(index, item)
}

Iteration order is increasing logical index order.

Ordinary for iteration is not consuming. A slice never transfers ownership of represented elements through iteration.

Reverse()

Reverse() is available on mutable slices:

writable.Reverse()

It reverses the represented elements in place.

Reverse():

  • does not allocate;
  • does not change Len;
  • does not change backing-storage identity;
  • does not consume the slice;
  • does not require T to be copyable.

Move-based swaps may be used internally, so Reverse() works for move-only element types.

Fill(value)

Fill(value) is available on:

ref mut T[]

when T supports ordinary infallible semantic copying.

writable.Fill(replacement)

The operation:

  • evaluates replacement exactly once;
  • uses it as the source for each element replacement;
  • visits elements in increasing logical index order;
  • replaces every represented element;
  • destroys each replaced owned value exactly once through normal replacement semantics;
  • does not allocate;
  • does not change Len;
  • does not change storage identity.

Fill(value) is unavailable when T is move-only or otherwise cannot be repeatedly copied.

Availability depends on the element type, not on whether the current slice happens to be empty.

Slices Do Not Structurally Mutate Their Owner

A mutable slice may modify the values already inside its represented range. It may not change the owner's sequence structure.

Slices do not expose structural operations such as:

Append
Insert
RemoveAt
Remove
Clear
Reserve
Resize

A slice cannot change the owner collection's length.

This distinction is central:

element mutation      // may be allowed through ref mut T[]
structural mutation   // belongs to the owner

Slice Function Parameters

Slices are the normal way to accept borrowed contiguous sequence data.

Read-only input:

fn Sum(values: ref int[]) int {
    ...
}

Mutable borrowed input:

fn ClearBytes(values: ref mut byte[]) void {
    values.Fill(0)
}

Passing a slice does not transfer ownership of the backing collection.

Slice creation at the call site remains explicit unless another rule specifically introduces a borrowing conversion.

For example, the caller may write:

let total := Sum(ref values[..])

This makes the borrow visible at the ownership boundary.

Returning Slices

A function may return a slice only when the referenced storage remains valid after return.

Returning a slice into local storage is invalid:

fn Invalid() ref byte[] {
    let bytes: byte[4] := [1, 2, 3, 4]
    return ref bytes[..]
}

The local array is destroyed when the function returns.

A returned slice must instead propagate an allowed reference to external storage, or otherwise refer to storage with a valid longer lifetime.

The ordinary reference-origin and borrow-lifetime rules determine whether that return is legal.

Slice Destruction

Destroying a slice does not destroy its elements.

Slice destruction:

  • destroys no represented element;
  • reclaims no backing storage;
  • does not run the owner's destructor;
  • ends only the reference/borrow value.

The storage owner remains responsible for element destruction and backing reclamation.

Equality

Ordinary slice == and != are not defined as content equality.

Reference identity and content equality are different concepts.

If code needs element-by-element content comparison, use an explicitly defined collection/core operation for that purpose rather than assuming ordinary slice equality.

Slices and FFI

A slice's physical descriptor representation is compiler-defined and is not generally FFI-stable.

Do not pass a Sec slice descriptor directly to foreign code unless a specific ABI contract explicitly defines that representation.

For contiguous data, an ordinary wrapper may expose explicit components:

unsafe {
    ForeignCall(view.Ptr, view.SizeOf)
}

The foreign call then receives the pointer and byte extent required by its ABI.

The slice must remain valid for the entire foreign access.

Slices and unsafe

Entering unsafe does not disable normal safe slice bounds semantics.

unsafe {
    let part := ref values[start..<end]
}

is still a safe slice operation and must satisfy ordinary checked range rules.

Use raw pointers when deliberately performing unchecked memory traversal.

Raw pointers do not inherit slice lifetime, bounds or ownership guarantees automatically.

What a Slice Does Not Do

A Sec slice does not:

  • own the represented elements;
  • own capacity;
  • allocate merely by being created;
  • deallocate backing storage;
  • implicitly copy the represented sequence;
  • implicitly extend the owner's lifetime;
  • permit mutation through ref T[];
  • permit structural collection mutation through ref mut T[];
  • allow ownership extraction of a move-only element;
  • reverse data merely because a descending range was requested;
  • become FFI-stable merely because it contains a pointer and a length conceptually;
  • remove the need for borrow analysis.

Diagnostics

Important slice diagnostics include:

  • source is not a valid contiguous slice source;
  • element type mismatch;
  • attempted implicit owner-to-slice conversion;
  • ref mut requested without mutable borrowing authority;
  • negative slice endpoint;
  • descending slice range;
  • compile-time-known range outside bounds;
  • dynamic checked range failure;
  • mutable reborrow from a shared slice;
  • slice lifetime escaping the source owner;
  • slice returned from destroyed local storage;
  • owner mutation that may invalidate a live slice;
  • conflicting shared and mutable borrows;
  • overlapping mutable element borrows that cannot be proven disjoint;
  • assignment through a shared slice;
  • attempt to move a move-only element out through slice indexing;
  • structural mutation method used on a slice;
  • Fill(value) used with a non-copyable element type;
  • direct foreign use of an unspecified slice descriptor layout.

A lifetime diagnostic should explain the actual storage problem:

cannot return this slice

the slice borrows local storage that is destroyed when the function returns

help: return owned storage
help: return a slice derived from a valid external owner

An invalid mutation diagnostic should distinguish element mutation from structure:

cannot append through ref mut Packet[]

a mutable slice may replace existing elements but cannot change the owner's length

help: perform Append on the owning collection when no conflicting slice is live

Best Practices

  • Use ref T[] for read-only borrowed contiguous input.
  • Use ref mut T[] only when the callee must mutate existing elements.
  • Create slices explicitly with ref source[range] or ref mut source[range].
  • Use ..< when an exclusive upper bound naturally matches index arithmetic.
  • Use open ranges such as [..] and [start..] rather than computing redundant endpoints.
  • Use Reverse() to reverse represented elements; do not try to encode reversal as a descending slice.
  • Use Fill(value) only when repeated copying of the element type is semantically valid.
  • Keep slice lifetimes narrow when the owner may need structural mutation.
  • Prefer slice parameters when a function does not need ownership.
  • Do not return slices into local storage.
  • Do not retain Ptr beyond the slice/backing lifetime.
  • Use explicit pointer/length or pointer/byte-count wrappers at FFI boundaries instead of assuming a slice ABI.

Summary

  • A slice is a non-owning borrowed view of contiguous initialized elements.
  • Canonical slice types are ref T[] and ref mut T[].
  • A slice owns neither elements, capacity nor backing storage.
  • The physical slice representation is compiler-defined and not generally FFI-stable.
  • Canonical slice sources include fixed arrays, dynamic arrays, lists and other slices.
  • Maps and sets are not ordinary slice sources.
  • Slice creation is explicit.
  • Use ref source[..] for a shared slice.
  • Use ref mut source[..] for a mutable slice.
  • There is no required AsSlice(), AsMutableSlice() or Slice() method.
  • There is no general implicit owner-to-slice conversion.
  • A shared slice may be borrowed from mutable storage without granting mutation.
  • A mutable slice requires mutable source authority.
  • Slice ranges use the same .. and ..< range notation as Sec.
  • Open forms include [..], [start..], [..end] and [..<end].
  • Valid slice ranges normalize to [start, endExclusive).
  • Negative endpoints are invalid.
  • Descending slice ranges are invalid.
  • Empty slices are valid.
  • Compile-time-known invalid ranges are compile-time errors.
  • Runtime checked slicing uses the normal range-checking path and RangeError where a fallible access form is required.
  • unsafe does not disable safe slice bounds checking.
  • A slice may be re-sliced.
  • A mutable reborrow requires mutable authority from the source slice.
  • A reborrow cannot outlive the source slice, ultimate owner or current backing/storage epoch.
  • A live slice prevents conflicting storage-invalidating operations.
  • A slice into T[] depends on the current dynamic-array backing incarnation.
  • Both slice types expose Len, IsEmpty, Ptr and SizeOf.
  • Len is the logical element count.
  • SizeOf is logical payload byte extent, not descriptor size.
  • Ptr does not extend lifetime or pin backing storage.
  • Slice indexing produces an element place whose use context determines copy, borrow or mutation.
  • A shared slice can produce shared element references.
  • A mutable slice can produce shared or mutable element references.
  • A mutable slice may replace existing elements.
  • A slice never transfers element ownership merely through indexing.
  • Move-only elements must be borrowed rather than implicitly extracted.
  • Slices participate directly in ordinary for iteration.
  • Plain iteration copies and therefore requires copyable elements.
  • Borrowed iteration uses ref and, for mutable slices, ref mut.
  • Reverse() is available on mutable slices and works for move-only elements.
  • Fill(value) is available on mutable slices when T can be repeatedly copied.
  • Fill(value) evaluates its value exactly once.
  • Slices do not expose structural operations such as Append, RemoveAt, Clear, Reserve or Resize.
  • A slice cannot change the owner's collection length.
  • Slice parameters borrow rather than transfer ownership.
  • Returned slices must refer to storage that remains valid after return.
  • Destroying a slice destroys no elements and reclaims no backing storage.
  • Ordinary slice ==/!= do not mean content equality.
  • FFI wrappers should pass explicit compatible pointer/size components rather than assume a stable slice descriptor ABI.