-
-
Notifications
You must be signed in to change notification settings - Fork 273
WIP: Codegen for NewExp::argprefix. #914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
f1b89ce to
6a452b1
Compare
|
This argprefix is only set by the front-end in case any arg expression may throw and previously constructed objects need to be destructed (dlang/dmd#4078). So ideally, we'd try to evaluate argprefix right before the constructor call, and only destruct it if its evaluation failed due to an exception, as the constructor takes care of destruction on success, i.e., something like: One issue of this approach arises when the NewExp is itself evaluated as part of a toElemDtor() call. In this case, I think the variables might be destructed a second time... /edit: This seems more feasible: |
|
The 3rd commit fixes this issue for NewExp expressions. Above test works for |
|
Seems like we don't: import core.stdc.stdio;
struct Struct { ~this() { printf("dtor\n"); } }
int foo(bool doThrow) { if (doThrow) throw new Exception("bla"); return 1; }
void main()
{
bool doThrow = true;
int a;
try
{
a = (Struct(), foo(doThrow)); // or Struct(), a = foo(doThrow);
printf("exiting try\n");
}
catch (Exception e) { printf("catch\n"); }
}The dtor isn't invoked if foo() throws. We should probably destruct a CommaExp in a finally block, similar to argprefix (but no need for a gate). |
|
The issue can be mitigated by (1) always destructing in a finally block, i.e., enforcing mode Another example of currently failing code: import core.stdc.stdio;
struct Struct { int a = 6; ~this() { printf("dtor\n"); } }
int foo(bool doThrow) { printf("foo()\n"); if (doThrow) throw new Exception("bla"); return 1; }
void main()
{
bool doThrow = true;
int r = Struct().a + foo(doThrow);
}The destructor of the temporary |
|
To be honest, it has been a while since I last looked at that piece of code, so @redstar or @AlexeyProkhin might be the better people to ask. |
gen/llvmhelpers.h
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prototype can be removed.
|
@kinke The The problem here is that the frontend AST is not well suited for our code generation. An additional lowering step is missing. (Insert the |
|
There is still the option of convincing people to change the upstream AST accordingly. |
|
I've been thinking about this issue and how to fix it properly. Currently, most expressions are codegen'd via That design is a problem imo. I'm in favor of collecting all temporaries when There are some issues with this approach though. Firstly, That'd obviously require quite some effort. What do you think about this approach? And how should exceptions thrown by destructors be handled? |
|
Your approach seems reasonable. It extends the current search for temporaries to the required level. |
|
Thanks Kai. The 4th commit is a 1st sketch. It compiles druntime, but phobos fails (only) for I guess I messed up the scope somewhere. |
|
Self-contained test case: struct Foo {
~this() {}
int a;
alias a this;
}
void callee(int, lazy string);
void caller(string s) {
callee(Foo(), "prefix" ~ s);
}The issue is that the delegate function body for the lazy parameter is codegen'd while the calling statement is, and picks up the temporaries from within the caller because of the way I think the correct solution is to drop all uses of At some point in the future we should also consider explicitly passing around the visitor(s) though all the codegen functions instead of relying on global state. |
|
Thanks David. The |
|
I chose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reordered the two blocks here, so that the more probable assertPassed block comes before assertFailed. I find the resulting .ll code way more readable this way.
Unfortunately, it's not that simple for landing pads - their basic blocks currently need to be created to push (finalize) a landing pad. The normally expected postinvoke basic block is created later, before the invoke instruction and thus placed after the landing pad, leading to .ll code in which following the expected control flow is overly complicated, especially wrt. nested invokes (e.g, in the landing pad).
|
Alright. import core.stdc.stdio;
struct Struct
{
int a = 6;
alias a this;
~this() { printf("dtor\n"); }
}
int foo(bool doThrow)
{
printf("foo()\n");
if (doThrow)
throw new Exception("bla");
return 1;
}
void main()
{
bool doThrow = true;
int r = (Struct(), foo(false), Struct()) + foo(doThrow);
}now yields this .ll: define i32 @_Dmain({ i64, { i64, i8* }* } %unnamed) #0 {
%doThrow = alloca i8, align 1 ; [#uses = 2 type = i8*]
%r = alloca i32, align 4 ; [#uses = 1 type = i32*]
%__slStruc2 = alloca %toelem.Struct, align 4 ; [#uses = 6 type = %toelem.Struct*]
%.structliteral = alloca %toelem.Struct ; [#uses = 2 type = %toelem.Struct*]
%__slStruc3 = alloca %toelem.Struct, align 4 ; [#uses = 4 type = %toelem.Struct*]
%.structliteral1 = alloca %toelem.Struct ; [#uses = 2 type = %toelem.Struct*]
store i8 1, i8* %doThrow
%1 = getelementptr %toelem.Struct* %.structliteral, i32 0, i32 0 ; [#uses = 1 type = i32*]
store i32 6, i32* %1
%2 = bitcast %toelem.Struct* %__slStruc2 to i8* ; [#uses = 1 type = i8*]
%3 = bitcast %toelem.Struct* %.structliteral to i8* ; [#uses = 1 type = i8*]
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %2, i8* %3, i64 4, i32 1, i1 false)
%tmp = invoke i32 @_D6toelem3fooFbZi(i1 false)
to label %postinvoke unwind label %temporariesLandingPad ; [#uses = 0 type = i32]
postinvoke: ; preds = %0
%4 = getelementptr %toelem.Struct* %.structliteral1, i32 0, i32 0 ; [#uses = 1 type = i32*]
store i32 6, i32* %4
%5 = bitcast %toelem.Struct* %__slStruc3 to i8* ; [#uses = 1 type = i8*]
%6 = bitcast %toelem.Struct* %.structliteral1 to i8* ; [#uses = 1 type = i8*]
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %5, i8* %6, i64 4, i32 1, i1 false)
%7 = getelementptr %toelem.Struct* %__slStruc3, i32 0, i32 0 ; [#uses = 1 type = i32*]
%8 = load i8* %doThrow ; [#uses = 1 type = i8]
%9 = trunc i8 %8 to i1 ; [#uses = 1 type = i1]
%tmp4 = invoke i32 @_D6toelem3fooFbZi(i1 %9)
to label %postinvoke3 unwind label %temporariesLandingPad2 ; [#uses = 1 type = i32]
postinvoke3: ; preds = %postinvoke
%10 = load i32* %7 ; [#uses = 1 type = i32]
%11 = add i32 %10, %tmp4 ; [#uses = 1 type = i32]
store i32 %11, i32* %r
invoke void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc3)
to label %postinvoke10 unwind label %temporariesLandingPad9
postinvoke10: ; preds = %postinvoke3
call void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc2)
ret i32 0
temporariesLandingPad9: ; preds = %postinvoke3
%landing_pad11 = landingpad { i8*, i32 } personality i32 (i32, i32, i64, i8*, i8*)* @_d_eh_personality
cleanup ; [#uses = 2 type = { i8*, i32 }]
%12 = extractvalue { i8*, i32 } %landing_pad11, 0 ; [#uses = 1 type = i8*]
%13 = extractvalue { i8*, i32 } %landing_pad11, 1 ; [#uses = 0 type = i32]
call void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc2)
call void @_d_eh_resume_unwind(i8* %12)
unreachable
temporariesLandingPad2: ; preds = %postinvoke
%landing_pad5 = landingpad { i8*, i32 } personality i32 (i32, i32, i64, i8*, i8*)* @_d_eh_personality
cleanup ; [#uses = 2 type = { i8*, i32 }]
%14 = extractvalue { i8*, i32 } %landing_pad5, 0 ; [#uses = 1 type = i8*]
%15 = extractvalue { i8*, i32 } %landing_pad5, 1 ; [#uses = 0 type = i32]
invoke void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc3)
to label %postinvoke7 unwind label %temporariesLandingPad6
postinvoke7: ; preds = %temporariesLandingPad2
call void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc2)
call void @_d_eh_resume_unwind(i8* %14)
unreachable
temporariesLandingPad6: ; preds = %temporariesLandingPad2
%landing_pad8 = landingpad { i8*, i32 } personality i32 (i32, i32, i64, i8*, i8*)* @_d_eh_personality
cleanup ; [#uses = 2 type = { i8*, i32 }]
%16 = extractvalue { i8*, i32 } %landing_pad8, 0 ; [#uses = 1 type = i8*]
%17 = extractvalue { i8*, i32 } %landing_pad8, 1 ; [#uses = 0 type = i32]
call void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc2)
call void @_d_eh_resume_unwind(i8* %16)
unreachable
temporariesLandingPad: ; preds = %0
%landing_pad = landingpad { i8*, i32 } personality i32 (i32, i32, i64, i8*, i8*)* @_d_eh_personality
cleanup ; [#uses = 2 type = { i8*, i32 }]
%18 = extractvalue { i8*, i32 } %landing_pad, 0 ; [#uses = 1 type = i8*]
%19 = extractvalue { i8*, i32 } %landing_pad, 1 ; [#uses = 0 type = i32]
call void @_D6toelem6Struct6__dtorMFZv(%toelem.Struct* %__slStruc2)
call void @_d_eh_resume_unwind(i8* %18)
unreachable
}Pseudo-code translation of the main statement auto __slStruc2 = Struct();
try { foo(false); }
catch // temporariesLandingPad
{
destruct __slStruc2;
throw;
}
// no throw: postinvoke
auto __slStruc3 = Struct();
int tmp4;
try { tmp4 = foo(doThrow); }
catch // temporariesLandingPad2
{
try { destruct __slStruc3; }
catch // temporariesLandingPad6
{
destruct __slStruc2;
throw;
}
// no throw: postinvoke7
destruct __slStruc2;
throw;
}
// no throw: postinvoke3
int r = __slStruc3 + tmp4;
try { destruct __slStruc3; }
catch // temporariesLandingPad9
{
destruct __slStruc2;
throw;
}
// no throw: postinvoke10
destruct __slStruc2; |
We'd like to evaluate expression NewExp::argprefix and only destruct it if an exception interrupts its evaluation.
The front-end uses a single bool variable as gate for all destructors (by replacing each dtor expression by `gate || dtor()`). This flag is set at the end of the argprefix chain of CommaExp. So when no exception occurs in argprefix, the dtors aren't invoked in the inserted finally block. Right after the finally block we have the call to NewExp's ctor, which will take care of destructing its parameters.
|
There's at least one major issue remaining - if a temporary's constructor throws, it is later destructed in a landing pad. That's because the temporary is declared by a nested |
|
For which case was "Restore toElemDtor() functionality." necessary? It seemed like the tests already looked quite good before that. |
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DMD uses a similarly ugly hack to detect this case.
|
Ah okay, thanks for the explanation! I'm merging this because it is clearly better than what we currently have, and to make collaboration easier. |
WIP: Codegen for NewExp::argprefix.
|
FYI - compile times for std/uni.d unittest soared after this commit. It went from 8 seconds to 4 minutes for x86_64 OS X and 8 seconds to 20 minutes for ARM. |
|
@smolt Wow, that's extreme. I haven't noticed any significant slow-down on Linux x64, but haven't measured the compile time for |
This is an attempt to fix
runnable/sdtor.d:3438.Reduced test case:
Without this patch, the first two argument expressions
S()andfoo(flag)forC::this()cannot be resolved to the variables (__pfxand__pfy) declared for them in DMD'sfunctionParameters(). Their declaration expressions (and dtor gates) form aCommaExpchain represented byNewExp::argprefixwhich is currently not codegen'd at all.This patch replaces the first arg subexpression of any NewExp with
argprefix, arg0to include argprefix during codegen. It works for above test case if flag is set to false and no exception is thrown. The two variables are allocated on the caller's stack; S instances are then passed in memory (byval) to C's ctor, and C's ctor then destructs the copies.The problem is that if an exception occurs when evaluating argprefix, we need to destruct all until then constructed objects at the call site, as C's ctor isn't invoked.