348 lines
10 KiB
Go
348 lines
10 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package core
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// The staged views design (docs/ARCHITECTURE.md) starts with the
|
||
|
|
// representation: an array may alias another's storage, either as a
|
||
|
|
// contiguous region (payload rebased, strides nil; invisible to every
|
||
|
|
// kernel) or as a strided view (payload rebased plus explicit strides).
|
||
|
|
// These tests pin the machinery before Slice starts producing views.
|
||
|
|
|
||
|
|
// stridedColumnViewOf builds the column slice [c0, c1) of a 2-D array
|
||
|
|
// as a strided view, the shape a non-contiguous Slice will produce.
|
||
|
|
func stridedColumnViewOf(a *Array, c0, c1 int) *Array {
|
||
|
|
rows, cols := a.Shape()[0], a.Shape()[1]
|
||
|
|
view := &Array{
|
||
|
|
shape: []int{rows, c1 - c0},
|
||
|
|
dt: a.Dtype(),
|
||
|
|
floats: a.RawFloats()[c0:],
|
||
|
|
strides: []int{cols, 1},
|
||
|
|
}
|
||
|
|
return view
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestStridedViewAccessors pins the accessor contract on a strided
|
||
|
|
// view: element i must come from payload[Σ i_d·strides[d]], never from
|
||
|
|
// payload[i].
|
||
|
|
func TestStridedViewAccessors(t *testing.T) {
|
||
|
|
base, err := FromFloats([]float64{
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
}, 3, 4)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
view := stridedColumnViewOf(base, 1, 3) // columns 1..2
|
||
|
|
|
||
|
|
if view.Len() != 6 {
|
||
|
|
t.Fatalf("view len %d, want 6", view.Len())
|
||
|
|
}
|
||
|
|
if view.isContiguous() {
|
||
|
|
t.Fatal("strided view reported contiguous")
|
||
|
|
}
|
||
|
|
// Row-major over (3, 2): [[2,3],[6,7],[10,11]].
|
||
|
|
want := []float64{2, 3, 6, 7, 10, 11}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := view.FloatAt(i); got != w {
|
||
|
|
t.Errorf("view.FloatAt(%d) = %v, want %v", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// The multi-dimensional accessor agrees.
|
||
|
|
for r := range 3 {
|
||
|
|
for c := range 2 {
|
||
|
|
if got, _ := FloatAt(view, r, c); got != want[r*2+c] {
|
||
|
|
t.Errorf("FloatAt(view, %d, %d) = %v, want %v", r, c, got, want[r*2+c])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := view.String(); got != "float (3, 2) [2, 3, 6, 7, 10, 11]" {
|
||
|
|
t.Errorf("view.String() = %q", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestStridedViewMaterialise pins that a view copies out its own
|
||
|
|
// elements, densely and in view order, and that the copy is independent
|
||
|
|
// of the aliased storage.
|
||
|
|
func TestStridedViewMaterialise(t *testing.T) {
|
||
|
|
base, err := FromFloats([]float64{
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
}, 3, 4)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
view := stridedColumnViewOf(base, 1, 3)
|
||
|
|
|
||
|
|
dense := view.materialise()
|
||
|
|
if !dense.isContiguous() {
|
||
|
|
t.Fatal("materialised copy still carries strides")
|
||
|
|
}
|
||
|
|
if dense.Len() != 6 || dense.Shape()[0] != 3 || dense.Shape()[1] != 2 {
|
||
|
|
t.Fatalf("materialised shape %s len %d", shapeText(dense.Shape()), dense.Len())
|
||
|
|
}
|
||
|
|
want := []float64{2, 3, 6, 7, 10, 11}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := dense.FloatAt(i); got != w {
|
||
|
|
t.Errorf("materialised[%d] = %v, want %v", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// The copy owns its payload: the view is still the base's columns.
|
||
|
|
if base.FloatAt(1) != 2 {
|
||
|
|
t.Fatalf("base mutated by materialise: %v", base.FloatAt(1))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestMaterialiseContiguousIsIdentity pins the fast path: a contiguous
|
||
|
|
// array is returned untouched, so callers can materialise unconditionally
|
||
|
|
// without paying a copy.
|
||
|
|
func TestMaterialiseContiguousIsIdentity(t *testing.T) {
|
||
|
|
a, err := FromFloats([]float64{1, 2, 3, 4}, 2, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !a.isContiguous() {
|
||
|
|
t.Fatal("fresh array is not contiguous")
|
||
|
|
}
|
||
|
|
if a.materialise() != a {
|
||
|
|
t.Fatal("contiguous materialise copied instead of returning the receiver")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestContiguousRebasedViewIsTransparent pins the property the kernels
|
||
|
|
// lean on: a contiguous region becomes a view by rebasing the payload
|
||
|
|
// with nil strides, so payload[i] is element i exactly as before and no
|
||
|
|
// kernel needs to know a view exists.
|
||
|
|
func TestContiguousRebasedViewIsTransparent(t *testing.T) {
|
||
|
|
base, err := FromInts([]int64{
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
}, 3, 4)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Row 1 of the base, as a (1, 4) 2-D view.
|
||
|
|
row := &Array{
|
||
|
|
shape: []int{1, 4},
|
||
|
|
dt: Int,
|
||
|
|
ints: base.RawInts()[4:8],
|
||
|
|
}
|
||
|
|
if !row.isContiguous() {
|
||
|
|
t.Fatal("rebased row view is not contiguous")
|
||
|
|
}
|
||
|
|
if row.Len() != 4 {
|
||
|
|
t.Fatalf("row view len %d, want 4", row.Len())
|
||
|
|
}
|
||
|
|
for i, w := range []int64{5, 6, 7, 8} {
|
||
|
|
if got, _ := IntAt(row, 0, i); got != w {
|
||
|
|
t.Errorf("IntAt(row, 0, %d) = %d, want %d", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Direct payload reads are valid for a contiguous view: this is the
|
||
|
|
// guarantee that keeps every kernel working unchanged.
|
||
|
|
if row.RawInts()[0] != 5 || row.RawInts()[3] != 8 {
|
||
|
|
t.Fatalf("direct payload read on a contiguous view is wrong: %v", row.RawInts()[:4])
|
||
|
|
}
|
||
|
|
// A copy of a view carries only the view's elements.
|
||
|
|
cp := Copy(row)
|
||
|
|
if cp.Len() != 4 || len(cp.RawInts()) != 4 {
|
||
|
|
t.Fatalf("Copy(view) len %d payload %d, want 4", cp.Len(), len(cp.RawInts()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestPhysIndexMapping pins the index mapping directly, including the
|
||
|
|
// contiguous fast path.
|
||
|
|
func TestPhysIndexMapping(t *testing.T) {
|
||
|
|
contiguous := &Array{shape: []int{2, 3}, dt: Int, ints: make([]int64, 6)}
|
||
|
|
for i := range 6 {
|
||
|
|
if got := contiguous.physIndex(i); got != i {
|
||
|
|
t.Errorf("contiguous physIndex(%d) = %d, want identity", i, got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// (2, 2) view over a 3-wide base starting at column 1:
|
||
|
|
// element (r, c) is at payload[r*3 + c].
|
||
|
|
view := &Array{shape: []int{2, 2}, dt: Int, ints: make([]int64, 6), strides: []int{3, 1}}
|
||
|
|
want := []int{0, 1, 3, 4}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := view.physIndex(i); got != w {
|
||
|
|
t.Errorf("view.physIndex(%d) = %d, want %d", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestStridedViewComplexAccessors pins complex and int views too, since
|
||
|
|
// each dtype has its own payload field.
|
||
|
|
func TestStridedViewComplexAccessors(t *testing.T) {
|
||
|
|
base, err := FromComplexes([]complex128{
|
||
|
|
1 + 1i, 2 + 2i,
|
||
|
|
3 + 3i, 4 + 4i,
|
||
|
|
}, 2, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Column 1 view of a 2x2 base.
|
||
|
|
view := &Array{
|
||
|
|
shape: []int{2, 1},
|
||
|
|
dt: Complex,
|
||
|
|
complexes: base.RawComplexes()[1:],
|
||
|
|
strides: []int{2, 1},
|
||
|
|
}
|
||
|
|
if got := view.ComplexAt(0); got != 2+2i {
|
||
|
|
t.Errorf("complexAt(0) = %v, want (2+2i)", got)
|
||
|
|
}
|
||
|
|
if got := view.ComplexAt(1); got != 4+4i {
|
||
|
|
t.Errorf("complexAt(1) = %v, want (4+4i)", got)
|
||
|
|
}
|
||
|
|
dense := view.materialise()
|
||
|
|
if dense.Len() != 2 || dense.ComplexAt(1) != 4+4i {
|
||
|
|
t.Errorf("materialised complex view: %v", dense)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestStridedViewFloat32Accessors covers the float32 payload path.
|
||
|
|
func TestStridedViewFloat32Accessors(t *testing.T) {
|
||
|
|
base, err := FromFloat32s([]float32{1, 2, 3, 4, 5, 6}, 2, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// Columns 1..2 of a 3-wide base.
|
||
|
|
view := &Array{
|
||
|
|
shape: []int{2, 2},
|
||
|
|
dt: Float32,
|
||
|
|
floats32: base.RawFloat32s()[1:],
|
||
|
|
strides: []int{3, 1},
|
||
|
|
}
|
||
|
|
want := []float32{2, 3, 5, 6}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := view.float32At(i); got != w {
|
||
|
|
t.Errorf("float32At(%d) = %v, want %v", i, got, w)
|
||
|
|
}
|
||
|
|
if got := view.FloatAt(i); math.Abs(got-float64(w)) > 1e-12 {
|
||
|
|
t.Errorf("floatAt(%d) = %v, want %v", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSliceReturnsContiguousView pins stage (b): a leading-axis slice is
|
||
|
|
// a view sharing the source's storage, not a copy.
|
||
|
|
func TestSliceReturnsContiguousView(t *testing.T) {
|
||
|
|
base, err := FromInts([]int64{
|
||
|
|
1, 2, 3,
|
||
|
|
4, 5, 6,
|
||
|
|
7, 8, 9,
|
||
|
|
}, 3, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
view, err := Slice(base, 0, 1, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if view.Len() != 6 || view.Shape()[0] != 2 {
|
||
|
|
t.Fatalf("slice shape %s len %d", shapeText(view.Shape()), view.Len())
|
||
|
|
}
|
||
|
|
want := []int64{4, 5, 6, 7, 8, 9}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := view.RawInts()[i]; got != w {
|
||
|
|
t.Errorf("view payload[%d] = %d, want %d", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Aliasing proof: a write into the base's storage (which only this
|
||
|
|
// test does; the library never writes through an array it did not
|
||
|
|
// allocate) is visible through the view. The view starts at row 1,
|
||
|
|
// so its payload is base.RawInts()[3:].
|
||
|
|
base.RawInts()[3] = 111
|
||
|
|
if got, _ := IntAt(view, 0, 0); got != 111 {
|
||
|
|
t.Fatalf("view does not alias the base: got %d", got)
|
||
|
|
}
|
||
|
|
// And the view is indistinguishable from a dense array to a kernel.
|
||
|
|
if !view.isContiguous() {
|
||
|
|
t.Fatal("slice view is not contiguous")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSliceFullDimensionReturnsView pins the second contiguous case: a
|
||
|
|
// slice that takes a dimension in full is a view too.
|
||
|
|
func TestSliceFullDimensionReturnsView(t *testing.T) {
|
||
|
|
base, err := FromFloats([]float64{1, 2, 3, 4, 5, 6}, 2, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
view, err := Slice(base, 1, 0, 3) // the whole of dim 1
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for i := range 6 {
|
||
|
|
if got := view.RawFloats()[i]; got != float64(i+1) {
|
||
|
|
t.Fatalf("view[%d] = %v, want %d", i, got, i+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
base.RawFloats()[2] = 42
|
||
|
|
if got := view.RawFloats()[2]; got != 42 {
|
||
|
|
t.Fatalf("full-dimension slice does not alias: %v", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSliceInteriorRangeCopies pins the deliberate boundary: an interior
|
||
|
|
// range is not a contiguous region, so Slice copies it: a strided view
|
||
|
|
// there would need the kernel-boundary materialisation audit.
|
||
|
|
func TestSliceInteriorRangeCopies(t *testing.T) {
|
||
|
|
base, err := FromInts([]int64{
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
}, 2, 4)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
out, err := Slice(base, 1, 1, 3)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !out.isContiguous() {
|
||
|
|
t.Fatal("interior slice is not contiguous")
|
||
|
|
}
|
||
|
|
// Columns 1..2: [[2,3],[6,7]].
|
||
|
|
want := []int64{2, 3, 6, 7}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := out.RawInts()[i]; got != w {
|
||
|
|
t.Errorf("interior slice[%d] = %d, want %d", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
base.RawInts()[1] = 99
|
||
|
|
if got := out.RawInts()[0]; got != 2 {
|
||
|
|
t.Fatalf("copy aliases the base: %d", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSliceOfViewStaysCorrect pins that slicing a view materialises the
|
||
|
|
// source first, so the copy path never reads the wrong payload slots.
|
||
|
|
func TestSliceOfViewStaysCorrect(t *testing.T) {
|
||
|
|
base, err := FromInts([]int64{1, 2, 3, 4, 5, 6}, 3, 2)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
view, err := Slice(base, 0, 1, 3) // rows 1..2, a view
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
inner, err := Slice(view, 1, 0, 1) // first column of the view
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
want := []int64{3, 5}
|
||
|
|
for i, w := range want {
|
||
|
|
if got := inner.RawInts()[i]; got != w {
|
||
|
|
t.Errorf("slice of view[%d] = %d, want %d", i, got, w)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|