YRC: tests and correctness proof

This commit is contained in:
araq
2026-02-09 14:09:09 +01:00
parent 933076191c
commit 2f779e1d5f
4 changed files with 526 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Test concurrent traversal and mutation of a shared cyclic list under YRC.
# Multiple threads race to replace nodes using a lock for synchronization.
# This exercises YRC's write barrier and cycle collection under contention.
import std/locks
type
Node = ref object
value: int
next: Node
proc newCycle(start, count: int): Node =
result = Node(value: start)
var cur = result
for i in 1..<count:
cur.next = Node(value: start + i)
cur = cur.next
cur.next = result # close the cycle
proc sumCycle(head: Node; count: int): int =
var cur = head
for i in 0..<count:
result += cur.value
cur = cur.next
const
NumThreads = 4
CycleLen = 6
Iterations = 50
var
shared: Node
sharedLock: Lock
threads: array[NumThreads, Thread[int]]
wins: array[NumThreads, int]
proc worker(id: int) {.thread.} =
{.cast(gcsafe).}:
for iter in 0..<Iterations:
# Under the lock, walk the shared list and replace a node's next pointer
withLock sharedLock:
var cur = shared
if cur == nil: continue
for step in 0..<CycleLen:
let nxt = cur.next
if nxt == nil: break
# Replace cur.next with a fresh node that points to nxt.next
let replacement = Node(value: id * 1000 + iter, next: nxt.next)
cur.next = replacement
wins[id] += 1
cur = cur.next
if cur == nil: break
# Outside the lock, create a local cycle to exercise the collector
let local = newCycle(id * 100 + iter, 3)
discard sumCycle(local, 3)
# Create initial shared cyclic list: 0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 0
initLock(sharedLock)
shared = newCycle(0, CycleLen)
for i in 0..<NumThreads:
createThread(threads[i], worker, i)
for i in 0..<NumThreads:
joinThread(threads[i])
# Verify: the list is still traversable (no crashes, no dangling pointers).
var totalWins = 0
for i in 0..<NumThreads:
totalWins += wins[i]
# Walk the list to verify it's still a valid cycle (or chain)
var cur = shared
var seen = 0
var maxSteps = CycleLen * 3 # generous bound
while cur != nil and seen < maxSteps:
seen += 1
cur = cur.next
if cur == shared: break # completed the cycle
shared = nil
GC_fullCollect()
deinitLock(sharedLock)
if totalWins > 0 and seen > 0:
echo "ok"
else:
echo "FAIL: wins=", totalWins, " seen=", seen

View File

@@ -0,0 +1,74 @@
discard """
cmd: "nim c --mm:yrc -d:useMalloc --threads:on $file"
output: "ok"
valgrind: "leaks"
disabled: "windows"
disabled: "freebsd"
disabled: "openbsd"
"""
# Test sharing a cyclic list between threads under YRC.
type
Node = ref object
value: int
next: Node
proc newCycle(start, count: int): Node =
## Create a cyclic linked list: start -> start+1 -> ... -> start+count-1 -> start
result = Node(value: start)
var cur = result
for i in 1..<count:
cur.next = Node(value: start + i)
cur = cur.next
cur.next = result # close the cycle
proc sumCycle(head: Node; count: int): int =
var cur = head
for i in 0..<count:
result += cur.value
cur = cur.next
const
NumThreads = 4
NodesPerCycle = 5
var
shared: Node
threads: array[NumThreads, Thread[int]]
results: array[NumThreads, int]
proc worker(id: int) {.thread.} =
# Each thread reads the shared cycle and computes a sum.
# Also creates its own local cycle to exercise the collector.
{.cast(gcsafe).}:
let local = newCycle(id * 100, NodesPerCycle)
let localSum = sumCycle(local, NodesPerCycle)
let sharedSum = sumCycle(shared, NodesPerCycle)
results[id] = sharedSum + localSum
# Create a shared cyclic list: 0 -> 1 -> 2 -> 3 -> 4 -> 0
shared = newCycle(0, NodesPerCycle)
let expectedSharedSum = 0 + 1 + 2 + 3 + 4 # = 10
for i in 0..<NumThreads:
createThread(threads[i], worker, i)
for i in 0..<NumThreads:
joinThread(threads[i])
var allOk = true
for i in 0..<NumThreads:
let expectedLocal = i * 100 * NodesPerCycle + (NodesPerCycle * (NodesPerCycle - 1) div 2)
# sum of id*100, id*100+1, ..., id*100+4
let expected = expectedSharedSum + expectedLocal
if results[i] != expected:
echo "FAIL thread ", i, ": got ", results[i], " expected ", expected
allOk = false
shared = nil # drop the shared cycle, collector should reclaim it
GC_fullCollect()
if allOk:
echo "ok"