Files

348 lines
10 KiB
Go
Raw Permalink Normal View History

2026-09-03 10:00:00 +02:00
// 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)
}
}
}