Array and Slice Design

This page documents the V2 array/slice model as it is implemented today, plus the remaining backend ABI work.

For implementation-level type normalization and coercion policy, see: docs/src/internals/ownership-representation.md.

Goals

  • Make fixed-size and runtime-sized arrays explicit and internally consistent.
  • Reserve the word "slice" for borrowed/view semantics only.
  • Keep ownership mode on TypeRef (T, &T, @T) unchanged.
  • Avoid unnecessary runtime checks and copies.
  • Encode slice values in WASM ABI as two value slots (i32 pointer + i32 length).

Surface Language Model

The source syntax remains:

  • [T; n] for static arrays.
  • [T] for runtime-sized sequence type syntax.

Ownership is still controlled by parameter/local mode markers:

  • T view mode.
  • &T mutable borrow mode.
  • @T owned mode.

For parameters:

  • [T] means read-only view of sequence data.
  • &[T] means mutable borrow of sequence data.
  • @[T] means owned runtime-sized array.
  • [T; n] means a compile-time-sized read-only view.
  • &[T; n] means a compile-time-sized mutable borrow.
  • @[T; n] means owned compile-time-sized array.

Return annotations do not use ownership sigils. Return values are always owned.

Semantic Type Kinds

The redesign uses separate base type concepts. Ownership/mutability is provided by access mode (View, MutBorrow, Owned), not by the base type itself.

  1. StaticArray<T, N>

    • Fixed-size value type.
    • Length known at compile time.
    • No runtime length header is needed for the value itself.
  2. DynamicArray<T>

    • Owned runtime-sized array value.
    • Heap allocated.
    • Carries runtime length metadata as a header before the elements.
  3. Slice<T>

    • Borrow/view only.
    • Runtime representation is (data_ptr, len).
    • May point into static arrays or dynamic arrays.
    • Never owns backing storage.
  4. StaticSlice<T, N>

    • Fixed-extent view window used in normalized ownership paths.
    • No new surface syntax; this is an internal base-type concept used for [T; N] and &[T; N] in non-owned positions.
    • Runtime ABI representation is a thin pointer (data_ptr) because N is compile-time known.
    • May point into static arrays, dynamic arrays, or slice views after checks.

Ownership Interpretation

Given the type constructor shape above:

  • View mode (T) reads from caller-visible storage.
  • Borrowed mode (&T) is mutable borrow.
  • Owned mode (@T) creates an owned value by copying or moving.

Examples:

  • x: [i64; 4] is a read-only view of StaticSlice<i64, 4>.
  • x: &[i64; 4] is a mutable borrow of StaticSlice<i64, 4>.
  • x: @[i64; 4] is an owned StaticArray<i64, 4> value transfer.
  • x: [i64] is a view Slice<i64>.
  • x: &[i64] is a mutable Slice<i64> borrow.
  • x: @[i64] is an owned DynamicArray<i64> transfer.

Read-only versus mutable for view/borrow forms is encoded by ownership mode, not by changing the underlying collection type constructor:

  • View mode (T) means read-only access.
  • Borrow mode (&T) means mutable access.
  • Owned mode (@T) means owned value transfer.

Coercion and Compatibility Rules

Coercions are defined over normalized pairs (BaseTy, AccessMode) and consumed by both checker and MIR lowering.

Value Coercions

  • StaticArray<T, N> -> DynamicArray<T> is allowed.
    • Requires allocation of dynamic storage and element transfer/copy.
  • DynamicArray<T> -> StaticArray<T, N> is allowed.
    • Requires runtime length check (len == N).
    • Traps on failure.
    • Produces static-array storage for the destination.

Normalized view of the same rules:

  • (StaticArray<T, N>, Owned) -> (DynamicArray<T>, Owned) = RequiresMaterialization.
  • (DynamicArray<T>, Owned) -> (StaticArray<T, N>, Owned) = AllowedWithRuntimeCheck(len == N) + RequiresMaterialization.

Borrow/View Coercions

  • StaticArray<T, N> -> Slice<T> is allowed without copying.
  • DynamicArray<T> -> Slice<T> is allowed without copying.
  • StaticArray<T, M> -> StaticSlice<T, N> is allowed when M >= N.
    • No runtime check.
    • No copy.
  • StaticSlice<T, M> -> StaticSlice<T, N> is allowed when M >= N.
    • No runtime check.
    • No copy.
  • Slice<T> -> StaticSlice<T, N> is allowed.
    • Compiler inserts runtime check len >= N unless statically proven.
    • Passing a longer slice is valid.
  • DynamicArray<T> -> StaticSlice<T, N> is allowed.
    • Compiler inserts runtime check len >= N unless statically proven.
    • No copy when used as a borrow/view coercion.
  • StaticSlice<T, N> -> Slice<T> is allowed without copying.
    • Length is materialized as compile-time constant N in generated code.

Normalized view of borrow/view coercions:

  • (StaticArray<T, M>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow) = AllowedNoCheck when M >= N.
  • (StaticSlice<T, M>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow) = AllowedNoCheck when M >= N.
  • (Slice<T>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow) = AllowedWithRuntimeCheck(len >= N) unless statically proven.
  • (DynamicArray<T>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow) = AllowedWithRuntimeCheck(len >= N) unless statically proven.

Static-Length Reference Behavior

  • &[T; N] or view [T; N] can be represented as a thin pointer.
  • When source length is already known to satisfy >= N, no runtime check is needed.
  • When source length is runtime-known (for example from Slice<T>), compiler inserts a runtime check.

Index and Range Semantics

  • xs[i] reads element i using normal bounds policy.
  • xs[a..b] always produces Slice<T> (borrow/view).
  • Slice ranges always refer to existing storage and carry (ptr, len).
  • Creating an owned array from a range requires an explicit owned conversion path.

No implicit owned copy is created for range results.

Allocation and Storage Model

Static Arrays

  • Default storage for non-boxed static arrays is stack-like aggregate storage.
  • In direct WASM backend this means shadow-stack local storage when needed.
  • Static arrays do not use dynamic array headers.

Dynamic Arrays

  • Always heap allocated.
  • Always carry runtime length metadata in header.
  • Release logic follows owned heap object rules.

Slices

  • Non-owning view values only.
  • Represented as pointer+length pair.
  • Static-sized references use StaticSlice<T, N> and are represented as thin pointers; N is carried in type metadata, not runtime payload.
  • Never allocate by themselves.
  • Never require retain/release ownership operations.

WASM ABI Contract (Current)

ABI projection is derived from normalized (BaseTy, AccessMode) forms.

Core Mapping

  • Scalars keep current scalar mapping.
  • Static-array references ([T; N] in view/borrow positions) are thin pointer ABI values (i32).
  • Dynamic-array owned values (@[T]) use pointer ABI value (i32) to heap object with length header followed by the elements.
  • Slice values currently lower as pointer-sized ABI values in direct WASM.
  • Static-array and dynamic-array owned values also lower as pointer-sized ABI values in direct WASM.

Equivalent normalized mapping:

  • (StaticSlice<T, N>, View|MutBorrow) -> one i32 (thin pointer concept in normalization).
  • (Slice<T>, View|MutBorrow) -> currently one i32 runtime handle in direct WASM backend.
  • (DynamicArray<T>, Owned) -> one i32 heap handle.

Returns

  • Borrowed returns are rejected before ABI planning.
  • Owned slice returns currently follow the aggregate out-slot return path in direct WASM.
  • Tuple/struct/static-array aggregate returns use out-slot rules.

Shadow Stack

  • Owned slice values currently participate in shadow-stack aggregate handling in direct WASM.
  • Shadow stack remains for aggregate values that require addressable temporary storage.

MIR and Backend Representation Notes

The implementation is expected to update MIR type/instruction modeling so that:

  • Dynamic arrays and slices are different concepts.
  • Slice-producing instructions return slice-pair values.
  • Call lowering can pass and return slice pairs directly.
  • Heap ownership inference excludes slices.
  • Heap ownership for static arrays only applies when storage is actually heap-owned (for example boxed paths), not by default.

Runtime Check Insertion Policy

Compiler-inserted checks are required whenever static bounds are not proven.

Examples:

  • Slice<T> -> &[T; N] check len >= N.
  • DynamicArray<T> -> StaticArray<T, N> check len == N.
  • Index/range operations maintain existing bounds safety behavior.
  • StaticArray<T, 8> -> &[T; 4] requires no check and no copy.
  • Slice<T> -> &[T; 4] checks len >= 4; on success it passes a thin pointer.

When compile-time facts prove the check condition, the check is omitted.

Implementation Status

Implemented:

  1. Ownership normalization uses explicit StaticArray, DynamicArray, Slice, and StaticSlice base kinds.
  2. Shared coercion matrix drives checker and MIR boundary decisions.
  3. Runtime length checks are emitted for:
    • DynamicArray<T> -> StaticArray<T, N> (len == N)
    • Slice<T>|DynamicArray<T> -> StaticSlice<T, N> (len >= N)
  4. Runtime trap path for failed coercion checks uses std::panic.
  5. Return ownership sigils are disallowed; borrowed returns are rejected.

Remaining backend ABI work:

  1. Move direct WASM slice view ABI to explicit (ptr, len) multi-slot passing and returning.
  2. Remove slice dependence on aggregate out-slot/shadow-stack paths where the value can be represented directly in locals/results.

Non-Goals

  • No new user-facing keywords.
  • No parser pre-processing.
  • No syntax split for "minimum-length slice" types in this proposal.