Skip to content

Fix empty cctor#43234

Merged
RikkiGibson merged 22 commits intodotnet:masterfrom
RikkiGibson:fix-empty-cctor
Apr 28, 2020
Merged

Fix empty cctor#43234
RikkiGibson merged 22 commits intodotnet:masterfrom
RikkiGibson:fix-empty-cctor

Conversation

@RikkiGibson
Copy link
Member

@RikkiGibson RikkiGibson commented Apr 9, 2020

Closes #42985

Solves the issue by excluding the .cctor from the public members list and waiting until emit to decide whether to synthesize the constructor.

Solves the issue without changing the conditions where we include the synthesized static constructor in GetMembers(). Instead we decide selectively to not emit the synthesized static constructor if the field initializers are only assigning the default values.

  • Some room for improvement in sharing logic to decide to skip emitting a method (similar to the default struct constructor or a partial method with no implementation part).
  • We could try to skip emitting even an explicitly declared static constructor if we find that the lowered implementation is empty. I feel that this is a bit more risky/difficult to get right. We will only eliminate synthesized static constructors.

@RikkiGibson
Copy link
Member Author

This is still not ready to merge (some sloppy copy-pasting and incomplete checks around 'extern static' constructors). But I would like to open it for input. I am unsure whether to skip the static constructor based on the lowered bound tree, or whether to skip it based on the emitted body containing only IlOpCode.Ret.

  • Using the lowered bound tree has a problem where we have to do some guesswork to decide if the method body would result in an "empty" IL body. This includes stuff like filtering out sequence point nodes and filtering out unreachable nodes, i.e. if (false) DoSomething();.

  • Using the emitted body from GenerateMethodBody() has a problem of unwanted side effects such as attaching unwanted test data or pdb entries to the compilation.

/cc @AlekseyTs.

@RikkiGibson RikkiGibson marked this pull request as ready for review April 14, 2020 21:31
@RikkiGibson RikkiGibson requested a review from a team as a code owner April 14, 2020 21:31
@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 15, 2020

We should never eliminate an explicitly declared constructor. Also, for the first phase we can simply handle the scenario where bound initializers (after initial binding) produce obviously "default" values, i.e. no conditional logic, etc. #Closed

@RikkiGibson
Copy link
Member Author

I misread #42985 (comment) to mean that we could even eliminate explicitly declared .cctors as long as the implementation is empty. Eliminating only a synthesized .cctor by checking the bound initializers is not too hard. I had it working that way in one of the commits along the way in this PR. Will update the PR accordingly.

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: why can’t we eliminate static constructors when they are just setting default values? Thought for a bit and can’t see that being a compat break. Is there one I’m missing or a completely different issue at play here?

@RikkiGibson
Copy link
Member Author

The first thing that comes to mind for me is a user may expect to be able to add an empty static constructor and set a breakpoint in it just to debug something about their program’s initialization.

Eliminating the explicitly declared but empty static constructors is something we could conceivably do, but it is riskier and less compelling. We probably wouldn’t be making this change for the synthesized static constructors if not for the string field = null! idiom.

@jaredpar
Copy link
Member

The first thing that comes to mind for me is a user may expect to be able to add an empty static constructor and set a breakpoint in it just to debug something about their program’s initialization

That makes sense. If a user explicitly adds one then we should not be deleting it. I was thinking more of the cases where we implicitly generate one for them.

if (methodSymbol is SynthesizedStaticConstructor)
{
foreach (var initializer in initializers)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ [](start = 24, length = 1)

Consider extracting the loop body to a helper method and use return initializers.Any(i => HasStatements(i));.

// Although we do not emit the synthesized static constructor, the source type symbol will still appear to have one
Assert.True(HasSynthesizedStaticConstructor(typeSymbol));
Assert.True(IsBeforeFieldInit(typeSymbol));
}
Copy link
Contributor

@cston cston Apr 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} [](start = 8, length = 1)

Please test:

static S s1 = default;
static S? s2 = null;
static S? s3 = default;
static S? s4 = default(S);
static object s5 = default(S);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a Theory to keep the test compact.

@AlekseyTs
Copy link
Contributor

It looks like there are two code paths where we create an instance of SynthesizedStaticConstructor outside of BuildMembersAndInitializers. Please make sure we have tests to verify expected behavior for those code paths too.

Was this addressed?


In reply to: 615538957 [](ancestors = 615538957)

@AlekseyTs
Copy link
Contributor

public static int Count = 0;

Is the new baseline exactly the same as for the case when this field doesn't have an initializer?

There was no response to this question.


In reply to: 615535416 [](ancestors = 615535416)


Refers to: src/Compilers/CSharp/Test/Emit/PDB/PDBAsyncTests.cs:37 in bd74323. [](commit_id = bd74323, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 24, 2020

Done with review pass (iteration 15) #Closed

@RikkiGibson
Copy link
Member Author

Is the new baseline exactly the same as for the case when this field doesn't have an initializer?

Modified TestAsyncDebug to demonstrate that this is the case.

It looks like there are two code paths where we create an instance of SynthesizedStaticConstructor outside of BuildMembersAndInitializers. Please make sure we have tests to verify expected behavior for those code paths too.

Added some tests specifically to verify the .cctor that gets created for a const decimal field and for a non-capturing lambda.

@RikkiGibson
Copy link
Member Author

It also sounds like I should also start using symbolValidator to check whether or not a .cctor is present. Will make a pass to fix that when I get the chance.

var source = @"
class C
{
const decimal d1 = 0.1m;
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.1m [](start = 23, length = 4)

Let's also add a test with 0m. Will it be treated as default and eliminate the constructor? #Closed

Copy link
Member Author

@RikkiGibson RikkiGibson Apr 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that 0.0m was treated differently than 0 or default. That seems like a bug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(actually, it seems like the precision of 0.0m made it a non-default value. 0m is treated as expected.)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Apr 24, 2020

Done with review pass (iteration 17) #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (iteration 20)

@RikkiGibson
Copy link
Member Author

Could I please get a second review @dotnet/roslyn-compiler

@RikkiGibson RikkiGibson requested a review from a team April 25, 2020 17:26
Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (iteration 22)

@RikkiGibson RikkiGibson merged commit bb6fba2 into dotnet:master Apr 28, 2020
@ghost ghost added this to the Next milestone Apr 28, 2020
@RikkiGibson RikkiGibson deleted the fix-empty-cctor branch April 28, 2020 00:58
333fred added a commit to 333fred/roslyn that referenced this pull request Apr 28, 2020
…-pointers

* upstream/master: (119 commits)
  Apply FixAll for IDE0044 (Make field readonly) for newly added fields
  Fix empty cctor (dotnet#43234)
  Add tests
  Avoid using an ArrayBuilder for IntervalTree<T>.Any
  Add tests
  Add in tests
  Do not remove cast when it would break a params call.
  Add tests
  Do not remove non-identity casts in conditional expressions
  Don't remove null-casts in switch expressions
  Updte comment
  Update src/Analyzers/Core/Analyzers/UseSystemHashCode/UseSystemHashCodeDiagnosticAnalyzer.cs
  Add assert
  bad attempt
  change variable name to Add_file_header
  Update eng/build-utils.ps1
  Use consistent namespaces for integration tests
  call it a header instead of a banner
  Improve VS Code build current project support
  Add docs
  ...
@JoeRobich JoeRobich modified the milestones: Next, 16.7.P2 May 18, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler generates empty cctor for static fields initialized to null!

6 participants