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

Emit readonly. for constrained array access #70949

Closed
wants to merge 1 commit into from

Conversation

jjonescz
Copy link
Member

@jjonescz jjonescz commented Nov 23, 2023

Fixes #70267.

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Nov 23, 2023
@jjonescz jjonescz force-pushed the 70267-ArrayTypeMismatch-ro branch from 7f0f874 to 8b514fe Compare November 24, 2023 10:38
@jjonescz jjonescz changed the title Emit constrained array access without runtime type check Emit readonly. for constrained array access Nov 24, 2023
@jjonescz jjonescz force-pushed the 70267-ArrayTypeMismatch-ro branch from 8b514fe to cd954d0 Compare November 24, 2023 10:54
@jjonescz jjonescz marked this pull request as ready for review November 24, 2023 12:20
@jjonescz jjonescz requested a review from a team as a code owner November 24, 2023 12:20
IL_0043: constrained. ""T""
IL_0049: callvirt ""void IMoveable.this[int].set""
IL_004e: ret
IL_0006: readonly.
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit warry of generating unverifiable IL. It's necessary sometimes, particularly with newer constructs that were created after verification was frozen. That doesn't seem to be the case for this.

I'm not as familiar with the .readonly prefix and had to look it up for this review. The set of allowed instructions does not include stloc / ldloc. Wondering if that is an ommision or not. The subsequent operations we perform after ldloc seem to fall into the allowed set.

I'd prefer to avoid this if possible. This is the opcodes that we generate today for the method:

        .maxstack 8

        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldc.i4.0
        IL_0003: ldelema !!T
        IL_0008: dup
        IL_0009: constrained. !!T
        IL_000f: callvirt instance int32 I1::get_P()
        IL_0014: ldc.i4.2
        IL_0015: add
        IL_0016: constrained. !!T
        IL_001c: callvirt instance void I1::set_P(int32)
        IL_0021: nop
        IL_0022: ret

Where is the ArrayTypeMismatchException being generated today? Is it on the ldelema instruction because the the type of the array is Base but the elements are Derived?

@davidwrighton in case he has any thoughts on how to best approach this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Where is the ArrayTypeMismatchException being generated today? Is it on the ldelema instruction because the the type of the array is Base but the elements are Derived?

Yes.

The set of allowed instructions does not include stloc / ldloc

We are not emitting readonly. for stloc/ldloc in this PR, I believe, only for ldelema.

I'm a bit warry of generating unverifiable IL. It's necessary sometimes, particularly with newer constructs that were created after verification was frozen. That doesn't seem to be the case for this.

Yeah, ok, that sounds good. We can instead spill the array access in various ways described in #70267. I will look into that.

{
resultExpr = base.VisitArrayAccess(node)!;
resultExpr = arr.Update(arr.Expression, arr.Indices, inCompoundAssignmentReceiver: true, arr.Type);
Copy link
Member

Choose a reason for hiding this comment

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

Do we only set inCompoundAssignmentReceiver when the element type is a type parameter? If so then I think we should rename that property such that it captures. Based on the current name I was confused looking at the changes to EmitAddress.cs as to why more baselines didn't change to have the .readonly prefix on them.

Comment on lines +762 to +766
var previousInCompoundAssignmentReceiver = _inCompoundAssignmentReceiver;
_inCompoundAssignmentReceiver |= isRegularCompoundAssignment;
var loweredReceiver = VisitExpression(receiver);
_inCompoundAssignmentReceiver = previousInCompoundAssignmentReceiver;
return loweredReceiver;
Copy link
Member

Choose a reason for hiding this comment

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

It seems like _inCompoundAssignmentReceiver will be set for the duration of the VisitExpression call. Doesn't that mean that all array accesses within the receiver will end up with the .readonly prefix? Essentially I'm thinking about the following:

array1[(array2[0]=someValue).Index].Prop += 42;

Will the inner array2[0] get the .readonly prefix? If so that seems incorrect and would lead to array mismatch bugs.

@jjonescz jjonescz marked this pull request as draft November 28, 2023 09:19
@AlekseyTs
Copy link
Contributor

It looks like we already deal with a similar situation in context of a compound assignment to an array element without emitting an unverifiable code. See usages of SpillArrayElementAccess helper. I there really a good reason to do something different for other scenarios?

@jjonescz
Copy link
Member Author

jjonescz commented Dec 1, 2023

It looks like we already deal with a similar situation in context of a compound assignment to an array element without emitting an unverifiable code. See usages of SpillArrayElementAccess helper. I there really a good reason to do something different for other scenarios?

I'm investigating using SpillArrayElementAccess. But for example nested array access is much simpler than general indexer (since it's limited to arrays, it can emit ldelem_ref since arrays are reference types, whereas the nested indexer can be a struct).

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Dec 1, 2023

But for example nested array access is much simpler than general indexer (since it's limited to arrays, it can emit ldelem_ref since arrays are reference types, whereas the nested indexer can be a struct).

Sorry, I am not following. It looks to me that SpillArrayElementAccess usage is limited to arrays. I am also not sure what "general indexer" term refers to in this context.
In addition consider the following:

  • If I remember correctly, VB compiler successfully deals with the same problematic scenarios without violating IL verification rules.
  • I think that async spilling rewriter in C# also deals with similar situations without violating IL verification rules.

Similar situations are situations when an array element access is used as a receiver of a method call and it is getting spilled (i.e. its side- effects are evaluated at one point of time and the actual access is performed at a later time, quite possibly more than once).

@AlekseyTs
Copy link
Contributor

I would like to bring your attention to the following quote from the issue:

Other scenarios where compiler captures references to array elements, which could be reference types, could be affected as well.

I am pretty sure that affected scenarios are not limited to compound assignment scenarios, while they are the most obvious/simple examples. I would scope affected scenarios to scenarios for which LocalRewriter captures receivers of method calls in a ByRef local. Those places should be easily identifiable based on the helper used for that.

@@ -664,7 +664,9 @@ private BoundExpression TransformCompoundAssignmentLHS(BoundExpression originalL
// I t_index = index; (possibly more indices)
// T value = t_array[t_index];
// t_array[t_index] = value op R;
var loweredArray = VisitExpression(arrayAccess.Expression);

var loweredArray = VisitCompoundAssignmentReceiver(arrayAccess.Expression, isRegularCompoundAssignment);
Copy link
Contributor

Choose a reason for hiding this comment

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

var loweredArray = VisitCompoundAssignmentReceiver(arrayAccess.Expression, isRegularCompoundAssignment);

I think that this code path doesn't need any special handling. The SpillArrayElementAccess call below should already handle things properly.

{
resultExpr = base.VisitArrayAccess(node)!;
resultExpr = arr.Update(arr.Expression, arr.Indices, inCompoundAssignmentReceiver: true, arr.Type);
Copy link
Contributor

Choose a reason for hiding this comment

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

inCompoundAssignmentReceiver: true

I think that, in a general case, this assumption is incorrect. The expression that we are rewriting while _inCompoundAssignmentReceiver is set to true could be arbitrarily complex, with any number of array access expressions used in it in any number of positions that are not receiver positions. More over, the array access can be a target of an explicit ref taking operation, like right hand side of a ref assignment, or a ref argument, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify. Strictly speaking, yes, the array access is in a compound assignment receiver. However, the value of inCompoundAssignmentReceiver is used by emit layer as an indicator that using readonly. prefix is appropriate. Which is incorrect in a general case, even if we put the IL verification issue aside.

@jjonescz
Copy link
Member Author

jjonescz commented Dec 4, 2023

  • I think that async spilling rewriter in C# also deals with similar situations without violating IL verification rules.

Similar situations are situations when an array element access is used as a receiver of a method call and it is getting spilled (i.e. its side- effects are evaluated at one point of time and the actual access is performed at a later time, quite possibly more than once).

Oh, thanks, I didn't think of looking into this scenario. Seems that it's not behaving as I would expect though.

Your test GenericTypeParameterAsReceiver_Assignment_Compound_Indexer_Struct_Index_Async_01_ThroughArray has a different output than GenericTypeParameterAsReceiver_Assignment_Compound_Indexer_Class_Index_Async_01_ThroughArray. Shouldn't they have the same output? I thought the whole point in expression like array[index1][index2] += 1 is to evaluate reference to array[index1] prior to index2 so side effects of index2 would not affect it. The class scenario does that, the struct scenario doesn't.

  • If I remember correctly, VB compiler successfully deals with the same problematic scenarios without violating IL verification rules.

Yes, that's true. VB checks at runtime if T is a class or a struct (default(T) == null) via EmitComplexConditionalAccessReceiver. Seems that this exists in C# as well, I will try to use it.

Sorry, I am not following. It looks to me that SpillArrayElementAccess usage is limited to arrays. I am also not sure what "general indexer" term refers to in this context.

Today, SpillArrayElementAccess is used in nested array access scenarios like array[index1][index2] to spill the array[index1] part. But using it in indexer access with array access receiver results in wrong order of operations (index2 is evaluated prior to array[index1]).

@AlekseyTs
Copy link
Contributor

@jjonescz I think it is time we sync up offline.

@AlekseyTs
Copy link
Contributor

But using it in indexer access with array access receiver results in wrong order of operations (index2 is evaluated prior to array[index1]).

I have absolutely no idea what exactly you are doing, and, therefore, cannot comment on results that you are getting and why you are getting them.

@jjonescz jjonescz closed this Dec 7, 2023
@jjonescz jjonescz deleted the 70267-ArrayTypeMismatch-ro branch December 7, 2023 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unexpected System.ArrayTypeMismatchException at execution time
3 participants