Currently lazy parameter implies creating delegate which references local variable. Allowing to escape lazy argument without allocating local variable at heap leads to leaked pointers to some stack zone which is a memory error.
extern(C) int printf(const char*,...) @safe;
auto foo(lazy double i) @safe
{
return { return i; } ;
}
auto bar() @safe
{
double i = 4.0;
return foo(i);
}
void baz() @safe
{
double[2] i = 3.14; // this value overwrites 4.0
// replace with char[16] val = 'f';
// to print deterministic garbage
}
void main() @safe
{
auto x = bar();
baz();
printf("%f\n", x());
}
This prints 3.14 which is wrong. Potentially compiler can recognize that lazy argument escapes and allocate i = 4.0 on heap, but due to separate compilation model, it cannot do this in general case, so the code should be rejected.
This is a more complex variation of known 'ref-ref' bug which consists in returning reference to local object which has gone out of scope:
ref int foo(ref int i) { return i; }
ref int bar() { int val; return foo(i); }
Comment #1 by maxim — 2013-09-15T13:24:28Z
This is a reduced version which better explains what happens behind lazy and demonstrates similar problem (also prints garbage):
import std.stdio;
alias long[10] T;
T delegate() dg;
void foo(ref T i) @safe
{
dg = { return i; } ;
}
void bar() @safe
{
T i = 1;
foo(i);
}
void rewrite_stack() @safe
{
T tmp = -1;
}
void main()
{
bar();
rewrite_stack();
writeln(dg());
}
Comment #2 by nick — 2017-05-01T08:40:42Z
With dmd 2.074, -dip1000 the `dg = { return i; }` from comment 1 won't compile, but the original lazy code still does.
Comment #3 by dlang-bugzilla — 2018-09-21T09:02:07Z
Another test case:
///////////// test.d ////////////
@safe:
auto toDg(E)(lazy E value)
{
return { return value; };
}
C t;
class C
{
auto getDg()
{
// return () => prop;
return toDg(prop);
}
final @property string prop()
{
assert(this is t);
return null;
}
}
void smashStack() {
int[1024] dummy = 0xcafebabe;
}
void main()
{
t = new C();
auto result = t.getDg();
smashStack();
result();
}
/////////////////////////////////
The compiler needs to either reject the code (accepts-invalid), or generate a closure (wrong-code).
Upgrading severity as this can manifest as a latent bug (depending on whether the stack was overwritten or not) in @safe code.
Comment #4 by ag0aep6g — 2019-05-16T13:03:18Z
*** Issue 19880 has been marked as a duplicate of this issue. ***
Comment #5 by bugzilla — 2020-03-04T09:46:03Z
I have a dip in the works to deprecate lazy parameters, replacing them with delegates, precisely because of problems like this. So this particular issue will likely not get fixed.
Comment #6 by bugzilla — 2020-04-04T09:39:20Z
I recommend in the meantime replacing the lazy parameter with a delegate.
Comment #7 by destructionator — 2023-01-15T01:54:12Z
lazy lowers to delegate, so it should be easy to just lower to the right kind of delegate and get correct behavior. There was a dconf online talk about this in 2022.
Comment #8 by dlang-bugzilla — 2023-06-05T11:22:52Z
(In reply to Walter Bright from comment #5)
> I have a dip in the works to deprecate lazy parameters, replacing them with
> delegates, precisely because of problems like this. So this particular issue
> will likely not get fixed.
Looks like this was not an easy decision to make, as the situation is unchanged three years later. I think we should at least do something about bugs like this in the meantime - the compiler should either reject such constructs or not generate bad code for them.
Personally I am skeptical that lazy can be removed from the language at this point, because for one thing doing so will break std.exception.enforce, and make it impossible to wrap or implement assert in user code. As far as I can see, we have no choice but to support it as part of the language going forward.
Comment #9 by dlang-bugzilla — 2023-06-05T11:26:07Z
(In reply to Adam D. Ruppe from comment #7)
> lazy lowers to delegate, so it should be easy to just lower to the right
> kind of delegate and get correct behavior.
Judging from the lazy-related bugs (including this one), this lowering seems a little broken at the moment. Perhaps the way forward is to fix the lowering, and everything else will align as a result.
I think lazy existed in the language long before lambda functions; this old implementation is probably where all the bugs come from. So, perhaps today it could be reimplemented as a lowering to a lambda.
Comment #10 by robert.schadek — 2024-12-13T18:11:26Z