Index Sugar and Tensor Modifiers

In Finch, expressions like x[i + 1] are compiled using tensor modifiers, like offset(x, 1)[i]. The user can construct tensor modifiers directly, e.g. offset(x, 1), or implicitly using the syntax x[i + 1]. Recognizable index expressions are converted to tensor modifiers before dimensionalization, so that the modified tensor will participate in dimensionalization.

While tensor modifiers may change the behavior of a tensor, they reference their parent tensor as the root tensor. Modified tensors are not understoond as distinct from their roots. For example, all accesses to the root tensor must obey lifecycle and dimensionalization rules. Additionally, root tensors which are themselves modifiers are unwrapped at the beginning of the program, so that modifiers are not obscured and the new root tensor is not a modifier.

The following table lists the recognized index expressions and their equivalent tensor expressions, where i is an index, a, b are constants, p is an iteration protocol, and x is an expression:

Original ExpressionTransformed Expression
A[i + a]offset(A, 1)[i]
A[i + x]toeplitz(A, 1)[i, x]
A[(a:b)(i)]window(A, a:b)[i]
A[a * i]scale(A, (3,))[i]
A[i * x]products(A, 1)[i, j]
A[~i]permissive(A)[i]
A[p(i)]protocolize(A, p)[i]

Each of these tensor modifiers is described below:

Finch.offsetFunction
offset(tns, delta...)

Create an OffsetArray such that offset(tns, delta...)[i...] == tns[i .+ delta...]. The dimensions declared by an OffsetArray are shifted, so that size(offset(tns, delta...)) == size(tns) .+ delta.

Finch.toeplitzFunction
toeplitz(tns, dim)

Create a ToeplitzArray such that

    Toeplitz(tns, dim)[i...] == tns[i[1:dim-1]..., i[dim] + i[dim + 1], i[dim + 2:end]...]

The ToplitzArray can be thought of as adding a dimension that shifts another dimension of the original tensor.

Finch.windowFunction
window(tns, dims)

Create a WindowedArray which represents a view into another tensor

    window(tns, dims)[i...] == tns[dim[1][i], dim[2][i], ...]

The windowed array restricts the new dimension to the dimension of valid indices of each dim. The dims may also be nothing to represent a full view of the underlying dimension.

Finch.scaleFunction
scale(tns, delta...)

Create a ScaleArray such that scale(tns, delta...)[i...] == tns[i .* delta...]. The dimensions declared by an OffsetArray are shifted, so that size(scale(tns, delta...)) == size(tns) .* delta. This is only supported on tensors with real-valued dimensions.

Finch.productsFunction
products(tns, dim)

Create a ProductArray such that

    products(tns, dim)[i...] == tns[i[1:dim-1]..., i[dim] * i[dim + 1], i[dim + 2:end]...]

This is like toeplitz but with times instead of plus.

Finch.permissiveFunction
permissive(tns, dims...)

Create an PermissiveArray where permissive(tns, dims...)[i...] is missing if i[n] is not in the bounds of tns when dims[n] is true. This wrapper allows all permissive dimensions to be exempt from dimension checks, and is useful when we need to access an array out of bounds, or for padding. More formally,

    permissive(tns, dims...)[i...] =
        if any(n -> dims[n] && !(i[n] in axes(tns)[n]))
            missing
        else
            tns[i...]
        end
Finch.protocolizeFunction
protocolize(tns, protos...)

Create a ProtocolizedArray that accesses dimension n with protocol protos[n], if protos[n] is not nothing. See the documention for Iteration Protocols for more information. For example, to gallop along the inner dimension of a matrix A, we write A[gallop(i), j], which becomes protocolize(A, gallop, nothing)[i, j].