In this workshop, we'll add a new in expression to C#, with example use cases shown below:
x in 1..10 // x >= 1 && x < 10
'a' in "bar" // "bar".IndexOf('a') >= 0 -or- "bar".Contains('a')
x in xs // xs.Contains(x)- Visual Studio 2022
- GitHub account
- Local installation of git (check
git --version) - .NET 9.0 (check
dotnet --version) - ILSpy
- Fork github.com/dotnet/roslyn to your own account
- Clone your fork using
git clone https://github.com/user/roslyn - Open the working folder using
cd roslyn - Check out a stable branch using
git checkout Visual-Studio-2022-Version-17.14.16 - Create a new branch using
git checkout -b InExpression
- Run
type globals.jsonand check the versions in: a.sdk/versionb.tools/dotnetc.tools/vs/version - Check .NET version using
dotnet --version - Check Visual Studio 2022 version using Help, About.
- Edit
global.jsonaccordingly, e.g.:
{
"sdk": {
"version": "9.0.305",
"allowPrerelease": false,
"rollForward": "patch"
},
"tools": {
"dotnet": "9.0.305",
"vs": {
"version": "17.8.0"
}
},
"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "9.0.0-beta.25380.1",
"Microsoft.DotNet.Helix.Sdk": "9.0.0-beta.25380.1",
"Microsoft.Build.Traversal": "3.4.0"
}
}- Restore dependencies using
restore.cmd - Build Roslyn using
build.cmd
- Open
Compilers.slnf - Unload every project except for
Compilers/Core/Microsoft.CodeAnalysisCompilers/CSharp/Microsoft.CodeAnalysis.CSharpCompilers/CSharp/cscDependencies/Microsoft.CodeAnalysis.CollectionsDependencies/Microsoft.CodeAnalysis.PooledObjects
- Save the solution
- Build the solution using CTRL-SHIFT-B
The result of step 3 should be a Compilers.slnf file containing:
{
"solution": {
"path": "Roslyn.sln",
"projects": [
"src\\Compilers\\CSharp\\Portable\\Microsoft.CodeAnalysis.CSharp.csproj",
"src\\Compilers\\CSharp\\csc\\AnyCpu\\csc.csproj",
"src\\Compilers\\Core\\Portable\\Microsoft.CodeAnalysis.csproj",
"src\\Dependencies\\Collections\\Microsoft.CodeAnalysis.Collections.shproj",
"src\\Dependencies\\PooledObjects\\Microsoft.CodeAnalysis.PooledObjects.shproj"
]
}
}-
mkdir c:\temp\InExpression -
cd c:\temp\InExpression -
Create a new
test.csfileusing System; bool b = 5 in 0..10; Console.WriteLine(b);
-
Copy prerequisite files from the .NET SDK for simplicity (see later)
copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\mscorlib.dll" . copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\netstandard.dll" . copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\System.Console.dll" . copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\System.dll" . copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\System.Runtime.dll" . copy /Y "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\9.0.9\ref\net9.0\System.Collections.dll" .
-
Right-click on the
cscproject and selectSet as Startup Project -
Right-click on the
cscproject and selectProperties -
Under
Debug,General, clickOpen debug launch profiles UI -
Specify the following for
Command line arguments/noconfig /fullpaths /nostdlib+ /reference:mscorlib.dll /reference:netstandard.dll /reference:System.Console.dll /reference:System.dll /reference:System.Runtime.dll /reference:System.Collections.dll /out:test.dll /optimize- test.cs -
Set
Working directorytoC:\temp\InExpression -
Close the dialog
-
Press CTRL-F5 to build and run
-
The output of
cscshould produce errors becauseinis not valid (yet)Microsoft (R) Visual C# Compiler version 4.14.0-dev (<developer build>) Copyright (C) Microsoft Corporation. All rights reserved. C:\temp\InExpression\test.cs(3,12): error CS1003: Syntax error, ',' expected C:\Program Files\dotnet\dotnet.exe (process 12020) exited with code 1.
-
Open the
src\Compilers\CSharp\Portable\Syntax\Syntax.xmlfile. -
Search for
BinaryExpressionSyntax:<Node Name="BinaryExpressionSyntax" Base="ExpressionSyntax">
and add a new
Kindvalue:<Kind Name="InExpression"/>
-
Search for the
OperatorTokenelement underneath theNodeforBinaryExpressionSyntax:<Field Name="OperatorToken" Type="SyntaxToken">
and add a new
Kindvalue:<Kind Name="InKeyword"/>
-
Compile the project using CTRL-SHIFT-B. A few errors will occur:
Error CS0117 'SyntaxKind' does not contain a definition for 'InExpression' Microsoft.CodeAnalysis.CSharp src\Compilers\CSharp\Portable\CSharpSyntaxGenerator\CSharpSyntaxGenerator.SourceGenerator\Syntax.xml.Internal.Generated.cs -
Open the
src\Compilers\CSharp\Portable\Syntax\SyntaxKind.csfile. -
Search for
UnsignedRightShiftExpression:UnsignedRightShiftExpression = 8692,
and add a new enum value for
InExpression:InExpression = 8693,
-
A red squiggle will occur underneath
InExpressionwith error:Error RS0016 Symbol 'InExpression' is not part of the declared API -
Fix the error by using CTRL+. to invoke the
Add InExpression to public API file PublicAPI.Unshipped.txt:Microsoft.CodeAnalysis.CSharp.SyntaxKind.InExpression = 8693 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind -
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. The same errors still occur:
C:\temp\InExpression\test.cs(3,12): error CS1003: Syntax error, ',' expected
-
Open the
src\Compilers\CSharp\Portable\Syntax\SyntaxKindFacts.csfile. -
Search for
GetBinaryExpression:
public static SyntaxKind GetBinaryExpression(SyntaxKind token)and add a case to the switch (token) statement:
case SyntaxKind.InKeyword:
return SyntaxKind.InExpression;-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we cause an assert in
LanguageParser.cs:
throw ExceptionUtilities.UnexpectedValue(op);with stack trace:
Microsoft.CodeAnalysis.dll!Roslyn.Utilities.ExceptionUtilities.UnexpectedValue(object o)
Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.Syntax.InternalSyntax.LanguageParser.GetPrecedence(Microsoft.CodeAnalysis.CSharp.SyntaxKind op)
...
-
Open the
src\Compilers\CSharp\Portable\Parser\LanguageParser.csfile. -
Search for
GetPrecedence:private static Precedence GetPrecedence(SyntaxKind op)
-
Search for
Precedence.Relational:case SyntaxKind.LessThanExpression: case SyntaxKind.LessThanOrEqualExpression: case SyntaxKind.GreaterThanExpression: case SyntaxKind.GreaterThanOrEqualExpression: case SyntaxKind.IsExpression: case SyntaxKind.AsExpression: case SyntaxKind.IsPatternExpression: return Precedence.Relational;
and add a case for
InExpression:... case SyntaxKind.IsPatternExpression: case SyntaxKind.InExpression: return Precedence.Relational;
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we cause an assert in
Binder_Expression.cs:Debug.Assert(false, "Unexpected SyntaxKind " + node.Kind());
in:
Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.Binder.BindExpressionInternal(Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax node, Microsoft.CodeAnalysis.CSharp.BindingDiagnosticBag diagnostics, bool invoked, bool indexed)
-
Open the
src\Compilers\CSharp\Portable\BoundTree\BoundNodes.xmlfile. -
Search for
BoundIsOperator:<Node Name="BoundIsOperator" Base="BoundExpression"> ... </Node>
-
We will insert a new
Nodeelement underneath:<Node Name="BoundInOperator" Base="BoundExpression"> <!-- Non-null type is required for this node kind --> <Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/> <Field Name="Element" Type="BoundExpression"/> <Field Name="Source" Type="BoundExpression"/> </Node>
-
Run the following command from the root of the
roslyncloned repo:eng\generate-compiler-code.cmd -
Build the project again using CTRL-SHIFT-B.
-
Open the
src\Compilers\CSharp\Portable\Binder\Binder_Expressions.csfile. -
Locate the place where the assert happened:
default: ... Debug.Assert(false, "Unexpected SyntaxKind " + node.Kind());
and insert a case for
SyntaxKind.InExpressionabove:case SyntaxKind.InExpression: return BindInExpression((BinaryExpressionSyntax)node, diagnostics);
-
Open the
src\Compilers\CSharp\Portable\Binder\Binder_Operators.csfile. -
Add a new placeholder
BindInExpressionmethod at the bottom:private BoundExpression BindInExpression(BinaryExpressionSyntax node, BindingDiagnosticBag diagnostics) { throw new NotImplementedException(); }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we hit our
NotImplementedException.
-
Edit the
BindInExpressionto add recursion.BoundExpression element = BindRValueWithoutTargetType(node.Left, diagnostics); BoundExpression source = BindRValueWithoutTargetType(node.Right, diagnostics);
-
Add top-level matching logic for the structure and types of the operands:
if (source is BoundRangeExpression) { throw new NotImplementedException(); } else { if (source.Type is ArrayTypeSymbol { IsSZArray: true }) { throw new NotImplementedException(); } else if (source.Type.IsStringType()) { throw new NotImplementedException(); } else { throw new NotImplementedException(); } }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we should see our first case being taken.
5 in 0..10
where the right operand is a
BoundRangeExpression. -
Edit this case to add an initial implementation.
var booleanType = Compilation.GetSpecialType(SpecialType.System_Boolean); if (source is BoundRangeExpression range) { if (element.Type?.SpecialType != SpecialType.System_Int32) { throw new NotImplementedException(); } void checkRangeOperand(BoundExpression operand) { if (operand != null) { if (operand is not BoundConversion { Operand.Type.SpecialType: SpecialType.System_Int32 }) { throw new NotImplementedException(); } } } checkRangeOperand(range.LeftOperandOpt); checkRangeOperand(range.RightOperandOpt); return new BoundInOperator(node, element, source, booleanType); } ...
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we hit an assert in
AbstractFlowPass.cs:public override BoundNode DefaultVisit(BoundNode node) { Debug.Assert(false, $"Should Visit{node.Kind} be overridden in {this.GetType().Name}?"); Diagnostics.Add(ErrorCode.ERR_InternalError, node.Syntax.Location); return null; }
with a call stack that mentions
NullableWalker:Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.NullableWalker.VisitExpressionWithoutStackGuard(Microsoft.CodeAnalysis.CSharp.BoundExpression node)
-
Open the
src\Compilers\CSharp\Portable\FlowAnalysis\NullableWalker.cs file. -
Search for
VisitIsOperator:public override BoundNode? VisitIsOperator(BoundIsOperator node) { ... }
and insert the following below:
public override BoundNode VisitInOperator(BoundInOperator node) { SetNotNullResult(node); return null!; }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we hit the same assert in
AbstractFlowPass.cs:public override BoundNode DefaultVisit(BoundNode node) { Debug.Assert(false, $"Should Visit{node.Kind} be overridden in {this.GetType().Name}?"); Diagnostics.Add(ErrorCode.ERR_InternalError, node.Syntax.Location); return null; }
but with a call stack that mentions
DefiniteAssignmentPass:Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.DefiniteAssignmentPass.VisitRvalue(Microsoft.CodeAnalysis.CSharp.BoundExpression node, bool isKnownToBeAnLvalue)The right fix will be to override the default visitor behavior for
BoundInExpression.
-
Open the
src\Compilers\CSharp\Portable\FlowAnalysis\AbstractFlowPass.csfile. -
Search for
VisitIsOperator:public override BoundNode VisitIsOperator(BoundIsOperator node) { ... }
and insert the following below:
public override BoundNode VisitInOperator(BoundInOperator node) { VisitRvalue(node.Element); VisitRvalue(node.Source); return null; }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we hit an assert in
EmitExpression.cs:throw ExceptionUtilities.UnexpectedValue(expression.Kind);
with a callstack that mentions
EmitExpression:Microsoft.CodeAnalysis.CSharp.dll!Microsoft.CodeAnalysis.CSharp.CodeGen.CodeGenerator.EmitExpression(Microsoft.CodeAnalysis.CSharp.BoundExpression expression, bool used)To fix this, we will lower the
BoundInExpressionto more primitive operations.
-
Open the
src\Compilers\CSharp\Portable\Lowering\LocalRewriter\LocalRewriter_BinaryOperator.csfile. -
Add the end of the file, add an override for
VisitInOperator.public override BoundNode? VisitInOperator(BoundInOperator node) { throw new NotImplementedException(); }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time we hit our
NotImplementedException. -
Edit the implementation of
VisitInOperatorto handleBoundRangeExpression.if (node.Source is BoundRangeExpression range) { var element = VisitExpression(node.Element); BoundExpression? expr = null; if (range.LeftOperandOpt is BoundConversion { Operand: var left }) { var l = VisitExpression(left); expr = _factory.IntLessThanOrEqual(l, element); } if (range.RightOperandOpt is BoundConversion { Operand: var right }) { var r = VisitExpression(right); var check = _factory.IntLessThan(element, right); if (expr == null) { expr = check; } else { expr = _factory.LogicalAnd(expr, check); } } expr ??= _factory.Literal(true); return expr; } else { throw new NotImplementedException(); }
-
Open the
src\Compilers\CSharp\Portable\Lowering\SyntheticBoundNodeFactory.csfile. -
Search for
IntLessThan:public BoundBinaryOperator IntLessThan(BoundExpression left, BoundExpression right)
and add another factory below:
public BoundBinaryOperator IntLessThanOrEqual(BoundExpression left, BoundExpression right) { return Binary(BinaryOperatorKind.IntLessThanOrEqual, SpecialType(Microsoft.CodeAnalysis.SpecialType.System_Boolean), left, right); }
-
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5. This time it doesn't crash.
-
Open ILSpy.
-
Open the
c:\temp\InExpression\test.dllfile. -
Under
-,Program,<Main>$(string[] args)we'll find:// Program using System; private static void <Main>$(string[] args) { bool value = 0 <= 5 && 5 < 10; Console.WriteLine(value); }
-
Open the
c:\test\InExpression\test.csfile. -
Edit the contents:
using System; int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } bool b = ReadInt("x") in ReadInt("start")..ReadInt("end"); Console.WriteLine(b);
-
Re-run the Roslyn project using F5.
-
Inspect the output in ILSpy:
// Program using System; private static void <Main>$(string[] args) { bool value = ReadInt("start") <= ReadInt("x") && ReadInt("x") < ReadInt("end"); Console.WriteLine(value); static int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } }
-
Note the following problems in the generated code:
- Duplicate evaluation of
ReadInt("x"); - Potentially not evaluating
ReadInt("end")if the<=check does not pass.
- Duplicate evaluation of
-
Edit the
VisitInOperatorfrom step 7 as follows:var temps = ImmutableArray.CreateBuilder<LocalSymbol>(); var stores = ImmutableArray.CreateBuilder<BoundExpression>(); BoundLocal storeToTemp(BoundExpression expression) { var local = _factory.StoreToTemp(expression, out var store); temps.Add(local.LocalSymbol); stores.Add(store); return local; } BoundExpression? expr = null; if (node.Source is BoundRangeExpression range) { var element = storeToTemp(VisitExpression(node.Element)); if (range.LeftOperandOpt is BoundConversion { Operand: var left }) { var l = storeToTemp(VisitExpression(left)); expr = _factory.IntLessThanOrEqual(l, element); } if (range.RightOperandOpt is BoundConversion { Operand: var right }) { var r = storeToTemp(VisitExpression(right)); var check = _factory.IntLessThan(element, r); if (expr == null) { expr = check; } else { expr = _factory.LogicalAnd(expr, check); } } expr ??= _factory.Literal(true); } else { throw new NotImplementedException(); } return _factory.Sequence(temps.ToImmutableArray(), stores.ToImmutableArray(), expr);
We're adding a
storeToTemplocal function, and use it to generate temporary variables and assignments to these variables, which are put together in aSequenceat the end. -
Build the project again using CTRL-SHIFT-B.
-
Run our new build of the compiler with F5.
-
Inspect the output in ILSpy:
// Program using System; private static void <Main>$(string[] args) { int num = ReadInt("x"); int num2 = ReadInt("start"); int num3 = ReadInt("end"); bool value = num2 <= num && num < num3; Console.WriteLine(value); static int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } }
In order to support other forms of in involving strings, arrays, and collections, we like to perform lowering like this:
- With
char c;andstring str;:- Lower
c in str - to
str.IndexOf(c) >= 0
- Lower
- With
int x;andint[] xs;:- Lower
x in xs - to
Array.IndexOf(x) >= 0
- Lower
- With
E element;andS source;:- Lower
element in source - to
source.Contains(element)
- Lower
To achieve this, we want the binder to resolve the operations required to evaluate the in expression based on the type of the element and the source. To represent the Boolean expression that will be applied to the lowered form of source and element, we introduce placeholders:
expr_element in expr_sourcebecomes
BoundInOperatorSourcePlaceholder s = ...;
BoundInOperatorElementPlaceholder e = ...;
new BoundInOperator
{
Source = expr_source,
Element = expr_element,
SourcePlaceholder = s,
ElementPlaceholder = e,
Test = /* any expression using s and e */
}where the Test can be any expression that refers to the placeholders s and e, e.g. to construct a s.IndexOf(e) >= 0 expression. At the time the lowering is performed, this Test is inlined with the placeholders being substituted by the lowered element and source expressions.
-
Open the
src\Compilers\CSharp\Portable\BoundTree\BoundNodes.xmlfile. -
Search for the
BoundInOperatorand insert two placeholder types in front:<Node Name="BoundInOperatorElementPlaceholder" Base="BoundValuePlaceholderBase"> <Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/> </Node> <Node Name="BoundInOperatorSourcePlaceholder" Base="BoundValuePlaceholderBase"> <Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/> </Node>
-
Edit the
BoundInOperatorto use the new placeholders:<Node Name="BoundInOperator" Base="BoundExpression"> <!-- Non-null type is required for this node kind --> <Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/> <Field Name="Element" Type="BoundExpression"/> <Field Name="Source" Type="BoundExpression"/> <Field Name="ElementPlaceholder" Type="BoundInOperatorElementPlaceholder?" SkipInVisitor="true" Null="allow"/> <Field Name="SourcePlaceholder" Type="BoundInOperatorSourcePlaceholder?" SkipInVisitor="true" Null="allow"/> <Field Name="Test" Type="BoundExpression?" SkipInVisitor="true" Null="allow"/> </Node>
-
Run the following command from the root of the
roslyncloned repo:eng\generate-compiler-code.cmd -
Build the project again using CTRL-SHIFT-B. A few errors will occur:
Error CS0534 'BoundInOperatorSourcePlaceholder' does not implement inherited abstract member 'BoundValuePlaceholderBase.IsEquivalentToThisReference.get' Error CS0534 'BoundInOperatorElementPlaceholder' does not implement inherited abstract member 'BoundValuePlaceholderBase.IsEquivalentToThisReference.get' Error CS7036 There is no argument given that corresponds to the required formal parameter 'sourcePlaceholder' of 'BoundInOperator.BoundInOperator(SyntaxNode, BoundExpression, BoundExpression, BoundInOperatorElementPlaceholder?, BoundInOperatorSourcePlaceholder?, BoundExpression?, TypeSymbol, bool)' -
We'll fix the first two errors by opening
src\Compilers\CSharp\Portable\BoundTree\BoundExpression.csand search forBoundThisReference. Insert the following code:internal partial class BoundInOperatorSourcePlaceholder { public sealed override bool IsEquivalentToThisReference => false; } internal partial class BoundInOperatorElementPlaceholder { public sealed override bool IsEquivalentToThisReference => false; }
-
For the last error, edit
BindInExpressionin theBinder_Operators.csfile and change:if (source is BoundRangeExpression) { var booleanType = Compilation.GetSpecialType(SpecialType.System_Boolean); return new BoundInOperator(node, element, source, booleanType, hasErrors: false); }
to
if (source is BoundRangeExpression) { var booleanType = Compilation.GetSpecialType(SpecialType.System_Boolean); return new BoundInOperator(node, element, source, elementPlaceholder: null, sourcePlaceholder: null, test: null, booleanType, hasErrors: false); }
-
Build the project again using CTRL-SHIFT-B. The build should pass now.
-
Open the
src\Compilers\CSharp\Portable\Binder\Binder_Operators.csfile. -
Edit the
BindInExpressionmethod:private BoundExpression BindInExpression(BinaryExpressionSyntax node, BindingDiagnosticBag diagnostics)
-
First, change the structure of the code to handle two cases, range versus others, using placeholders for the latter:
var booleanType = Compilation.GetSpecialType(SpecialType.System_Boolean); BoundInOperatorElementPlaceholder? elementPlaceholder = null; BoundInOperatorSourcePlaceholder? sourcePlaceholder = null; BoundExpression? test = null; if (source is BoundRangeExpression range) { if (element.Type?.SpecialType != SpecialType.System_Int32) { throw new NotImplementedException(); } void checkRangeOperand(BoundExpression operand) { if (operand != null) { if (operand is not BoundConversion { Operand.Type.SpecialType: SpecialType.System_Int32 }) { throw new NotImplementedException(); } } } checkRangeOperand(range.LeftOperandOpt); checkRangeOperand(range.RightOperandOpt); } else { elementPlaceholder = new(node, element.Type); sourcePlaceholder = new(node, source.Type); if (source.Type is ArrayTypeSymbol { IsSZArray: true }) { throw new NotImplementedException(); } else if (source.Type.IsStringType()) { throw new NotImplementedException(); } else { throw new NotImplementedException(); } } return new BoundInOperator(node, element, source, elementPlaceholder, sourcePlaceholder, test, booleanType);
-
Edit the case handling arrays as follows:
var int32Type = GetSpecialType(SpecialType.System_Int32, diagnostics, node); if (source.Type is ArrayTypeSymbol { IsSZArray: true, ElementType: var elementType }) { if (!elementType.Equals(element.Type, TypeCompareKind.ConsiderEverything)) { throw new NotImplementedException(); } var arrayType = GetSpecialType(SpecialType.System_Array, diagnostics, node); test = new BoundBinaryOperator( node, BinaryOperatorKind.GreaterThanOrEqual, data: null, LookupResultKind.Viable, left: MakeInvocationExpression( node, new BoundTypeExpression(node, aliasOpt: null, arrayType), "IndexOf", [ sourcePlaceholder, elementPlaceholder ], diagnostics ), right: new BoundLiteral( node, ConstantValue.Create(0), int32Type ), booleanType ); }
-
-
Build the project again using CTRL-SHIFT-B. The build should pass.
-
Edit the
VisitInOperatorfrom step 7 by changing theelsecase:else { var element = storeToTemp(VisitExpression(node.Element)); var source = storeToTemp(VisitExpression(node.Source)); AddPlaceholderReplacement(node.ElementPlaceholder, element); AddPlaceholderReplacement(node.SourcePlaceholder, source); var test = VisitExpression(node.Test); RemovePlaceholderReplacement(node.SourcePlaceholder); RemovePlaceholderReplacement(node.ElementPlaceholder); expr = test; }
-
Add the following overrides to the class:
public override BoundNode? VisitInOperatorElementPlaceholder(BoundInOperatorElementPlaceholder node) => PlaceholderReplacement(node); public override BoundNode? VisitInOperatorSourcePlaceholder(BoundInOperatorSourcePlaceholder node) => PlaceholderReplacement(node);
-
Build the project again using CTRL-SHIFT-B.
-
Edit
c:\temp\InExpression\test.cs:bool b = ReadInt("x") in new int[] { ReadInt("x1"), ReadInt("x2") };
-
Re-run the Roslyn project using F5. A crash occurs in
LocalRewriter.cs:Debug.Assert(expr is not BoundValuePlaceholderBase, $"Placeholder kind {expr.Kind} must be handled explicitly");
-
Edit the code to handle the new placeholders:
case BoundKind.InOperatorElementPlaceholder: case BoundKind.InOperatorSourcePlaceholder: return false;
A better implementation would consider the possiblity of passing these by reference to a
Containsmethod wheninis applied to arbitrary source and element types. -
Build the project again using CTRL-SHIFT-B.
-
Re-run the Roslyn project using F5.
-
Inspect the output in ILSpy:
// Program using System; private static void <Main>$(string[] args) { int value = ReadInt("x"); bool value2 = Array.IndexOf(new int[2] { ReadInt("x1"), ReadInt("x2") }, value) >= 0; Console.WriteLine(value2); static int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } }
-
Open the
src\Compilers\CSharp\Portable\Binder\Binder_Operators.csfile. -
Edit the
BindInExpressionmethod:-
Abstract over the
IndexOfand>= 0checking logic by lifting it out of the array case.else { elementPlaceholder = new(node, leftOperand.Type); sourcePlaceholder = new(node, rightOperand.Type); var int32Type = GetSpecialType(SpecialType.System_Int32, diagnostics, node); BoundExpression makeIndexOfGreaterThanZero(BoundExpression receiver, ImmutableArray<BoundExpression> arguments) { return new BoundBinaryOperator( node, BinaryOperatorKind.GreaterThanOrEqual, data: null, LookupResultKind.Viable, left: MakeInvocationExpression( node, receiver, "IndexOf", arguments, diagnostics ), right: new BoundLiteral( node, ConstantValue.Create(0), int32Type ), booleanType ); } if (source.Type is ArrayTypeSymbol { IsSZArray: true, ElementType: var elementType }) { if (!elementType.Equals(element.Type, TypeCompareKind.ConsiderEverything)) { throw new NotImplementedException(); } var arrayType = GetSpecialType(SpecialType.System_Array, diagnostics, node); test = makeIndexOfGreaterThanZero( new BoundTypeExpression(node, aliasOpt: null, arrayType), [ sourcePlaceholder, elementPlaceholder ] ); } ...
-
Implement the
stringcase as follows:else if (source.Type.IsStringType()) { if (element.Type?.SpecialType != SpecialType.System_Char) { throw new NotImplementedException(); } test = makeIndexOfGreaterThanZero( sourcePlaceholder, [ elementPlaceholder ] ); }
-
Implement the remaining case to find a
Containsmethod:else { test = MakeInvocationExpression( node, sourcePlaceholder, "Contains", [ elementPlaceholder ], diagnostics ); }
-
-
Edit the
c:\temp\InExpression\test.csfile:using System; using System.Collections.Generic; int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } bool b0 = ReadInt("x") in ReadInt("start")..ReadInt("end"); bool b1 = ReadInt("x") in new int[] { ReadInt("x1"), ReadInt("x2") }; bool b2 = ReadInt("x") in new List<int> { ReadInt("x1"), ReadInt("x2") }; bool b3 = 'a' in "Bart";
-
Run the Roslyn project using F5.
-
Inspect the output using ILSpy:
// Program using System; using System.Collections.Generic; private static void <Main>$(string[] args) { int num = ReadInt("x"); int num2 = ReadInt("start"); int num3 = ReadInt("end"); bool flag = num2 <= num && num < num3; num3 = ReadInt("x"); bool flag2 = Array.IndexOf(new int[2] { ReadInt("x1"), ReadInt("x2") }, num3) >= 0; num3 = ReadInt("x"); bool flag3 = new List<int> { ReadInt("x1"), ReadInt("x2") }.Contains(num3); char value = 'a'; bool flag4 = "Bart".IndexOf(value) >= 0; static int ReadInt(string name) { Console.Write(name + ": "); return int.Parse(Console.ReadLine()); } }