Stack materialization, zeroing, and copying of purely heap-based data with `iterator` and case object
Nim Version
Nim Compiler Version 2.2.4 [Linux: amd64]
Compiled at 2025-11-14
Copyright (c) 2006-2025 by Andreas Rumpf
git hash: f7145dd26efeeeb6eeae6fff649db244d81b212d
active boot switches: -d:release
Nim Compiler Version 2.2.6 [Linux: amd64]
Compiled at 2025-11-14
Copyright (c) 2006-2025 by Andreas Rumpf
git hash: ab00c56904e3126ad826bb520d243513a139436a
active boot switches: -d:release
Nim Compiler Version 2.2.7 [Linux: amd64]
Compiled at 2025-11-14
Copyright (c) 2006-2025 by Andreas Rumpf
git hash: 38384d040a1d7a7a6242698c56287ecadf4e7e56
active boot switches: -d:release
Nim Compiler Version 2.3.1 [Linux: amd64]
Compiled at 2025-11-14
Copyright (c) 2006-2025 by Andreas Rumpf
git hash: 39be9b981d6608c9da33ca5c25118114eab121ea
active boot switches: -d:release
gcc (Debian 15.2.0-8) 15.2.0
Copyright (C) 2025 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Description
type K = object
case h: bool
of false, true: v: array[131072, int]
iterator u(_: array[131072, int]): int = yield 0
for _ in u((new K)[].v): discard
And compile with
nim c --passC="-flto -fstack-usage -Werror=stack-usage=524288" --passL="-flto -fstack-usage -Werror=stack-usage=524288" j.nim
The specific flags are just to get the automated stack size warning/error. Nim's C codegen doesn't change regardless, this just makes it easier to detect the large stack variable.
Nim generates:
typedef NI tyArray__l8t5ob5uoIl4BSdK55SzmA[131072];
struct tyObject_K__FtjI9b3q2PIgzdRYyTysrNQ {
NIM_BOOL h;
union {
tyArray__l8t5ob5uoIl4BSdK55SzmA v;
};
};
/* ... */
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
{
tyObject_K__FtjI9b3q2PIgzdRYyTysrNQ *colontmpD_;
tyArray__l8t5ob5uoIl4BSdK55SzmA colontmp_;
colontmpD_ = NIM_NIL;
nimZeroMem(((void *)colontmp_), sizeof(tyArray__l8t5ob5uoIl4BSdK55SzmA));
colontmpD_ = new__j_u6();
if ((!(((3 & (((NU8)1) << (((NU)(*colontmpD_).h) & 7U))) != 0)))) {
raiseFieldError2(TM__LxzjbF2clhIWozvyVkkJCg_3, ((NI)(*colontmpD_).h));
goto BeforeRet_;
}
nimCopyMem(((void *)colontmp_), ((NIM_CONST void *)(*colontmpD_).v),
sizeof(tyArray__l8t5ob5uoIl4BSdK55SzmA));
___j_u66 = ((NI)0);
eqdestroy___j_u18(colontmpD_);
}
BeforeRet_:;
nimTestErrorFlag();
popFrame();
}
}
Where it materializes this colontmp_ variable of type tyArray__l8t5ob5uoIl4BSdK55SzmA, i.e. NI[131072], and zeros and copies through it, on the stack despite every data structure here existing in the heap. Aside from being pointlessly inefficient, this can corrupt a stack, or in this case, trigger a warning or error set up to avoid such stack corruption.
Current Output
CC: system/exceptions.nim
CC: std/private/digitsutils.nim
CC: system/dollars.nim
CC: system.nim
CC: j.nim
Hint: [Link]
/tmp/tmp.7jPorOJyit/@mj.nim.c: In function ‘NimMainModule’:
/tmp/tmp.7jPorOJyit/@mj.nim.c:162:15: error: stack usage is 1048656 bytes [-Werror=stack-usage=]
162 | N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
| ^
lto1: some warnings being treated as errors
lto-wrapper: fatal error: gcc returned 1 exit status
compilation terminated.
/usr/bin/ld: error: lto-wrapper failed
collect2: error: ld returned 1 exit status
Error: execution of an external program failed: 'gcc -o /tmp/j /tmp/tmp.7jPorOJyit/@[email protected] /tmp/tmp.7jPorOJyit/@pstd@[email protected] /tmp/tmp.7jPorOJyit/@[email protected] /tmp/tmp.7jPorOJyit/@psystem.nim.c.o /tmp/tmp.7jPorOJyit/@mj.nim.c.o -pthread -pthread -flto -fstack-usage -Werror=stack-usage=524288 -ldl'
Expected Output
No gcc -fstack-usage -Werror=stack-usage=524288-related error, i.e. no stack materialization of heap data in this scenario.
Known Workarounds
No response
Additional Information
No response
If one wants a version where the object field in question isn't part of the case-dependent part of the case object, this does the same thing:
type K = object
case h: bool
of false, true: r: int
v: array[131072, int]
iterator u(_: array[131072, int]): int = yield 0
for _ in u((new K)[].v): discard
where it generates instead
typedef NI tyArray__l8t5ob5uoIl4BSdK55SzmA[131072];
struct tyObject_K__FtjI9b3q2PIgzdRYyTysrNQ {
NIM_BOOL h;
union {
NI r;
};
tyArray__l8t5ob5uoIl4BSdK55SzmA v;
};
/* ... */
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
{
tyObject_K__FtjI9b3q2PIgzdRYyTysrNQ *colontmpD_;
tyArray__l8t5ob5uoIl4BSdK55SzmA colontmp_;
colontmpD_ = NIM_NIL;
nimZeroMem(((void *)colontmp_), sizeof(tyArray__l8t5ob5uoIl4BSdK55SzmA));
colontmpD_ = new__j_u7();
nimCopyMem(((void *)colontmp_), ((NIM_CONST void *)(*colontmpD_).v),
sizeof(tyArray__l8t5ob5uoIl4BSdK55SzmA));
___j_u68 = ((NI)0);
eqdestroy___j_u19(colontmpD_);
}
nimTestErrorFlag();
popFrame();
}
}
and it doesn't do the field checking of:
if ((!(((3 & (((NU8)1) << (((NU)(*colontmpD_).h) & 7U))) != 0)))) {
raiseFieldError2(TM__LxzjbF2clhIWozvyVkkJCg_3, ((NI)(*colontmpD_).h));
goto BeforeRet_;
}
so that's not the issue here.
I agree the code generator is all to happy to copy stack objects around but at what point are you gonna accept that array[131072, int] would be fine with a ref indirection that prevents this from happening by design?
I agree the code generator is all to happy to copy stack objects around but at what point are you gonna accept that
array[131072, int]would be fine with arefindirection that prevents this from happening by design?
In general, these appear as part of large objects which are deliberately only held in the heap. Sometimes, one would like to use specific fields of these objects, which can even on occasion be case objects, as iterator arguments. That is more or less explicitly using ref, just on the object as a whole. It shouldn't be necessary to make each field of these objects also ref to avoid this.