Skip to content

IOperation support for interpolated string handler conversions #54718

@333fred

Description

@333fred

Background and Motivation

C# 10 adds interpolated string handler conversions. These conversions have a number of new components that don't map to existing interpolated string nodes, so we need to add public API support for them.

Proposed API

namespace Microsoft.CodeAnalysis.Operations
{
    public enum OperationKind
    {
+       /// <summary>Indicates an <see cref="IInterpolatedStringHandlerCreationOperation"/>.</summary>
+       InterpolatedStringHandler = 0x72,
+       /// <summary>Indicates an <see cref="IInterpolatedStringAdditionOperation"/>.</summary>
+       InterpolatedStringAddition = 0x73,
+       /// <summary>Indicates an <see cref="IInterpolatedStringAppendOperation"/>. </summary>
+       InterpolatedStringAppendLiteral = 0x74,
+       /// <summary>Indicates an <see cref="IInterpolatedStringAppendOperation"/>. This append is of an interpolation component</summary>
+       InterpolatedStringAppendFormatted = 0x75,
+       /// <summary>Indicates an <see cref="IInterpolatedStringHandlerArgumentPlaceholderOperation"/>.</summary>
+       InterpolatedStringHandlerArgumentPlaceholder = 0x76,
    }

+   /// <summary>
+   /// Represents an interpolated string converted to a custom interpolated string handler type.
+   /// </summary>
+   public interface IInterpolatedStringHandlerCreationOperation : IOperation
+   {
+       /// <summary>
+       /// The construction of the interpolated string handler instance. This can be an <see cref="IObjectCreationOperation" /> for valid code, and
+       /// <see cref="IDynamicObjectCreationOperation" /> or <see cref="IInvalidOperation" /> for invalid code.
+       /// </summary>
+       IOperation HandlerCreation { get; }
+       /// <summary>
+       /// True if the last parameter of <see cref="HandlerCreation" /> is an out <see langword="bool" /> parameter that will be checked before executing the code in
+       /// <see cref="Content" />. False otherwise.
+       /// </summary>
+       bool HandlerCreationHasSuccessParameter { get; }
+       /// <summary>
+       /// True if the AppendLiteral or AppendFormatted calls in <see cref="Parts" /> return <see langword="bool" />. When that is true, each part will be conditional
+       /// on the return of the part before it, only being executed when the Append call returns true. False otherwise.
+       /// </summary>
+       /// <remarks>
+       /// when this is true and <see cref="HandlerCreationHasSuccessParameter" /> is true, then the first part in <see cref="Parts" /> is conditionally run. If this is
+       /// true and <see cref="HandlerCreationHasSuccessParameter" /> is false, then the first part is unconditionally run.
+       /// <br />
+       /// Just because this is true or false does not guarantee that all Append calls actually do return boolean values, as there could be dynamic calls or errors.
+       /// It only governs what the compiler was expecting, based on the first calls it did see.
+       /// </remarks>
+       bool HandlerAppendCallsReturnBool { get; }
+       /// <summary>
+       /// The interpolated string expression or addition operation that makes up the content of this string. This is either an <see cref="IInterpolatedStringOperation" />
+       /// or an <see cref="IInterpolatedStringAdditionOperation" /> operation.
+       /// </summary>
+       IOperation Content { get; }
+   }
+   /// <summary>
+   /// Represents an addition of multiple interpolated string literals being converted to an interpolated string handler type.
+   /// </summary>
+   public interface IInterpolatedStringAdditionOperation : IOperation
+   {
+       /// <summary>
+       /// The interpolated string expression or addition operation on the left side of the operator. This is either an <see cref="IInterpolatedStringOperation" />
+       /// or an <see cref="IInterpolatedStringAdditionOperation" /> operation.
+       /// </summary>
+       IOperation Left { get; }
+       /// <summary>
+       /// The interpolated string expression or addition operation on the right side of the operator. This is either an <see cref="IInterpolatedStringOperation" />
+       /// or an <see cref="IInterpolatedStringAdditionOperation" /> operation.
+       /// </summary>
+       IOperation Right { get; }
+   }
+   /// <summary>
+   /// Represents a call to either AppendLiteral or AppendFormatted as part of an interpolated string handler conversion.
+   /// </summary>
+   public interface IInterpolatedStringAppendOperation : IInterpolatedStringContentOperation
+   {
+       /// <summary>
+       /// If this interpolated string is subject to an interpolated string handler conversion, the construction of the interpolated string handler instance.
+       /// This can be an IInvocationOperation or IDynamicInvocationOperation for valid code, and IInvalidOperation for invalid code.
+       /// </summary>
+       IOperation? AppendCall { get; }
+   }
+   /// <summary>
+   /// Represents an argument from the method call, indexer access, or constructor invocation that is creating the containing <see cref="IInterpolatedStringHandlerCreationOperation" />
+   /// </summary>
+   public interface IInterpolatedStringHandlerArgumentPlaceholderOperation : IOperation
+   {
+       /// <summary>
+       /// The index of the argument of the method call containing the interpolated string handler conversion this placeholder is referencing. -1 if
+       /// <see cref="IsContainingMethodReceiver" /> or if the argument was not resolved.
+       /// </summary>
+       int ArgumentIndex { get; }
+       /// <summary>
+       /// True if this placeholder represents the instance receiver of the method call containing the interpolated string handler conversion.
+       /// </summary>
+       /// <remarks>
+       /// Extension method receivers are not treated as receivers here: they have an <see cref="ArgumentIndex" /> of 0.
+       /// </remarks>
+       bool IsContainingMethodReceiver { get; }
+   }

    public enum InstanceReferenceKind
    {
+       /// <summary>
+       /// Reference to the interpolated string handler instance created as part of a parent interpolated string handler conversion.
+       /// </summary>
+       InterpolatedStringHandler
    }
}

Alternative Designs

We could potentially introduce an IPlaceholderOperation instead of IInterpolatedStringHandlerArgumentPlaceholder, with an additional PlaceholderKind enum to
differentiate the variants. However, that a) doesn't follow existing patterns, such as IInstanceReferenceOperation, and b) I don't believe that it's actually useful
to operate on all placeholders regardless of what type of placeholder it is.

I debated for a while on whether we should have HandlerAppendCallsReturnBool and HandlerCreationHasSuccessParameter. The latter can be figured out by examining the
call by hand, and the former could just be a single boolean that's true if there's any conditional evaluation of the holes. However, after implementing the BoundTree
I found HandlerCreationHasSuccessParameter to be very valuable, and the single bool version is lossy. For full fidelity of the runtime semantics here, we do need both.

We could potentially add a flag to IInterpolatedStringAppendOperation to say whether it's AppendFormatted or AppendLiteral, but I think that's fine to grab the call
and examine the type symbol for.

Examples of IOperation Trees

CustomHandler c = /*<bind>*/$"Hello world"/*</bind>*/;
TypeInfo
    ConvertedType: CustomHandler
    Type: null

IInterpolatedStringHandlerOperation (HandlerCreationHasSuccessParameter: true, HandlerAppendCallsReturnBool: true) (IsImplicit, Type: CustomHandler)
    BuilderCreation - IOperation
        IObjectCreationOperation
    Content
        IInterpolatedStringOperation (Explicit, Type: null)
            Parts
                IInterpolatedStringAppendOperation
                    IsLiteral True
                    AppendCall
                        IInvocationOperation
                            Receiver
                                IInstanceReferenceOperation (InstanceReferenceKind.InterpolatedStringHandler)
                            Arguments
                                ILiteralOperation "Hello world"
CustomHandler c = /*<bind>*/$"Hello" + $" " + $"world"/*</bind>*/;
TypeInfo
    ConvertedType: CustomHandler
    Type: null

IInterpolatedStringHandlerCreationOperation (HandlerCreationHasSuccessParameter: true, HandlerAppendCallsReturnBool: true) (IsImplicit, Type: CustomHandler)
    BuilderCreation:
        IObjectCreationOperation
    Content:
        IInterpolatedStringAdditionOperation (Type: null)
            Left
                InterpolatedStringAdditionOperation (Type: null)
                    Left:
                        IInterpolatedStringOperation (Type: null)
                            Parts
                                IInterpolatedStringAppendOperation
                                    IsLiteral True
                                    AppendCall
                                        IInvocationOperation
                                            Receiver
                                                IInstanceReferenceOperation (InstanceReferenceKind.InterpolatedStringHandler)
                                            Arguments
                                                ILiteralOperation "Hello"
                    Right:
                        IInterpolatedStringOperation (Type: null)
                            Parts
                                IInterpolatedStringAppendOperation
                                    IsLiteral True
                                    AppendCall
                                        IInvocationOperation
                                            Receiver
                                                IInstanceReferenceOperation (InstanceReferenceKind.InterpolatedStringHandler)
                                            Arguments
                                                ILiteralOperation " "
            Right
                IInterpolatedStringOperation (Type: null)
                    Parts
                        IInterpolatedStringAppendOperation
                            IsLiteral True
                            AppendCall
                                IInvocationOperation
                                    Receiver
                                        IInstanceReferenceOperation (InstanceReferenceKind.InterpolatedStringHandler)
                                    Arguments
                                        ILiteralOperation "world"

Followup Questions

In our original design, we did not approve a way to distinguish between placeholders for the receiver of the containing method, and the optional trailing out parameter placeholder.

namespace Microsoft.CodeAnalysis.Operations
{
+    public enum InterpolatedStringArgumentPlaceholderKind
+    {
+        Argument,
+        ContainingMethodReceiver,
+        TrailingValidityParameter,
+    }
    public interface IInterpolatedStringHandlerArgumentPlaceholderOperation : IOperation
    {
        /// <summary>
        /// The index of the argument of the method call containing the interpolated string handler conversion this placeholder is referencing. -1 if <see cref="PlaceholderKind" /> is
        /// anything other than <see cref="InterpolatedStringArgumentPlaceholderKind.Argument" />.
        /// </summary>
        int ArgumentIndex { get; }

+        InterpolatedStringArgumentPlaceholderKind PlaceholderKind { get; }
    }
}

Metadata

Metadata

Assignees

Labels

Area-AnalyzersConcept-APIThis issue involves adding, removing, clarification, or modification of an API.Feature - Interpolated String ImprovementsInterpolated string improvementsFeature Requestapi-approvedAPI was approved in API review, it can be implementedblockingAPI needs to reviewed with priority to unblock work

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions