Merge branch 'devel' into pr_leak2_re

This commit is contained in:
ringabout
2026-08-18 20:02:43 +08:00
committed by GitHub
60 changed files with 706 additions and 644 deletions

View File

@@ -937,3 +937,47 @@ proc mainRegen() =
doAssert b.a.c == right
mainRegen()
from std/typetraits import distinctBase, supportsCopyMem
block: # bug #26025
type
M[B] = distinct seq[B]
W = object
g: U # `U` is only declared below, so it used to be a `tyForward`
# here and `W` ended up without `tfHasAsgn`
U = M[uint64]
doAssert not supportsCopyMem(W)
var h: M[W]
seq[W](h).add W(g: U(@[1'u64]))
var copied = h
for it in items(distinctBase(copied)):
doAssert seq[uint64](it.g) == @[1'u64]
doAssert seq[uint64](seq[W](h)[0].g) == @[1'u64]
block: # bug #26025, the propagation has to reach the indirect owners too
type
M2[B] = distinct seq[B]
ViaArray = object
g: array[2, Late] # the forward type sits inside the field's type
Outer = object # `Inner` is forward here...
a: Inner
Inner = object
b: Late
Late = M2[uint64]
Reader = object # ...whereas `Outer` is already reified but its
z: Outer # own flags were still provisional
AsTuple = tuple[a: Late]
doAssert not supportsCopyMem(ViaArray)
doAssert not supportsCopyMem(Inner)
doAssert not supportsCopyMem(Outer)
doAssert not supportsCopyMem(Reader)
doAssert not supportsCopyMem(AsTuple)

16
tests/ccgbugs/t26104.nim Normal file
View File

@@ -0,0 +1,16 @@
discard """
action: compile
ccodeCheck: "@'((*Result).f);' .*"
"""
# bug #26104: a compile-time-only `typeof` argument was treated as a runtime
# alias of the result field, forcing an unnecessarily large temporary and copy.
type
B = array[131072, byte]
Y = object
f: B
proc fill(_: type B): B = discard
proc make(): Y = result.f = fill(typeof(result.f))
discard make()

35
tests/ccgbugs/t26112.nim Normal file
View File

@@ -0,0 +1,35 @@
discard """
matrix: "--mm:refc; --mm:orc"
ccodeCheck: "'result.fromScalar = x_p0;'"
ccodeCheck: "'result.fromObject = x_p0.fromObject;'"
ccodeCheck: "'result.nested.fromNested = x_p0.fromNested;'"
"""
# bug #26112: unrelated parameters were considered potential aliases of the
# result location when their types could be contained in the returned object.
type
Inner = object
fromNested: int
P = object
fromScalar: int
fromObject: int
nested: Inner
func fromScalar(x: int): P =
P(fromScalar: x)
proc fromObject(x: P): P =
P(fromObject: x.fromObject)
func fromNested(x: Inner): P =
P(nested: Inner(fromNested: x.fromNested))
proc selfAlias(): P =
result.fromScalar = 42
result = P(fromScalar: result.fromScalar)
doAssert fromScalar(1).fromScalar == 1
doAssert fromObject(P(fromObject: 2)).fromObject == 2
doAssert fromNested(Inner(fromNested: 3)).nested.fromNested == 3
doAssert selfAlias().fromScalar == 42

View File

@@ -0,0 +1,56 @@
discard """
matrix: "--mm:refc; --mm:orc"
"""
type
A = object of RootObj
V = object
case g: bool
of true:
v: A
of false:
e: string
var r = V(g: true, v: A())
discard move r
GC_fullCollect()
type
Kind = enum nested, other
Nested = object
case kind: Kind
of nested:
case enabled: bool
of true: payload: A
of false: message: string
of other:
discard
var n = Nested(kind: nested, enabled: true, payload: A())
discard move n
GC_fullCollect()
# Moving from the other branch must keep its value alive and leave the source
# in the default state.
var s = V(g: false, e: "hello")
let moved = move s
doAssert moved.e == "hello"
doAssert not s.g
doAssert s.e.len == 0
# Reinitializing the zeroed value must also restore embedded object type
# headers.
type W = object
a: A
value: V
text: string
var w = W(a: A(), value: V(g: true, v: A()), text: "content")
let movedW = move w
doAssert movedW.text == "content"
doAssert cast[ptr pointer](addr w.a)[] != nil
doAssert not w.value.g
doAssert w.value.e.len == 0
doAssert w.text.len == 0
GC_fullCollect()

View File

@@ -0,0 +1,59 @@
discard """
matrix: "--mm:arc --threads:on --tlsEmulation:off; --mm:orc --threads:on --tlsEmulation:off"
disabled: "windows"
output: "ok"
timeout: "30"
"""
import std/posix
var
escaped: pointer
reused: pointer
proc allocateOnForeignThread(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
escaped = allocShared(96)
cast[ptr int](escaped)[] = 73
tearDownForeignThreadGc()
result = nil
proc reuseOnForeignThread(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
doAssert cast[ptr int](escaped)[] == 73
deallocShared(escaped)
reused = allocShared(96)
doAssert reused == escaped
deallocShared(reused)
tearDownForeignThreadGc()
result = nil
proc consumeDeferredFree(_: pointer): pointer {.noconv.} =
setupForeignThreadGc()
let first = allocShared(96)
let second = allocShared(96)
# The first allocation advances the active chunk and collects its deferred
# foreign frees. The next allocation reuses the remotely returned cell.
doAssert second == escaped
deallocShared(first)
deallocShared(second)
tearDownForeignThreadGc()
result = nil
proc run(worker: proc(_: pointer): pointer {.noconv.}) =
var thread: Pthread
doAssert pthread_create(addr thread, nil, worker, nil) == 0
doAssert pthread_join(thread, nil) == 0
# setup/teardown is the checkout/return boundary. A distinct native thread can
# safely inherit the allocator even while one of its allocations is still live.
run(allocateOnForeignThread)
run(reuseOnForeignThread)
# A free that arrives while the allocator is idle is queued on its handle and
# consumed after that allocator is handed to another foreign thread.
run(allocateOnForeignThread)
deallocShared(escaped)
run(consumeDeferredFree)
echo "ok"

View File

@@ -0,0 +1,64 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const
pointerCount = 512
drainCount = 2048
iterations {.intdefine.} = 200
sizes = [16, 64, 4000, 4096, 8192]
var
pointers: array[pointerCount, pointer]
mayExit: Atomic[bool]
proc owner() {.thread.} =
for i in 0..<pointers.len:
let size = sizes[i mod sizes.len]
pointers[i] = allocShared(size)
cast[ptr byte](pointers[i])[] = byte(i)
proc borrower() {.thread.} =
while not mayExit.load(moAcquire):
discard
proc drain() {.thread.} =
var drained: array[drainCount, pointer]
for i in 0..<drained.len:
drained[i] = allocShared(sizes[i mod sizes.len])
for p in drained:
deallocShared(p)
let occupied = getOccupiedMem()
doAssert occupied == 0, "allocator retained " & $occupied & " bytes"
for _ in 0..<iterations:
# The owner retires with live small and big allocations. The borrower checks
# out that region while this already-running main thread returns the cells.
# This races foreign queue publication against both directions of the
# MemRegion handoff without creating an unbounded number of regions.
block:
var thread: Thread[void]
createThread(thread, owner)
joinThread(thread)
mayExit.store(false, moRelaxed)
var borrowerThread: Thread[void]
createThread(borrowerThread, borrower)
for i, p in pointers:
if i == pointers.len div 2:
# Let the borrower tear the allocator down while the second half of the
# foreign publications are still in flight.
mayExit.store(true, moRelease)
deallocShared(p)
joinThread(borrowerThread)
block:
var thread: Thread[void]
createThread(thread, drain)
joinThread(thread)
echo "ok"

View File

@@ -0,0 +1,92 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const concurrentThreads = 4
var
escaped: pointer
reused: pointer
bigEscaped: pointer
roundAddresses: array[2, array[concurrentThreads, pointer]]
ready: Atomic[int]
mayExit: Atomic[bool]
proc allocateEscaped() {.thread.} =
escaped = allocShared(64)
cast[ptr int](escaped)[] = 42
proc consumeAfterHandoff() {.thread.} =
doAssert cast[ptr int](escaped)[] == 42
deallocShared(escaped)
reused = allocShared(64)
doAssert reused == escaped
deallocShared(reused)
# A live allocation can outlast its original thread. The next thread receives
# the same allocator and its stable handle makes the deallocation local again.
block:
var thread: Thread[void]
createThread(thread, allocateEscaped)
joinThread(thread)
createThread(thread, consumeAfterHandoff)
joinThread(thread)
proc allocateBigEscaped() {.thread.} =
bigEscaped = allocShared(8192)
cast[ptr int](bigEscaped)[] = 91
proc consumeBigAfterHandoff() {.thread.} =
doAssert cast[ptr int](bigEscaped)[] == 91
deallocShared(bigEscaped)
let p = allocShared(8192)
doAssert p == bigEscaped
deallocShared(p)
# Big chunks use a separate deferred-free queue on the stable handle.
block:
var thread: Thread[void]
createThread(thread, allocateBigEscaped)
joinThread(thread)
createThread(thread, consumeBigAfterHandoff)
joinThread(thread)
proc allocateConcurrently(arg: tuple[round, index: int]) {.thread.} =
let p = allocShared(80)
roundAddresses[arg.round][arg.index] = p
deallocShared(p)
discard ready.fetchAdd(1, moRelease)
while not mayExit.load(moAcquire):
discard
proc runConcurrentRound(round: int) =
var threads: array[concurrentThreads, Thread[tuple[round, index: int]]]
ready.store(0, moRelaxed)
mayExit.store(false, moRelaxed)
for i in 0..<threads.len:
createThread(threads[i], allocateConcurrently, (round, i))
while ready.load(moAcquire) != concurrentThreads:
discard
mayExit.store(true, moRelease)
for thread in threads.mitems:
joinThread(thread)
# The first round establishes the peak number of simultaneous allocators. The
# following rounds must reuse those regions instead of reserving one region per
# new thread.
runConcurrentRound(0)
for _ in 0..<32:
runConcurrentRound(1)
for p in roundAddresses[1]:
var found = false
for old in roundAddresses[0]:
if p == old:
found = true
break
doAssert found
echo "ok"

View File

@@ -0,0 +1,56 @@
discard """
matrix: "--mm:arc --threads:on; --mm:orc --threads:on"
output: "ok"
timeout: "30"
"""
import std/[atomics, typedthreads]
const
pointerCount = 1024
iterations = 100
var
pointers: array[pointerCount, pointer]
drainPointers: array[pointerCount, pointer]
ready: Atomic[bool]
start: Atomic[bool]
remoteDone: Atomic[bool]
proc owner() {.thread.} =
for i in 0..<pointers.len:
pointers[i] = allocShared(16 + (i mod 8) * 16)
ready.store(true, moRelease)
while not start.load(moAcquire):
discard
# Race allocator activity against remote frees. The owner can consume cells
# while the remote thread is still publishing entries to its handle.
while not remoteDone.load(moAcquire):
for i in 0..<8:
let p = allocShared(16 + i * 16)
deallocShared(p)
# Exhaust local free lists so all remaining remote lists are consumed before
# this allocator is returned to the pool.
for i in 0..<drainPointers.len:
drainPointers[i] = allocShared(16 + (i mod 8) * 16)
for p in drainPointers:
deallocShared(p)
doAssert getOccupiedMem() == 0
for _ in 0..<iterations:
ready.store(false, moRelaxed)
start.store(false, moRelaxed)
remoteDone.store(false, moRelaxed)
var thread: Thread[void]
createThread(thread, owner)
while not ready.load(moAcquire):
discard
start.store(true, moRelease)
for p in pointers:
deallocShared(p)
remoteDone.store(true, moRelease)
joinThread(thread)
echo "ok"