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; }
}
}
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
IPlaceholderOperationinstead ofIInterpolatedStringHandlerArgumentPlaceholder, with an additionalPlaceholderKindenum todifferentiate the variants. However, that a) doesn't follow existing patterns, such as
IInstanceReferenceOperation, and b) I don't believe that it's actually usefulto operate on all placeholders regardless of what type of placeholder it is.
I debated for a while on whether we should have
HandlerAppendCallsReturnBoolandHandlerCreationHasSuccessParameter. The latter can be figured out by examining thecall 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
HandlerCreationHasSuccessParameterto 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
IInterpolatedStringAppendOperationto say whether it'sAppendFormattedorAppendLiteral, but I think that's fine to grab the calland examine the type symbol for.
Examples of IOperation Trees
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; } } }