From 42e8472ca6eab740c0879428bd119ec94e70fe74 Mon Sep 17 00:00:00 2001 From: metagn Date: Wed, 5 Jun 2024 21:53:05 +0300 Subject: [PATCH 1/4] fix noreturn/implicit discard check logic (#23681) fixes #10440, fixes #13871, fixes #14665, fixes #19672, fixes #23677 The false positive in #23677 was caused by behavior in `implicitlyDiscardable` where only the last node of `if`/`case`/`try` etc expressions were considered, as in the final node of the final branch (in this case `else`). To fix this we use the same iteration in `implicitlyDiscardable` that we use in `endsInNoReturn`, with the difference that for an `if`/`case`/`try` statement to be implicitly discardable, all of its branches must be implicitly discardable. `noreturn` calls are also considered implicitly discardable for this reason, otherwise stuff like `if true: discardableCall() else: error()` doesn't compile. However `endsInNoReturn` also had bugs, one where `finally` was considered in noreturn checking when it shouldn't, another where only `nkIfStmt` was checked and not `nkIfExpr`, and the node given for the error message was bad. So `endsInNoReturn` now skips over `skipForDiscardable` which no longer contains `nkIfStmt`/`nkCaseStmt`/`nkTryStmt`, stores the first encountered returning node in a var parameter for the error message, and handles `finally` and `nkIfExpr`. Fixing #23677 already broke a line in `syncio` so some package code might be affected. --- compiler/sem.nim | 61 +------------ compiler/semstmts.nim | 145 ++++++++++++++++++++++++++++--- lib/std/syncio.nim | 2 +- tests/discard/t23677.nim | 12 +++ tests/discard/tdiscardable.nim | 30 +++++++ tests/discard/tfinallyerrmsg.nim | 19 ++++ 6 files changed, 195 insertions(+), 74 deletions(-) create mode 100644 tests/discard/t23677.nim create mode 100644 tests/discard/tfinallyerrmsg.nim diff --git a/compiler/sem.nim b/compiler/sem.nim index a4552beee856..760b83941d10 100644 --- a/compiler/sem.nim +++ b/compiler/sem.nim @@ -222,66 +222,7 @@ proc shouldCheckCaseCovered(caseTyp: PType): bool = else: discard -proc endsInNoReturn(n: PNode): bool = - ## check if expr ends the block like raising or call of noreturn procs do - result = false # assume it does return - - template checkBranch(branch) = - if not endsInNoReturn(branch): - # proved a branch returns - return false - - var it = n - # skip these beforehand, no special handling needed - while it.kind in {nkStmtList, nkStmtListExpr} and it.len > 0: - it = it.lastSon - - case it.kind - of nkIfStmt: - var hasElse = false - for branch in it: - checkBranch: - if branch.len == 2: - branch[1] - elif branch.len == 1: - hasElse = true - branch[0] - else: - raiseAssert "Malformed `if` statement during endsInNoReturn" - # none of the branches returned - result = hasElse # Only truly a no-return when it's exhaustive - of nkCaseStmt: - let caseTyp = skipTypes(it[0].typ, abstractVar-{tyTypeDesc}) - # semCase should already have checked for exhaustiveness in this case - # effectively the same as having an else - var hasElse = caseTyp.shouldCheckCaseCovered() - - # actual noreturn checks - for i in 1 ..< it.len: - let branch = it[i] - checkBranch: - case branch.kind - of nkOfBranch: - branch[^1] - of nkElifBranch: - branch[1] - of nkElse: - hasElse = true - branch[0] - else: - raiseAssert "Malformed `case` statement in endsInNoReturn" - # Can only guarantee a noreturn if there is an else or it's exhaustive - result = hasElse - of nkTryStmt: - checkBranch(it[0]) - for i in 1 ..< it.len: - let branch = it[i] - checkBranch(branch[^1]) - # none of the branches returned - result = true - else: - result = it.kind in nkLastBlockStmts or - it.kind in nkCallKinds and it[0].kind == nkSym and sfNoReturn in it[0].sym.flags +proc endsInNoReturn(n: PNode): bool proc commonType*(c: PContext; x: PType, y: PNode): PType = # ignore exception raising branches in case/if expressions diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim index a29b07d927c3..8c7dda4e7c2c 100644 --- a/compiler/semstmts.nim +++ b/compiler/semstmts.nim @@ -132,17 +132,140 @@ proc semExprBranchScope(c: PContext, n: PNode; expectedType: PType = nil): PNode closeScope(c) const - skipForDiscardable = {nkIfStmt, nkIfExpr, nkCaseStmt, nkOfBranch, - nkElse, nkStmtListExpr, nkTryStmt, nkFinally, nkExceptBranch, + skipForDiscardable = {nkStmtList, nkStmtListExpr, + nkOfBranch, nkElse, nkFinally, nkExceptBranch, nkElifBranch, nkElifExpr, nkElseExpr, nkBlockStmt, nkBlockExpr, nkHiddenStdConv, nkHiddenDeref} proc implicitlyDiscardable(n: PNode): bool = - var n = n - while n.kind in skipForDiscardable: n = n.lastSon - result = n.kind in nkLastBlockStmts or - (isCallExpr(n) and n[0].kind == nkSym and - sfDiscardable in n[0].sym.flags) + # same traversal as endsInNoReturn + template checkBranch(branch) = + if not implicitlyDiscardable(branch): + return false + + var it = n + # skip these beforehand, no special handling needed + while it.kind in skipForDiscardable and it.len > 0: + it = it.lastSon + + case it.kind + of nkIfExpr, nkIfStmt: + for branch in it: + checkBranch: + if branch.len == 2: + branch[1] + elif branch.len == 1: + branch[0] + else: + raiseAssert "Malformed `if` statement during implicitlyDiscardable" + # all branches are discardable + result = true + of nkCaseStmt: + for i in 1 ..< it.len: + let branch = it[i] + checkBranch: + case branch.kind + of nkOfBranch: + branch[^1] + of nkElifBranch: + branch[1] + of nkElse: + branch[0] + else: + raiseAssert "Malformed `case` statement in endsInNoReturn" + # all branches are discardable + result = true + of nkTryStmt: + checkBranch(it[0]) + for i in 1 ..< it.len: + let branch = it[i] + if branch.kind != nkFinally: + checkBranch(branch[^1]) + # all branches are discardable + result = true + of nkCallKinds: + result = it[0].kind == nkSym and {sfDiscardable, sfNoReturn} * it[0].sym.flags != {} + of nkLastBlockStmts: + result = true + else: + result = false + +proc endsInNoReturn(n: PNode, returningNode: var PNode): bool = + ## check if expr ends the block like raising or call of noreturn procs do + result = false # assume it does return + + template checkBranch(branch) = + if not endsInNoReturn(branch, returningNode): + # proved a branch returns + return false + + var it = n + # skip these beforehand, no special handling needed + while it.kind in skipForDiscardable and it.len > 0: + it = it.lastSon + + case it.kind + of nkIfExpr, nkIfStmt: + var hasElse = false + for branch in it: + checkBranch: + if branch.len == 2: + branch[1] + elif branch.len == 1: + hasElse = true + branch[0] + else: + raiseAssert "Malformed `if` statement during endsInNoReturn" + # none of the branches returned + result = hasElse # Only truly a no-return when it's exhaustive + of nkCaseStmt: + let caseTyp = skipTypes(it[0].typ, abstractVar-{tyTypeDesc}) + # semCase should already have checked for exhaustiveness in this case + # effectively the same as having an else + var hasElse = caseTyp.shouldCheckCaseCovered() + + # actual noreturn checks + for i in 1 ..< it.len: + let branch = it[i] + checkBranch: + case branch.kind + of nkOfBranch: + branch[^1] + of nkElifBranch: + branch[1] + of nkElse: + hasElse = true + branch[0] + else: + raiseAssert "Malformed `case` statement in endsInNoReturn" + # Can only guarantee a noreturn if there is an else or it's exhaustive + result = hasElse + of nkTryStmt: + checkBranch(it[0]) + var lastIndex = it.len - 1 + if it[lastIndex].kind == nkFinally: + # if finally is noreturn, then the entire statement is noreturn + if endsInNoReturn(it[lastIndex][^1], returningNode): + return true + dec lastIndex + for i in 1 .. lastIndex: + let branch = it[i] + checkBranch(branch[^1]) + # none of the branches returned + result = true + of nkLastBlockStmts: + result = true + of nkCallKinds: + result = it[0].kind == nkSym and sfNoReturn in it[0].sym.flags + if not result: + returningNode = it + else: + result = false + returningNode = it + +proc endsInNoReturn(n: PNode): bool = + var dummy: PNode = nil + result = endsInNoReturn(n, dummy) proc fixNilType(c: PContext; n: PNode) = if isAtom(n): @@ -165,13 +288,9 @@ proc discardCheck(c: PContext, result: PNode, flags: TExprFlags) = localError(c.config, result.info, "expression has no type: " & renderTree(result, {renderNoComments})) else: - var n = result - while n.kind in skipForDiscardable: - if n.kind == nkTryStmt: n = n[0] - else: n = n.lastSon - # Ignore noreturn procs since they don't have a type - if n.endsInNoReturn: + var n = result + if result.endsInNoReturn(n): return var s = "expression '" & $n & "' is of type '" & diff --git a/lib/std/syncio.nim b/lib/std/syncio.nim index 2b39375eaf00..c34a025af5c8 100644 --- a/lib/std/syncio.nim +++ b/lib/std/syncio.nim @@ -874,7 +874,7 @@ proc writeFile*(filename: string, content: openArray[byte]) {.since: (1, 1).} = var f: File = nil if open(f, filename, fmWrite): try: - f.writeBuffer(unsafeAddr content[0], content.len) + discard f.writeBuffer(unsafeAddr content[0], content.len) finally: close(f) else: diff --git a/tests/discard/t23677.nim b/tests/discard/t23677.nim new file mode 100644 index 000000000000..1ed7386bde20 --- /dev/null +++ b/tests/discard/t23677.nim @@ -0,0 +1,12 @@ +discard """ + errormsg: "expression '0' is of type 'int literal(0)' and has to be used (or discarded); start of expression here: t23677.nim(1, 1)" + line: 10 + column: 3 +""" + +# issue #23677 + +if true: + 0 +else: + raise newException(ValueError, "err") diff --git a/tests/discard/tdiscardable.nim b/tests/discard/tdiscardable.nim index 69cb9f6a195f..5988f59497d1 100644 --- a/tests/discard/tdiscardable.nim +++ b/tests/discard/tdiscardable.nim @@ -5,6 +5,7 @@ tdiscardable 1 something defered something defered +hi ''' """ @@ -110,3 +111,32 @@ block: doAssertRaises(ValueError): doAssert foo() == 12 + +block: # issue #10440 + proc x(): int {.discardable.} = discard + try: + x() + finally: + echo "hi" + +import macros + +block: # issue #14665 + macro test(): untyped = + let b = @[1, 2, 3, 4] + + result = nnkStmtList.newTree() + var i = 0 + while i < b.len: + if false: + # this quote do is mandatory, removing it fixes the problem + result.add quote do: + let testtest = 5 + else: + result.add quote do: + let test = 6 + inc i + # removing this continue fixes the problem too + continue + inc i + test() diff --git a/tests/discard/tfinallyerrmsg.nim b/tests/discard/tfinallyerrmsg.nim new file mode 100644 index 000000000000..fbc8140aaf29 --- /dev/null +++ b/tests/discard/tfinallyerrmsg.nim @@ -0,0 +1,19 @@ +discard """ + cmd: "nim check $file" +""" + +block: # issue #19672 + try: + 10 #[tt.Error + ^ expression '10' is of type 'int literal(10)' and has to be used (or discarded); start of expression here: tfinallyerrmsg.nim(5, 1)]# + finally: + echo "Finally block" + +block: # issue #13871 + template t(body: int) = + try: + body + finally: + echo "expression" + t: 2 #[tt.Error + ^ expression '2' is of type 'int literal(2)' and has to be used (or discarded)]# From 2d1533f34f74d69978f20d09be28740be2ec721c Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:54:00 +0800 Subject: [PATCH 2/4] fixes #5901 #21211; don't fold cast function types because of gcc 14 (#23683) follow up https://github.com/nim-lang/Nim/pull/6265 fixes #5901 fixes #21211 It causes many problems with gcc14 if we fold the cast function types. Let's check what it will break --- compiler/semfold.nim | 3 ++- tests/misc/tcast.nim | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/semfold.nim b/compiler/semfold.nim index f7da4ea7526a..466df2e4eb54 100644 --- a/compiler/semfold.nim +++ b/compiler/semfold.nim @@ -771,7 +771,8 @@ proc getConstExpr(m: PSym, n: PNode; idgen: IdGenerator; g: ModuleGraph): PNode of nkCast: var a = getConstExpr(m, n[1], idgen, g) if a == nil: return - if n.typ != nil and n.typ.kind in NilableTypes: + if n.typ != nil and n.typ.kind in NilableTypes and + not (n.typ.kind == tyProc and a.typ.kind == tyProc): # we allow compile-time 'cast' for pointer types: result = a result.typ = n.typ diff --git a/tests/misc/tcast.nim b/tests/misc/tcast.nim index 6d67b1c5283c..73196e76cbcd 100644 --- a/tests/misc/tcast.nim +++ b/tests/misc/tcast.nim @@ -1,6 +1,7 @@ discard """ output: ''' Hello World +Hello World Hello World''' joinable: false """ @@ -8,7 +9,7 @@ type MyProc = proc() {.cdecl.} type MyProc2 = proc() {.nimcall.} type MyProc3 = proc() #{.closure.} is implicit -proc testProc() = echo "Hello World" +proc testProc() {.exportc:"foo".} = echo "Hello World" template reject(x) = doAssert(not compiles(x)) @@ -23,6 +24,10 @@ proc callPointer(p: pointer) = ffunc0() ffunc1() + # bug #5901 + proc foo() {.importc.} + (cast[proc(a: int) {.cdecl.}](foo))(5) + callPointer(cast[pointer](testProc)) reject: discard cast[enum](0) From 87e56cabbb4d0d7326c37799d34d10e395a970fc Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:54:25 +0800 Subject: [PATCH 3/4] make `std/options` compatible with strictdefs (#23675) --- lib/pure/options.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pure/options.nim b/lib/pure/options.nim index b7a6a62128be..b34ff72c0dfb 100644 --- a/lib/pure/options.nim +++ b/lib/pure/options.nim @@ -117,9 +117,10 @@ proc option*[T](val: sink T): Option[T] {.inline.} = assert option[Foo](nil).isNone assert option(42).isSome - result.val = val - when T isnot SomePointer: - result.has = true + when T is SomePointer: + result = Option[T](val: val) + else: + result = Option[T](has: true, val: val) proc some*[T](val: sink T): Option[T] {.inline.} = ## Returns an `Option` that has the value `val`. @@ -136,10 +137,9 @@ proc some*[T](val: sink T): Option[T] {.inline.} = when T is SomePointer: assert not val.isNil - result.val = val + result = Option[T](val: val) else: - result.has = true - result.val = val + result = Option[T](has: true, val: val) proc none*(T: typedesc): Option[T] {.inline.} = ## Returns an `Option` for this type that has no value. From 69d0b73d667c4be9383f29cda3f70e411995d9af Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 Jun 2024 00:52:01 +0200 Subject: [PATCH 4/4] fixes #22510 (#23100) --- lib/system/alloc.nim | 51 ++++++++++++++++------------------ tests/alloc/tmembug.nim | 54 ++++++++++++++++++++++++++++++++++++ tests/alloc/tmembug2.nim | 58 +++++++++++++++++++++++++++++++++++++++ tests/threads/tmembug.nim | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 tests/alloc/tmembug.nim create mode 100644 tests/alloc/tmembug2.nim create mode 100644 tests/threads/tmembug.nim diff --git a/lib/system/alloc.nim b/lib/system/alloc.nim index 9c7c83aab03f..441dc17194d5 100644 --- a/lib/system/alloc.nim +++ b/lib/system/alloc.nim @@ -93,8 +93,6 @@ type freeList: ptr FreeCell free: int # how many bytes remain acc: int # accumulator for small object allocation - when defined(gcDestructors): - sharedFreeList: ptr FreeCell # make no attempt at avoiding false sharing for now for this object field data {.align: MemAlign.}: UncheckedArray[byte] # start of usable memory BigChunk = object of BaseChunk # not necessarily > PageSize! @@ -109,7 +107,9 @@ type MemRegion = object when not defined(gcDestructors): minLargeObj, maxLargeObj: int - freeSmallChunks: array[0..max(1,SmallChunkSize div MemAlign-1), PSmallChunk] + freeSmallChunks: array[0..max(1, SmallChunkSize div MemAlign-1), PSmallChunk] + when defined(gcDestructors): + sharedFreeLists: array[0..max(1, SmallChunkSize div MemAlign-1), ptr FreeCell] flBitmap: uint32 slBitmap: array[RealFli, uint32] matrix: array[RealFli, array[MaxSli, PBigChunk]] @@ -777,8 +777,10 @@ when defined(gcDestructors): sysAssert c.next == nil, "c.next pointer must be nil" atomicPrepend a.sharedFreeListBigChunks, c - proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell) {.inline.} = - atomicPrepend c.sharedFreeList, f + proc addToSharedFreeList(c: PSmallChunk; f: ptr FreeCell; size: int) {.inline.} = + atomicPrepend c.owner.sharedFreeLists[size], f + + const MaxSteps = 20 proc compensateCounters(a: var MemRegion; c: PSmallChunk; size: int) = # rawDealloc did NOT do the usual: @@ -788,30 +790,26 @@ when defined(gcDestructors): # we split the list in order to achieve bounded response times. var it = c.freeList var x = 0 - var maxIters = 20 # make it time-bounded while it != nil: - if maxIters == 0: - let rest = it.next.loada - if rest != nil: - it.next.storea nil - addToSharedFreeList(c, rest) - break inc x, size - it = it.next.loada - dec maxIters - inc(c.free, x) + let chunk = cast[PSmallChunk](pageAddr(it)) + inc(chunk.free, x) + it = it.next dec(a.occ, x) proc freeDeferredObjects(a: var MemRegion; root: PBigChunk) = var it = root - var maxIters = 20 # make it time-bounded + var maxIters = MaxSteps # make it time-bounded while true: + let rest = it.next.loada + it.next.storea nil + deallocBigChunk(a, cast[PBigChunk](it)) if maxIters == 0: - let rest = it.next.loada - it.next.storea nil - addToSharedFreeListBigChunks(a, rest) + if rest != nil: + addToSharedFreeListBigChunks(a, rest) + sysAssert a.sharedFreeListBigChunks != nil, "re-enqueing failed" break - it = it.next.loada + it = rest dec maxIters if it == nil: break @@ -835,8 +833,6 @@ proc rawAlloc(a: var MemRegion, requestedSize: int): pointer = sysAssert c.size == PageSize, "rawAlloc 3" c.size = size c.acc = size - when defined(gcDestructors): - c.sharedFreeList = nil c.free = SmallChunkSize - smallChunkOverhead() - size sysAssert c.owner == addr(a), "rawAlloc: No owner set!" c.next = nil @@ -853,10 +849,11 @@ proc rawAlloc(a: var MemRegion, requestedSize: int): pointer = when defined(gcDestructors): if c.freeList == nil: when hasThreadSupport: - c.freeList = atomicExchangeN(addr c.sharedFreeList, nil, ATOMIC_RELAXED) + # Steal the entire list from `sharedFreeList`: + c.freeList = atomicExchangeN(addr a.sharedFreeLists[s], nil, ATOMIC_RELAXED) else: - c.freeList = c.sharedFreeList - c.sharedFreeList = nil + c.freeList = a.sharedFreeLists[s] + a.sharedFreeLists[s] = nil compensateCounters(a, c, size) if c.freeList == nil: sysAssert(c.acc + smallChunkOverhead() + size <= SmallChunkSize, @@ -923,7 +920,7 @@ proc rawDealloc(a: var MemRegion, p: pointer) = if isSmallChunk(c): # `p` is within a small chunk: var c = cast[PSmallChunk](c) - var s = c.size + let s = c.size # ^ We might access thread foreign storage here. # The other thread cannot possibly free this block as it's still alive. var f = cast[ptr FreeCell](p) @@ -957,7 +954,7 @@ proc rawDealloc(a: var MemRegion, p: pointer) = freeBigChunk(a, cast[PBigChunk](c)) else: when defined(gcDestructors): - addToSharedFreeList(c, f) + addToSharedFreeList(c, f, s div MemAlign) sysAssert(((cast[int](p) and PageMask) - smallChunkOverhead()) %% s == 0, "rawDealloc 2") else: diff --git a/tests/alloc/tmembug.nim b/tests/alloc/tmembug.nim new file mode 100644 index 000000000000..63b51ec5daef --- /dev/null +++ b/tests/alloc/tmembug.nim @@ -0,0 +1,54 @@ +discard """ + joinable: false +""" + +import std / [atomics, strutils, sequtils] + +type + BackendMessage* = object + field*: seq[int] + +var + chan1: Channel[BackendMessage] + chan2: Channel[BackendMessage] + +chan1.open() +chan2.open() + +proc routeMessage*(msg: BackendMessage) = + discard chan2.trySend(msg) + +var + recv: Thread[void] + stopToken: Atomic[bool] + +proc recvMsg() = + while not stopToken.load(moRelaxed): + let resp = chan1.tryRecv() + if resp.dataAvailable: + routeMessage(resp.msg) + echo "child consumes ", formatSize getOccupiedMem() + +createThread[void](recv, recvMsg) + +const MESSAGE_COUNT = 100 + +proc main() = + let msg: BackendMessage = BackendMessage(field: (0..500).toSeq()) + for j in 0..0: #100: + echo "New iteration" + + for _ in 1..MESSAGE_COUNT: + chan1.send(msg) + echo "After sending" + + var counter = 0 + while counter < MESSAGE_COUNT: + let resp = recv(chan2) + counter.inc + echo "After receiving ", formatSize getOccupiedMem() + + stopToken.store true, moRelaxed + joinThreads(recv) + +main() diff --git a/tests/alloc/tmembug2.nim b/tests/alloc/tmembug2.nim new file mode 100644 index 000000000000..01bce6f141a3 --- /dev/null +++ b/tests/alloc/tmembug2.nim @@ -0,0 +1,58 @@ +discard """ + disabled: "true" +""" + +import std / [atomics, strutils, sequtils, isolation] + +import threading / channels + +type + BackendMessage* = object + field*: seq[int] + +const MESSAGE_COUNT = 100 + +var + chan1 = newChan[BackendMessage](MESSAGE_COUNT*2) + chan2 = newChan[BackendMessage](MESSAGE_COUNT*2) + +#chan1.open() +#chan2.open() + +proc routeMessage*(msg: BackendMessage) = + var m = isolate(msg) + discard chan2.trySend(m) + +var + thr: Thread[void] + stopToken: Atomic[bool] + +proc recvMsg() = + while not stopToken.load(moRelaxed): + var resp: BackendMessage + if chan1.tryRecv(resp): + #if resp.dataAvailable: + routeMessage(resp) + echo "child consumes ", formatSize getOccupiedMem() + +createThread[void](thr, recvMsg) + +proc main() = + let msg: BackendMessage = BackendMessage(field: (0..5).toSeq()) + for j in 0..100: + echo "New iteration" + + for _ in 1..MESSAGE_COUNT: + chan1.send(msg) + echo "After sending" + + var counter = 0 + while counter < MESSAGE_COUNT: + let resp = recv(chan2) + counter.inc + echo "After receiving ", formatSize getOccupiedMem() + + stopToken.store true, moRelaxed + joinThreads(thr) + +main() diff --git a/tests/threads/tmembug.nim b/tests/threads/tmembug.nim new file mode 100644 index 000000000000..3618f0eccbb6 --- /dev/null +++ b/tests/threads/tmembug.nim @@ -0,0 +1,51 @@ + +import std / [atomics, strutils, sequtils] + +type + BackendMessage* = object + field*: seq[int] + +var + chan1: Channel[BackendMessage] + chan2: Channel[BackendMessage] + +chan1.open() +chan2.open() + +proc routeMessage*(msg: BackendMessage) = + discard chan2.trySend(msg) + +var + recv: Thread[void] + stopToken: Atomic[bool] + +proc recvMsg() = + while not stopToken.load(moRelaxed): + let resp = chan1.tryRecv() + if resp.dataAvailable: + routeMessage(resp.msg) + echo "child consumes ", formatSize getOccupiedMem() + +createThread[void](recv, recvMsg) + +const MESSAGE_COUNT = 100 + +proc main() = + let msg: BackendMessage = BackendMessage(field: (0..500).toSeq()) + for j in 0..0: #100: + echo "New iteration" + + for _ in 1..MESSAGE_COUNT: + chan1.send(msg) + echo "After sending" + + var counter = 0 + while counter < MESSAGE_COUNT: + let resp = recv(chan2) + counter.inc + echo "After receiving ", formatSize getOccupiedMem() + + stopToken.store true, moRelaxed + joinThreads(recv) + +main()