Skip to content
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

Out as return only #64318

Merged
merged 28 commits into from
Sep 29, 2022
Merged

Out as return only #64318

merged 28 commits into from
Sep 29, 2022

Conversation

jaredpar
Copy link
Member

@jaredpar jaredpar commented Sep 27, 2022

Implementation side of dotnet/csharplang#6483

This unifies out parameters with return values with respect to ref safety. A value that can be returned through one can be returned through the other.

closes #62094
closes #64155
closes #64365

Note this does not implement having [UnscopedRef] ref on a parameter widen the ref-safe-to-escape. That will be handled as a follow up change. That is strictly non-breaking, hence lower risk and can be considered in ask mode or post RTM.

@jaredpar jaredpar changed the base branch from main to release/dev17.4 September 27, 2022 15:48
@RikkiGibson RikkiGibson self-assigned this Sep 27, 2022
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
if (parameter?.EffectiveScope == DeclarationScope.ValueScoped)
foreach (var (parameter, argument, refKind) in escapeArguments)
{
// This means it's part of an __arglist or function pointer receiver.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused as to why function pointer receiver is a scenario here. Isn't the receiver just the function pointer itself? It seems like nothing can escape into it or out of it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was attempting to maintain the original code here. I'm also unsure why a function pointer could come into play here. Not sure you can really do those on a ref struct. I will try removing and see what happens.

src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
// (12,79): error CS9075: Cannot return a parameter by reference 'y' because it is scoped to the current method
// static void F51(scoped ref R x, scoped ref R y) { F1(ref x, __arglist(ref y)); } // 6
Diagnostic(ErrorCode.ERR_RefReturnScopedParameter, "y").WithArguments("y").WithLocation(12, 79));
comp.VerifyEmitDiagnostics();
Copy link
Contributor

Choose a reason for hiding this comment

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

We should have errors here, at least when we call F1 and the argument for parameter ref R a is scoped ref. This is because we could assign __refvalue(...) = M(ref a) in the implementation of F1 and ref a will escape.

Copy link
Member Author

Choose a reason for hiding this comment

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

This change updates ref passed to arglist as not being being treated like any other ref parameter. So they can escape to caller but not in a cyclic fashion.

}";
var comp = CreateCompilation(source, runtimeFeature: RuntimeFlag.ByRefFields);
comp.VerifyEmitDiagnostics(
// (9,59): error CS8374: Cannot ref-assign 'tValue' to 'F' because 'tValue' has a narrower escape scope than 'F'.
// static void AssignValueToValue<T>(S<T> s, T tValue) { s.F = ref tValue; } // 1
Diagnostic(ErrorCode.ERR_RefAssignNarrower, "s.F = ref tValue").WithArguments("F", "tValue").WithLocation(9, 59),
// (10,59): error CS9079: Cannot ref-assign 'tRef' to 'F' because 'tRef' can only escape the current method through a return statement.
// static void AssignRefToValue<T>(S<T> s, ref T tRef) { s.F = ref tRef; } // 14
Copy link
Member

Choose a reason for hiding this comment

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

// 14

Is this correct?

Copy link
Contributor

@RikkiGibson RikkiGibson Sep 28, 2022

Choose a reason for hiding this comment

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

STE of S<T> s is calling method and RSTE of tRef is return only. so it makes sense that you can't escape tRef by-ref into s.F.

fwiw, I don't think there's any reason to do this assignment in real world code.

@cston
Copy link
Member

cston commented Sep 28, 2022

static void AssignRefToValue<T>(S<T> s, ref T tRef) { s.F = ref tRef; }

Consider adding // 2, etc.


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs:6057 in dd92bf2. [](commit_id = dd92bf2, deletion_comment = False)

}";
var comp = CreateCompilation(source, runtimeFeature: RuntimeFlag.ByRefFields);
comp.VerifyEmitDiagnostics(
// (9,59): error CS8374: Cannot ref-assign 'tValue' to 'F' because 'tValue' has a narrower escape scope than 'F'.
// static void AssignValueToValue<T>(S<T> s, T tValue) { s.F = ref tValue; } // 1
Diagnostic(ErrorCode.ERR_RefAssignNarrower, "s.F = ref tValue").WithArguments("F", "tValue").WithLocation(9, 59),
// (10,59): error CS9079: Cannot ref-assign 'tRef' to 'F' because 'tRef' can only escape the current method through a return statement.
// static void AssignRefToValue<T>(S<T> s, ref T tRef) { s.F = ref tRef; } // 14
Copy link
Contributor

@RikkiGibson RikkiGibson Sep 28, 2022

Choose a reason for hiding this comment

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

STE of S<T> s is calling method and RSTE of tRef is return only. so it makes sense that you can't escape tRef by-ref into s.F.

fwiw, I don't think there's any reason to do this assignment in real world code.

@jaredpar jaredpar added the servicing-consider .NET Core Tactics bug label Sep 29, 2022
@@ -11312,7 +11315,10 @@ static void Main()
}";

var comp = CreateCompilation(new[] { code, InterpolatedStringHandlerAttribute, InterpolatedStringHandlerArgumentAttribute }, targetFramework: TargetFramework.NetCoreApp);
comp.VerifyDiagnostics();
comp.VerifyDiagnostics(
// (5,104): error CS8352: Cannot use variable 'out CustomHandler' in this context because it may expose referenced variables outside of their declaration scope
Copy link
Contributor

Choose a reason for hiding this comment

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

would this scenario work if [UnscopedRef] were applied to the method?

Copy link
Member Author

@jaredpar jaredpar Sep 29, 2022

Choose a reason for hiding this comment

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

No. The [UnscopedRef] would only apply to the ref portion of this. That doesn't come into play here though as the violation is the following:

s.Handler = this;

In this case s has a STE of containing method and this has STE of return only as we model this as an out.

Also note that we specifically disallow [UnscopedRef] on a struct constructor because it would create some really awkward interactions. At that point the STE and RSTE would be equivalent and the ctor could capture refs to itself. So cyclic self assignment in a ctor. I think we could now model that correctly but have it disallowed because it creates really awkward scenarios and I can't see a scenario in which it would be justified. If one was provided I'd be interested in allowing but it won't help this particular issue.

Copy link
Contributor

@RikkiGibson RikkiGibson Sep 29, 2022

Choose a reason for hiding this comment

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

Ah, of course. STE of this is not customizable and neither is STE of a scoped ref parameter. Thanks!

Though I guess if it were possible to say ref scoped S, you would be able to value-assign this to it. I wonder if that fact is related to why ref scoped was cut? I wasn't following as closely when that happened.

Copy link
Member Author

@jaredpar jaredpar Sep 29, 2022

Choose a reason for hiding this comment

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

I wonder if that fact is related to why ref scoped was cut? I wasn't following as closely when that happened.

The initial design only had essentially two scopes: calling method and containing method. A parameter which was ref scoped had it's STS set to containing method. That decision though was wrong and it allowed for easy exploitation as follows:

void M(ref scoped Span<int> parameter)
{
  Span<int> local = stackalloc int[42];
  parameter = local; // uh oh 
}

This works because the STS of both values was containing method so the assignment just falls out. At the time we realized the mistake here we still were also looking at other unintended design issues we'd created and cut this on a focus of

Get back to the original intent of just allowing ref fields in current use with minimal additions to language

Now that we've gotten comfortable representing multiple scopes in the design it is possible to model ref scoped. For example it could be done effectively as:

Span<int> M(Span<int> p1, ref scoped Span<int> p2, scoped Span<int> p3)
// Becomes
'a Span<int> M('a Span<inT>, 'b ref 'c Span<int> p2, 'd Span<int> p3) 
  lifetime 'a >= 'b
  lifetime 'c >= 'd

The relationship between 'd and 'c means that you can assign these values to scoped locals but not the other way around. There is very deliberately no relationship because that requires thinking through a bit more.

This also introduces the other problem that we have to deal with: variance. Essentially we have to make sure that in the above design 'b ref 'c Span<int> cannot be assigned to 'b ref 'd Span<int> as that just recreates the original problem. Yet 'c Span<int> can be assigned to 'd Span<int>. That is just assigning a Span<int> with some value to scoped Span<int> which is always safe.

Copy link
Contributor

@RikkiGibson RikkiGibson Sep 29, 2022

Choose a reason for hiding this comment

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

right..with a 2-scope solution, where STE of ref scoped parameters is current method, it's sort of unsafe by construction. The caller is essentially saying to the callee, "somehow, I am passing you references to your own local variables".

@jaredpar jaredpar enabled auto-merge (squash) September 29, 2022 21:47
@jaredpar
Copy link
Member Author

Unix failure is known Helix issue with Unix queue. Merging

@jaredpar jaredpar merged commit e479688 into dotnet:release/dev17.4 Sep 29, 2022
@jaredpar jaredpar deleted the rf2 branch September 29, 2022 22:39
jaredpar added a commit to jaredpar/runtime that referenced this pull request Sep 30, 2022
dotnet/roslyn#64318

This change allows anything returnable from a method to be assigned to
an `out` parameter. In several places had to add `scoped` to `ref` to
inform compiler they could not be captured in an `out` parameter.
jaredpar added a commit to jaredpar/runtime that referenced this pull request Sep 30, 2022
dotnet/roslyn#64318

This change allows anything returnable from a method to be assigned to
an `out` parameter. In several places had to add `scoped` to `ref` to
inform compiler they could not be captured in an `out` parameter.
jaredpar added a commit to dotnet/runtime that referenced this pull request Oct 3, 2022
* Patches for scoped locals

dotnet/roslyn#64093

This change enforced that `scoped` on a local set the escape scope to
the current block where previously it was incorrectly setting to the
containing method.

* Make return and out equivalent for ref safety

dotnet/roslyn#64318

This change allows anything returnable from a method to be assigned to
an `out` parameter. In several places had to add `scoped` to `ref` to
inform compiler they could not be captured in an `out` parameter.

* Warnings on managed pointer types

dotnet/roslyn#64294

Compiler now issues warnings for pointer operations involving managed
types

* Update compiler version

* PR feedback

* Ref safety rules attribute

Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com>
carlossanlop pushed a commit to dotnet/runtime that referenced this pull request Oct 4, 2022
* Patches for scoped locals

dotnet/roslyn#64093

This change enforced that `scoped` on a local set the escape scope to
the current block where previously it was incorrectly setting to the
containing method.

* Make return and out equivalent for ref safety

dotnet/roslyn#64318

This change allows anything returnable from a method to be assigned to
an `out` parameter. In several places had to add `scoped` to `ref` to
inform compiler they could not be captured in an `out` parameter.

* Warnings on managed pointer types

dotnet/roslyn#64294

Compiler now issues warnings for pointer operations involving managed
types

* Update compiler version

* Fixup

* Ref safety rules attribute

Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants