// 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 } } } }