Skip to content

seq pop malfunctions in array of seqs #24596

@bptato

Description

@bptato

Description

Following code

proc main() =
  var trimMap = [@[1], @[2]]
  var z = 0
  for j in 0 ..< 2:
    var i = trimMap.high
    while i >= 0:
      #let len {.volatile} = trimMap[i].len # this way it "works"
      let len = trimMap[i].len
      if len > 0:
        z = trimMap[i].pop()
        echo "set"
        break
      dec i
    echo "popped ", i
    doAssert z != 0
  echo "z", z

main()

prints:

set
popped 1
set
popped 1
fatal.nim(53)            sysFatal
Error: unhandled exception: z.nim(15, 5) `z != 0`  [AssertionDefect]

on both devel and 2.0.14, when compiled like:

nim c --mm:arc -d:danger -d:lto --cc:clang z.nim

with clang version 18.1.8. (-d:release instead of -d:danger has the same effect.)

The crash disappears if I do any of:

  • set len to volatile
  • --passc:-fno-strict-aliasing
  • disable LTO
  • --mm:refc

It also disappears if I switch to GCC, but this is probably by accident.
The original program that I reduced this from crashes under GCC as well.

len is supposed to be set in shrink:
/* z.nim.c */
struct tySequence__qwqHTkRvwhrRyENtudHQ7g {
	NI len;
	tySequence__qwqHTkRvwhrRyENtudHQ7g_Content* p;
};
struct tySequence__qwqHTkRvwhrRyENtudHQ7g_Content {
	NI cap;
	NI data[SEQ_DECL_SIZE];
};

static N_INLINE(NI, pop__z_u27)(tySequence__qwqHTkRvwhrRyENtudHQ7g* s_p0) {
	NI result;
NI L;
NI T1_;
T1_ = (*s_p0).len;
L = ((NI) (T1_ - ((NI) 1)));
result = ((*s_p0)).p->data[L];
((*s_p0)).p->data[L] = 0;
shrink__z_u42(s_p0, (L));
	return result;
}

/* system.nim.c */
typedef struct tyObject_NimSeqPayload__g7Ny9bTlNzn6dsxoJOHv6LQ tyObject_NimSeqPayload__g7Ny9bTlNzn6dsxoJOHv6LQ;
/* ^ incomplete struct; never gets defined anywhere */
struct tyObject_NimSeqV2__PEQQ33xihBDluJkSxo81LQ {
	NI len;
	tyObject_NimSeqPayload__g7Ny9bTlNzn6dsxoJOHv6LQ* p;
};

N_LIB_PRIVATE N_NIMCALL(void, shrink__z_u42)(tySequence__qwqHTkRvwhrRyENtudHQ7g* x_p0, NI newLen_p1) {
	(*((tyObject_NimSeqV2__PEQQ33xihBDluJkSxo81LQ*) (x_p0))).len = newLen_p1;
}

Perhaps the cast violates strict aliasing?

Nim Version

Nim Compiler Version 2.3.1 [Linux: amd64]
Compiled at 2025-01-01
Copyright (c) 2006-2025 by Andreas Rumpf

git hash: 3dda60a8ce32cb7d5e3e99111399a1550c145176
active boot switches: -d:release

Current Output

No response

Expected Output

set
popped 1
set
popped 0
z1

Known Workarounds

No response

Additional Information

ETA: this seems to fix it

diff --git a/lib/system/seqs_v2.nim b/lib/system/seqs_v2.nim
index 572e77408..3817b6bc5 100644
--- a/lib/system/seqs_v2.nim
+++ b/lib/system/seqs_v2.nim
@@ -133,7 +133,7 @@ proc shrink*[T](x: var seq[T]; newLen: Natural) {.tags: [], raises: [].} =
         reset x[i]
     # XXX This is wrong for const seqs that were moved into 'x'!
     {.noSideEffect.}:
-      cast[ptr NimSeqV2[T]](addr x).len = newLen
+      cast[ptr int](addr x)[] = newLen
 
 proc grow*[T](x: var seq[T]; newLen: Natural; value: T) {.nodestroy.} =
   let oldLen = x.len

C allows aliasing with the first member, so it makes sense.
But if it's a strict aliasing problem, then it's not an adequate fix, because the cast is also performed in other functions.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions