Update to go v1.23.2
This commit is contained in:
4
VERSION
4
VERSION
@@ -1,2 +1,2 @@
|
|||||||
go1.23.1
|
go1.23.2
|
||||||
time 2024-08-29T20:56:24Z
|
time 2024-09-28T01:34:15Z
|
||||||
|
|||||||
@@ -318,9 +318,9 @@ func containsClosure(f, c *ir.Func) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closures within function Foo are named like "Foo.funcN..."
|
// Closures within function Foo are named like "Foo.funcN..." or "Foo-rangeN".
|
||||||
// TODO(mdempsky): Better way to recognize this.
|
// TODO(mdempsky): Better way to recognize this.
|
||||||
fn := f.Sym().Name
|
fn := f.Sym().Name
|
||||||
cn := c.Sym().Name
|
cn := c.Sym().Name
|
||||||
return len(cn) > len(fn) && cn[:len(fn)] == fn && cn[len(fn)] == '.'
|
return len(cn) > len(fn) && cn[:len(fn)] == fn && (cn[len(fn)] == '.' || cn[len(fn)] == '-')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,40 @@ type timer struct {
|
|||||||
// mu protects reads and writes to all fields, with exceptions noted below.
|
// mu protects reads and writes to all fields, with exceptions noted below.
|
||||||
mu mutex
|
mu mutex
|
||||||
|
|
||||||
astate atomic.Uint8 // atomic copy of state bits at last unlock
|
astate atomic.Uint8 // atomic copy of state bits at last unlock
|
||||||
state uint8 // state bits
|
state uint8 // state bits
|
||||||
isChan bool // timer has a channel; immutable; can be read without lock
|
isChan bool // timer has a channel; immutable; can be read without lock
|
||||||
blocked uint32 // number of goroutines blocked on timer's channel
|
|
||||||
|
// isSending is used to handle races between running a
|
||||||
|
// channel timer and stopping or resetting the timer.
|
||||||
|
// It is used only for channel timers (t.isChan == true).
|
||||||
|
// The lowest zero bit is set when about to send a value on the channel,
|
||||||
|
// and cleared after sending the value.
|
||||||
|
// The stop/reset code uses this to detect whether it
|
||||||
|
// stopped the channel send.
|
||||||
|
//
|
||||||
|
// An isSending bit is set only when t.mu is held.
|
||||||
|
// An isSending bit is cleared only when t.sendLock is held.
|
||||||
|
// isSending is read only when both t.mu and t.sendLock are held.
|
||||||
|
//
|
||||||
|
// Setting and clearing Uint8 bits handles the case of
|
||||||
|
// a timer that is reset concurrently with unlockAndRun.
|
||||||
|
// If the reset timer runs immediately, we can wind up with
|
||||||
|
// concurrent calls to unlockAndRun for the same timer.
|
||||||
|
// Using matched bit set and clear in unlockAndRun
|
||||||
|
// ensures that the value doesn't get temporarily out of sync.
|
||||||
|
//
|
||||||
|
// We use a uint8 to keep the timer struct small.
|
||||||
|
// This means that we can only support up to 8 concurrent
|
||||||
|
// runs of a timer, where a concurrent run can only occur if
|
||||||
|
// we start a run, unlock the timer, the timer is reset to a new
|
||||||
|
// value (or the ticker fires again), it is ready to run,
|
||||||
|
// and it is actually run, all before the first run completes.
|
||||||
|
// Since completing a run is fast, even 2 concurrent timer runs are
|
||||||
|
// nearly impossible, so this should be safe in practice.
|
||||||
|
isSending atomic.Uint8
|
||||||
|
|
||||||
|
blocked uint32 // number of goroutines blocked on timer's channel
|
||||||
|
|
||||||
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
|
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
|
||||||
// each time calling f(arg, seq, delay) in the timer goroutine, so f must be
|
// each time calling f(arg, seq, delay) in the timer goroutine, so f must be
|
||||||
@@ -431,6 +461,15 @@ func (t *timer) stop() bool {
|
|||||||
// Stop any future sends with stale values.
|
// Stop any future sends with stale values.
|
||||||
// See timer.unlockAndRun.
|
// See timer.unlockAndRun.
|
||||||
t.seq++
|
t.seq++
|
||||||
|
|
||||||
|
// If there is currently a send in progress,
|
||||||
|
// incrementing seq is going to prevent that
|
||||||
|
// send from actually happening. That means
|
||||||
|
// that we should return true: the timer was
|
||||||
|
// stopped, even though t.when may be zero.
|
||||||
|
if t.isSending.Load() > 0 {
|
||||||
|
pending = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.unlock()
|
t.unlock()
|
||||||
if !async && t.isChan {
|
if !async && t.isChan {
|
||||||
@@ -525,6 +564,15 @@ func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay in
|
|||||||
// Stop any future sends with stale values.
|
// Stop any future sends with stale values.
|
||||||
// See timer.unlockAndRun.
|
// See timer.unlockAndRun.
|
||||||
t.seq++
|
t.seq++
|
||||||
|
|
||||||
|
// If there is currently a send in progress,
|
||||||
|
// incrementing seq is going to prevent that
|
||||||
|
// send from actually happening. That means
|
||||||
|
// that we should return true: the timer was
|
||||||
|
// stopped, even though t.when may be zero.
|
||||||
|
if t.isSending.Load() > 0 {
|
||||||
|
pending = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.unlock()
|
t.unlock()
|
||||||
if !async && t.isChan {
|
if !async && t.isChan {
|
||||||
@@ -1013,6 +1061,24 @@ func (t *timer) unlockAndRun(now int64) {
|
|||||||
}
|
}
|
||||||
t.updateHeap()
|
t.updateHeap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async := debug.asynctimerchan.Load() != 0
|
||||||
|
var isSendingClear uint8
|
||||||
|
if !async && t.isChan {
|
||||||
|
// Tell Stop/Reset that we are sending a value.
|
||||||
|
// Set the lowest zero bit.
|
||||||
|
// We do this awkward step because atomic.Uint8
|
||||||
|
// doesn't support Add or CompareAndSwap.
|
||||||
|
// We only set bits with t locked.
|
||||||
|
v := t.isSending.Load()
|
||||||
|
i := sys.TrailingZeros8(^v)
|
||||||
|
if i == 8 {
|
||||||
|
throw("too many concurrent timer firings")
|
||||||
|
}
|
||||||
|
isSendingClear = 1 << i
|
||||||
|
t.isSending.Or(isSendingClear)
|
||||||
|
}
|
||||||
|
|
||||||
t.unlock()
|
t.unlock()
|
||||||
|
|
||||||
if raceenabled {
|
if raceenabled {
|
||||||
@@ -1028,7 +1094,6 @@ func (t *timer) unlockAndRun(now int64) {
|
|||||||
ts.unlock()
|
ts.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
async := debug.asynctimerchan.Load() != 0
|
|
||||||
if !async && t.isChan {
|
if !async && t.isChan {
|
||||||
// For a timer channel, we want to make sure that no stale sends
|
// For a timer channel, we want to make sure that no stale sends
|
||||||
// happen after a t.stop or t.modify, but we cannot hold t.mu
|
// happen after a t.stop or t.modify, but we cannot hold t.mu
|
||||||
@@ -1044,6 +1109,10 @@ func (t *timer) unlockAndRun(now int64) {
|
|||||||
// and double-check that t.seq is still the seq value we saw above.
|
// and double-check that t.seq is still the seq value we saw above.
|
||||||
// If not, the timer has been updated and we should skip the send.
|
// If not, the timer has been updated and we should skip the send.
|
||||||
// We skip the send by reassigning f to a no-op function.
|
// We skip the send by reassigning f to a no-op function.
|
||||||
|
//
|
||||||
|
// The isSending field tells t.stop or t.modify that we have
|
||||||
|
// started to send the value. That lets them correctly return
|
||||||
|
// true meaning that no value was sent.
|
||||||
lock(&t.sendLock)
|
lock(&t.sendLock)
|
||||||
if t.seq != seq {
|
if t.seq != seq {
|
||||||
f = func(any, uintptr, int64) {}
|
f = func(any, uintptr, int64) {}
|
||||||
@@ -1053,6 +1122,9 @@ func (t *timer) unlockAndRun(now int64) {
|
|||||||
f(arg, seq, delay)
|
f(arg, seq, delay)
|
||||||
|
|
||||||
if !async && t.isChan {
|
if !async && t.isChan {
|
||||||
|
// We are no longer sending a value.
|
||||||
|
t.isSending.And(^isSendingClear)
|
||||||
|
|
||||||
unlock(&t.sendLock)
|
unlock(&t.sendLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -785,6 +785,68 @@ func TestAdjustTimers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStopResult(t *testing.T) {
|
||||||
|
testStopResetResult(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResetResult(t *testing.T) {
|
||||||
|
testStopResetResult(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that when racing between running a timer and stopping a timer Stop
|
||||||
|
// consistently indicates whether a value can be read from the channel.
|
||||||
|
// Issue #69312.
|
||||||
|
func testStopResetResult(t *testing.T, testStop bool) {
|
||||||
|
for _, name := range []string{"0", "1", "2"} {
|
||||||
|
t.Run("asynctimerchan="+name, func(t *testing.T) {
|
||||||
|
testStopResetResultGODEBUG(t, testStop, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStopResetResultGODEBUG(t *testing.T, testStop bool, godebug string) {
|
||||||
|
t.Setenv("GODEBUG", "asynctimerchan="+godebug)
|
||||||
|
|
||||||
|
stopOrReset := func(timer *Timer) bool {
|
||||||
|
if testStop {
|
||||||
|
return timer.Stop()
|
||||||
|
} else {
|
||||||
|
return timer.Reset(1 * Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start := make(chan struct{})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const N = 1000
|
||||||
|
wg.Add(N)
|
||||||
|
for range N {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-start
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
timer1 := NewTimer(1 * Millisecond)
|
||||||
|
timer2 := NewTimer(1 * Millisecond)
|
||||||
|
select {
|
||||||
|
case <-timer1.C:
|
||||||
|
if !stopOrReset(timer2) {
|
||||||
|
// The test fails if this
|
||||||
|
// channel read times out.
|
||||||
|
<-timer2.C
|
||||||
|
}
|
||||||
|
case <-timer2.C:
|
||||||
|
if !stopOrReset(timer1) {
|
||||||
|
// The test fails if this
|
||||||
|
// channel read times out.
|
||||||
|
<-timer1.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
// Benchmark timer latency when the thread that creates the timer is busy with
|
// Benchmark timer latency when the thread that creates the timer is busy with
|
||||||
// other work and the timers must be serviced by other threads.
|
// other work and the timers must be serviced by other threads.
|
||||||
// https://golang.org/issue/38860
|
// https://golang.org/issue/38860
|
||||||
|
|||||||
173
test/fixedbugs/issue69434.go
Normal file
173
test/fixedbugs/issue69434.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// run
|
||||||
|
|
||||||
|
// Copyright 2024 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WordReader is the struct that implements io.Reader
|
||||||
|
type WordReader struct {
|
||||||
|
scanner *bufio.Scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWordReader creates a new WordReader from an io.Reader
|
||||||
|
func NewWordReader(r io.Reader) *WordReader {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Split(bufio.ScanWords)
|
||||||
|
return &WordReader{
|
||||||
|
scanner: scanner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads data from the input stream and returns a single lowercase word at a time
|
||||||
|
func (wr *WordReader) Read(p []byte) (n int, err error) {
|
||||||
|
if !wr.scanner.Scan() {
|
||||||
|
if err := wr.scanner.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
word := wr.scanner.Text()
|
||||||
|
cleanedWord := removeNonAlphabetic(word)
|
||||||
|
if len(cleanedWord) == 0 {
|
||||||
|
return wr.Read(p)
|
||||||
|
}
|
||||||
|
n = copy(p, []byte(cleanedWord))
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns an iterator allowing the caller to iterate over the WordReader using for/range.
|
||||||
|
func (wr *WordReader) All() iter.Seq[string] {
|
||||||
|
word := make([]byte, 1024)
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
var err error
|
||||||
|
var n int
|
||||||
|
for n, err = wr.Read(word); err == nil; n, err = wr.Read(word) {
|
||||||
|
if !yield(string(word[:n])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != io.EOF {
|
||||||
|
fmt.Fprintf(os.Stderr, "error reading word: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeNonAlphabetic removes non-alphabetic characters from a word using strings.Map
|
||||||
|
func removeNonAlphabetic(word string) string {
|
||||||
|
return strings.Map(func(r rune) rune {
|
||||||
|
if unicode.IsLetter(r) {
|
||||||
|
return unicode.ToLower(r)
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}, word)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbabilisticSkipper determines if an item should be retained with probability 1/(1<<n)
|
||||||
|
type ProbabilisticSkipper struct {
|
||||||
|
n int
|
||||||
|
counter uint64
|
||||||
|
bitmask uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProbabilisticSkipper initializes the ProbabilisticSkipper
|
||||||
|
func NewProbabilisticSkipper(n int) *ProbabilisticSkipper {
|
||||||
|
pr := &ProbabilisticSkipper{n: n}
|
||||||
|
pr.refreshCounter()
|
||||||
|
return pr
|
||||||
|
}
|
||||||
|
|
||||||
|
// check panics if pr.n is not the expected value
|
||||||
|
func (pr *ProbabilisticSkipper) check(n int) {
|
||||||
|
if pr.n != n {
|
||||||
|
panic(fmt.Sprintf("check: pr.n != n %d != %d", pr.n, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshCounter refreshes the counter with a new random value
|
||||||
|
func (pr *ProbabilisticSkipper) refreshCounter() {
|
||||||
|
if pr.n == 0 {
|
||||||
|
pr.bitmask = ^uint64(0) // All bits set to 1
|
||||||
|
} else {
|
||||||
|
pr.bitmask = rand.Uint64()
|
||||||
|
for i := 0; i < pr.n-1; i++ {
|
||||||
|
pr.bitmask &= rand.Uint64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pr.counter = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldSkip returns true with probability 1/(1<<n)
|
||||||
|
func (pr *ProbabilisticSkipper) ShouldSkip() bool {
|
||||||
|
remove := pr.bitmask&1 == 0
|
||||||
|
pr.bitmask >>= 1
|
||||||
|
pr.counter--
|
||||||
|
if pr.counter == 0 {
|
||||||
|
pr.refreshCounter()
|
||||||
|
}
|
||||||
|
return remove
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateUniqueWordsIter estimates the number of unique words using a probabilistic counting method
|
||||||
|
func EstimateUniqueWordsIter(reader io.Reader, memorySize int) int {
|
||||||
|
wordReader := NewWordReader(reader)
|
||||||
|
words := make(map[string]struct{}, memorySize)
|
||||||
|
|
||||||
|
rounds := 0
|
||||||
|
roundRemover := NewProbabilisticSkipper(1)
|
||||||
|
wordSkipper := NewProbabilisticSkipper(rounds)
|
||||||
|
wordSkipper.check(rounds)
|
||||||
|
|
||||||
|
for word := range wordReader.All() {
|
||||||
|
wordSkipper.check(rounds)
|
||||||
|
if wordSkipper.ShouldSkip() {
|
||||||
|
delete(words, word)
|
||||||
|
} else {
|
||||||
|
words[word] = struct{}{}
|
||||||
|
|
||||||
|
if len(words) >= memorySize {
|
||||||
|
rounds++
|
||||||
|
|
||||||
|
wordSkipper = NewProbabilisticSkipper(rounds)
|
||||||
|
for w := range words {
|
||||||
|
if roundRemover.ShouldSkip() {
|
||||||
|
delete(words, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wordSkipper.check(rounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(words) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
invProbability := 1 << rounds
|
||||||
|
estimatedUniqueWords := len(words) * invProbability
|
||||||
|
return estimatedUniqueWords
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
input := "Hello, world! This is a test. Hello, world, hello!"
|
||||||
|
expectedUniqueWords := 6 // "hello", "world", "this", "is", "a", "test" (but "hello" and "world" are repeated)
|
||||||
|
memorySize := 6
|
||||||
|
|
||||||
|
reader := strings.NewReader(input)
|
||||||
|
estimatedUniqueWords := EstimateUniqueWordsIter(reader, memorySize)
|
||||||
|
if estimatedUniqueWords != expectedUniqueWords {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
133
test/fixedbugs/issue69507.go
Normal file
133
test/fixedbugs/issue69507.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// run
|
||||||
|
|
||||||
|
// Copyright 2024 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := run()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
methods := "AB"
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
tag string
|
||||||
|
choices []string
|
||||||
|
}
|
||||||
|
all := []node{
|
||||||
|
{"000", permutations(methods)},
|
||||||
|
}
|
||||||
|
|
||||||
|
next := 1
|
||||||
|
for len(all) > 0 {
|
||||||
|
cur := all[0]
|
||||||
|
k := copy(all, all[1:])
|
||||||
|
all = all[:k]
|
||||||
|
|
||||||
|
if len(cur.choices) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestM map[byte][]string
|
||||||
|
bMax := len(cur.choices) + 1
|
||||||
|
bMin := -1
|
||||||
|
for sel := range selections(methods) {
|
||||||
|
m := make(map[byte][]string)
|
||||||
|
for _, order := range cur.choices {
|
||||||
|
x := findFirstMatch(order, sel)
|
||||||
|
m[x] = append(m[x], order)
|
||||||
|
}
|
||||||
|
|
||||||
|
min := len(cur.choices) + 1
|
||||||
|
max := -1
|
||||||
|
for _, v := range m {
|
||||||
|
if len(v) < min {
|
||||||
|
min = len(v)
|
||||||
|
}
|
||||||
|
if len(v) > max {
|
||||||
|
max = len(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if max < bMax || (max == bMax && min > bMin) {
|
||||||
|
bestM = m
|
||||||
|
bMin = min
|
||||||
|
bMax = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bMax == len(cur.choices) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := Keys(bestM)
|
||||||
|
for c := range cc {
|
||||||
|
choices := bestM[c]
|
||||||
|
next++
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case 'A':
|
||||||
|
case 'B':
|
||||||
|
default:
|
||||||
|
panic("unexpected selector type " + string(c))
|
||||||
|
}
|
||||||
|
all = append(all, node{"", choices})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func permutations(s string) []string {
|
||||||
|
if len(s) <= 1 {
|
||||||
|
return []string{s}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for i, char := range s {
|
||||||
|
rest := s[:i] + s[i+1:]
|
||||||
|
for _, perm := range permutations(rest) {
|
||||||
|
result = append(result, string(char)+perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Seq[V any] func(yield func(V) bool)
|
||||||
|
|
||||||
|
func selections(s string) Seq[string] {
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
for bits := 1; bits < 1<<len(s); bits++ {
|
||||||
|
var choice string
|
||||||
|
for j, char := range s {
|
||||||
|
if bits&(1<<j) != 0 {
|
||||||
|
choice += string(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !yield(choice) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFirstMatch(order, sel string) byte {
|
||||||
|
for _, c := range order {
|
||||||
|
return byte(c)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func Keys[Map ~map[K]V, K comparable, V any](m Map) Seq[K] {
|
||||||
|
return func(yield func(K) bool) {
|
||||||
|
for k := range m {
|
||||||
|
if !yield(k) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user