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 (
i32pointer +i32length).
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:
Tview mode.&Tmutable borrow mode.@Towned 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.
-
StaticArray<T, N>- Fixed-size value type.
- Length known at compile time.
- No runtime length header is needed for the value itself.
-
DynamicArray<T>- Owned runtime-sized array value.
- Heap allocated.
- Carries runtime length metadata as a header before the elements.
-
Slice<T>- Borrow/view only.
- Runtime representation is
(data_ptr, len). - May point into static arrays or dynamic arrays.
- Never owns backing storage.
-
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) becauseNis 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 ofStaticSlice<i64, 4>.x: &[i64; 4]is a mutable borrow ofStaticSlice<i64, 4>.x: @[i64; 4]is an ownedStaticArray<i64, 4>value transfer.x: [i64]is a viewSlice<i64>.x: &[i64]is a mutableSlice<i64>borrow.x: @[i64]is an ownedDynamicArray<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.
- Requires runtime length check (
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 whenM >= N.- No runtime check.
- No copy.
StaticSlice<T, M> -> StaticSlice<T, N>is allowed whenM >= N.- No runtime check.
- No copy.
Slice<T> -> StaticSlice<T, N>is allowed.- Compiler inserts runtime check
len >= Nunless statically proven. - Passing a longer slice is valid.
- Compiler inserts runtime check
DynamicArray<T> -> StaticSlice<T, N>is allowed.- Compiler inserts runtime check
len >= Nunless statically proven. - No copy when used as a borrow/view coercion.
- Compiler inserts runtime check
StaticSlice<T, N> -> Slice<T>is allowed without copying.- Length is materialized as compile-time constant
Nin generated code.
- Length is materialized as compile-time constant
Normalized view of borrow/view coercions:
(StaticArray<T, M>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow)=AllowedNoCheckwhenM >= N.(StaticSlice<T, M>, View|MutBorrow) -> (StaticSlice<T, N>, View|MutBorrow)=AllowedNoCheckwhenM >= 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 elementiusing normal bounds policy.xs[a..b]always producesSlice<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;Nis 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)-> onei32(thin pointer concept in normalization).(Slice<T>, View|MutBorrow)-> currently onei32runtime handle in direct WASM backend.(DynamicArray<T>, Owned)-> onei32heap 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]checklen >= N.DynamicArray<T> -> StaticArray<T, N>checklen == N.- Index/range operations maintain existing bounds safety behavior.
StaticArray<T, 8> -> &[T; 4]requires no check and no copy.Slice<T> -> &[T; 4]checkslen >= 4; on success it passes a thin pointer.
When compile-time facts prove the check condition, the check is omitted.
Implementation Status
Implemented:
- Ownership normalization uses explicit
StaticArray,DynamicArray,Slice, andStaticSlicebase kinds. - Shared coercion matrix drives checker and MIR boundary decisions.
- Runtime length checks are emitted for:
DynamicArray<T> -> StaticArray<T, N>(len == N)Slice<T>|DynamicArray<T> -> StaticSlice<T, N>(len >= N)
- Runtime trap path for failed coercion checks uses
std::panic. - Return ownership sigils are disallowed; borrowed returns are rejected.
Remaining backend ABI work:
- Move direct WASM slice view ABI to explicit
(ptr, len)multi-slot passing and returning. - 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.