blob: d50cea67497d01f07e1bd4a6df289f2abc53e20a [file]
// Copyright 2024 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package uniquelist
import (
"iter"
"slices"
"unique"
)
// UniqueList is a workaround for Go limitation that slices are not comparable and
// thus can't be used with unique.Make. It interns slices by storing them in an
// unrolled linked list, where each node has a fixed size array, which are comparable
// and can be stored using the unique package. A UniqueList is immutable.
type UniqueList[T comparable] struct {
handle unique.Handle[node[T]]
}
// Len returns the length of the slice that was originally passed to Make. It returns
// a stored value and does not require iterating the linked list.
func (s UniqueList[T]) Len() int {
var zeroList unique.Handle[node[T]]
if s.handle == zeroList {
return 0
}
return s.handle.Value().len
}
// ToSlice returns a slice containing a shallow copy of the list.
func (s UniqueList[T]) ToSlice() []T {
return s.AppendTo(nil)
}
// Iter returns a iter.Seq that iterates the elements of the list.
func (s UniqueList[T]) Iter() iter.Seq[T] {
var zeroSlice unique.Handle[node[T]]
return func(yield func(T) bool) {
cur := s.handle
for cur != zeroSlice {
impl := cur.Value()
for _, v := range impl.elements[:min(nodeSize, impl.len)] {
if !yield(v) {
return
}
}
cur = impl.next
}
}
}
// iterNodes returns an iter.Seq that iterates each node of the
// unrolled linked list, returning a slice that contains all the
// elements in a node at once.
func (s UniqueList[T]) iterNodes() iter.Seq[[]T] {
var zeroSlice unique.Handle[node[T]]
return func(yield func([]T) bool) {
cur := s.handle
for cur != zeroSlice {
impl := cur.Value()
l := min(impl.len, len(impl.elements))
if !yield(impl.elements[:l]) {
return
}
cur = impl.next
}
}
}
// AppendTo appends the contents of the list to the given slice and returns
// the results.
func (s UniqueList[T]) AppendTo(slice []T) []T {
// TODO: should this grow by more than s.Len() to amortize reallocation costs?
slice = slices.Grow(slice, s.Len())
for chunk := range s.iterNodes() {
slice = append(slice, chunk...)
}
return slice
}
// node is a node in an unrolled linked list object that holds a group of elements of a
// list in a fixed size array in order to satisfy the comparable constraint.
type node[T comparable] struct {
// elements is a group of up to nodeSize elements of a list.
elements [nodeSize]T
// len is the length of the list stored in this node and any transitive linked nodes.
// If len is less than nodeSize then only the first len values in the elements array
// are part of the list. If len is greater than nodeSize then next will point to the
// next node in the unrolled linked list.
len int
// next is the next node in the linked list. If it is the zero value of unique.Handle
// then this is the last node.
next unique.Handle[node[T]]
}
// nodeSize is the number of list elements stored in each node. The value 6 was chosen to make
// the size of node 64 bytes to match the cache line size.
const nodeSize = 6
// Make returns a UniqueList for the given slice. Two calls to UniqueList with the same slice contents
// will return identical UniqueList objects.
func Make[T comparable](slice []T) UniqueList[T] {
if len(slice) == 0 {
return UniqueList[T]{}
}
var last unique.Handle[node[T]]
l := 0
// Iterate backwards through the lists in chunks of nodeSize, with the first chunk visited
// being the partial chunk if the length of the slice is not a multiple of nodeSize.
//
// For each chunk, create an unrolled linked list node with a chunk of slice elements and a
// pointer to the previously created node, uniquified through unique.Make.
for chunk := range chunkReverse(slice, nodeSize) {
var node node[T]
copy(node.elements[:], chunk)
node.next = last
l += len(chunk)
node.len = l
last = unique.Make(node)
}
return UniqueList[T]{last}
}
// chunkReverse is similar to slices.Chunk, except that it returns the chunks in reverse
// order. If the length of the slice is not a multiple of n then the first chunk returned
// (which is the last chunk of the input slice) is a partial chunk.
func chunkReverse[T any](slice []T, n int) iter.Seq[[]T] {
return func(yield func([]T) bool) {
l := len(slice)
lastPartialChunkSize := l % n
if lastPartialChunkSize > 0 {
if !yield(slice[l-lastPartialChunkSize : l : l]) {
return
}
}
for i := l - lastPartialChunkSize - n; i >= 0; i -= n {
if !yield(slice[i : i+n : i+n]) {
return
}
}
}
}