Bug 18058 – @nogc and forwarding lazy argument, particularly with scope

Status
NEW
Severity
normal
Priority
P3
Component
dmd
Product
D
Version
D2
Platform
x86_64
OS
Linux
Creation time
2017-12-11T07:23:05Z
Last change time
2024-12-13T18:55:24Z
Keywords
industry
Assigned to
No Owner
Creator
Shachar Shemesh
Moved to GitHub: dmd#19344 →

Comments

Comment #0 by shachar — 2017-12-11T07:23:05Z
Relevant forum thread is at https://forum.dlang.org/thread/[email protected] The program: void func1(scope lazy string msg) @nogc { } void func2(scope lazy string msg) @nogc { func1(msg); } does not compile: test.d(5): Error: @nogc function 'test.func2' cannot call non-@nogc delegate 'msg' This is wrong. Since "msg" is never evaluated, the fact that it is not @nogc should not matter (not to mention we should have a syntax where we mandate that a lazy argument be @nogc, but that's another issue). According to my analysis, there are actually two problems here. The first is that "scope" on lazy is being ignored. The second problem is that the lowering is overly complicated. Instead of lowering to: void func1(string delegate() msg) @nogc { } void func2(string delegate() msg) @nogc { func1(msg); } which would work with or without the "scope", it is probably lowered to: void func1(string delegate() msg) @nogc { } void func2(string delegate() msg) @nogc { func1( (){ return msg();} ); } which, if no scope is specified, requires that func2's frame be GCed. Since the first argument is already a delegate, the second wrapping is completely unnecessary.
Comment #1 by maksim_fomin — 2017-12-11T16:58:41Z
Since scope and lazy are poorly specified, it is not surprising that sometime they behave unexpectedly (and in my experience scope and lazy features are those which are mostly overlooked). Worse, because of underspecification it is often unclear whether issue is bug or not. Compiler seems to fail here: https://github.com/dlang/dmd/blob/6f99996ba80ad51b37401552be4b3ace7f8e710b/src/ddmd/expressionsem.d#L3481 if (!tf.isnogc && sc.func.setGC()) { exp.error("@nogc %s '%s' cannot call non-@nogc %s '%s'", sc.func.kind(), sc.func.toPrettyChars(), p, exp.e1.toChars()); err = true; } i.e. because 'func2' formally requires gc. P.S. Unfortunatelly d developers put almost all efforts to fixing regressions plus some bugs and leave documentation as it is. The situation has not improved for many years. > This is wrong. Since "msg" is never evaluated, the fact that it is not > @nogc should not matter. Why? Accordging to the spec closure must be always allocated regarless of scope (of course, this can be proposed as an improvement). > The second problem is that the lowering is overly complicated. Instead > of lowering to: Again, currently compiler behaves according to the spec (in my understanding). If you prefer option 1 you can write it explicitly.
Comment #2 by greensunny12 — 2017-12-11T19:39:28Z
FWIW that attributes are set wrongly on lazy parameters and their function is a well-known bug and has been around for a long time. See e.g. issue 12647 (there's quite a list of duplicates attached to it). There even have been two PRs (see issue 12647 for a list) aimed at fixing them, but they didn't tackle the root problem. The problem becomes easy to inspect if one uses the hidden -vcg-ast flag at the compiler. For the example linked at -- import object; void assertThrown(E)(lazy E expression) { expression(); } void main() { assertThrown(delegate int() => 0); return 0; } assertThrown!int { pure @safe void assertThrown(lazy int expression) // <- should have nothrow { expression(); } } --- For yours: --- import object; @nogc void func1(lazy scope string msg) { } void func2(lazy scope string msg) { func1(delegate string() => msg()); // <- lambda doesn't have @nogc } ---
Comment #3 by shachar — 2017-12-12T02:31:01Z
(In reply to Maksim Fomin from comment #1) > > The second problem is that the lowering is overly complicated. Instead > > of lowering to: > > Again, currently compiler behaves according to the spec (in my > understanding). If you prefer option 1 you can write it explicitly. No, I cannot. "lazy" and "delegate" behave the same only internally. As far as syntax goes, I cannot get the delegate that a lazy has turned into, nor can I can pass a delegate where a function expects a lazy argument.
Comment #4 by shachar — 2017-12-12T02:37:07Z
(In reply to Seb from comment #2) > FWIW that attributes are set wrongly on lazy parameters and their function > is a well-known bug and has been around for a long time. True, but it wouldn't be a problem had the overly complex forwarding not take place. > > For yours: > > --- > import object; > @nogc void func1(lazy scope string msg) > { > } > void func2(lazy scope string msg) > { > func1(delegate string() => msg()); // <- lambda doesn't have @nogc > } > --- The problem is not that the lambda doesn't have @nogc. The problem is that if we're going to provide a lambda there, it has no choice *but* to GC. The lambda has to have access to the frame (because "scope" was not honored), and so must allocate. The issue here is that, once you've started lowering, the code the compiler emits is way under-optimized. It essentially creates a lambda whose sole purpose is evaluating another delegate that has the precise same interface. Had that redundant delegate not been generated, there would not be a need to allocate func2's frame with a GC, even without scope on the lazy.
Comment #5 by maksim_fomin — 2017-12-12T06:47:57Z
(In reply to Shachar Shemesh from comment #3) > (In reply to Maksim Fomin from comment #1) > > > The second problem is that the lowering is overly complicated. Instead > > > of lowering to: > > > > Again, currently compiler behaves according to the spec (in my > > understanding). If you prefer option 1 you can write it explicitly. > > No, I cannot. "lazy" and "delegate" behave the same only internally. As far > as syntax goes, I cannot get the delegate that a lazy has turned into, nor > can I can pass a delegate where a function expects a lazy argument. I meant rewriting in another syntax explicitly.
Comment #6 by maksim_fomin — 2017-12-12T06:54:02Z
(In reply to Shachar Shemesh from comment #4) > (In reply to Seb from comment #2) > > FWIW that attributes are set wrongly on lazy parameters and their function > > is a well-known bug and has been around for a long time. > > True, but it wouldn't be a problem had the overly complex forwarding not > take place. > > > > For yours: > > > > --- > > import object; > > @nogc void func1(lazy scope string msg) > > { > > } > > void func2(lazy scope string msg) > > { > > func1(delegate string() => msg()); // <- lambda doesn't have @nogc > > } > > --- > > The problem is not that the lambda doesn't have @nogc. The problem is that > if we're going to provide a lambda there, it has no choice *but* to GC. The > lambda has to have access to the frame (because "scope" was not honored), > and so must allocate. Exactly. This is how D works: some feature is lowered to code and if lowered code does not work, than the original code must be rewritten. Yes, compiler errs strangely as was mentioned in the forum. I am not aware of any example (except UFCS which is different case) where compiler is smart enough to know in advance than lowered code will not work, but compiler tries to rewrite in other way because it can work (i would call such behavior 'semantic optimization'). > The issue here is that, once you've started lowering, the code the compiler > emits is way under-optimized. It essentially creates a lambda whose sole > purpose is evaluating another delegate that has the precise same interface. > Had that redundant delegate not been generated, there would not be a need to > allocate func2's frame with a GC, even without scope on the lazy. As mentioned previously, this is how D works. Optimization refers to code optimization and this stage happens after semantic analysis.
Comment #7 by shachar — 2017-12-12T09:00:49Z
(In reply to Maksim Fomin from comment #6) > As mentioned previously, this is how D works. Quite so. That's why the label on this page reads "bug".
Comment #8 by maksim_fomin — 2017-12-12T12:29:35Z
(In reply to Shachar Shemesh from comment #7) > (In reply to Maksim Fomin from comment #6) > > As mentioned previously, this is how D works. > > Quite so. That's why the label on this page reads "bug". I mean dmd works as intended and according to the spec. What you want is outside of current compiler optimization. This does not prevent you from requesting new behavior as an enhancement.
Comment #9 by shachar — 2017-12-12T12:33:57Z
(In reply to Maksim Fomin from comment #8) > I mean dmd works as intended and according to the spec. What you want is > outside of current compiler optimization. This does not prevent you from > requesting new behavior as an enhancement. Feel free to modify the category this bug is filed under to the one that is applicable to bugs in the spec. Hint: it already is.
Comment #10 by robert.schadek — 2024-12-13T18:55:24Z
THIS ISSUE HAS BEEN MOVED TO GITHUB https://github.com/dlang/dmd/issues/19344 DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB