Skip to content

Commit

Permalink
implements Record.Deconstruct() method (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianoc committed Jun 6, 2024
1 parent 170f781 commit 67edeaf
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 4 deletions.
11 changes: 10 additions & 1 deletion Cecilifier.Core.Tests/Framework/CecilifierTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,16 @@ protected CecilifyResult CecilifyAndExecute(Stream tbc, string testBasePath)
return testCompilationResult;

CopyFilesNextToGeneratedExecutable(cecilifierRunnerPath, refsToCopy);
TestFramework.Execute("dotnet", $"{cecilifierRunnerPath} {outputAssemblyPath}");

try
{
TestFramework.Execute("dotnet", $"{cecilifierRunnerPath} {outputAssemblyPath}");
}
catch (Exception)
{
Console.WriteLine($"Cecilified Code:\n{cecilifiedCode}\nCecilified Assembly: {cecilifierRunnerPath}\nCecilified Output Assembly:{outputAssemblyPath}");
throw;
}

return testCompilationResult;

Expand Down
13 changes: 11 additions & 2 deletions Cecilifier.Core.Tests/Framework/OutputBasedTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ protected OutputBasedTestResult CecilifyAndExecute(string code)
var outputBasedTestFolder = GetTestOutputBaseFolderFor("OutputBasedTests");

var cecilifyResult = CecilifyAndExecute(new MemoryStream(Encoding.ASCII.GetBytes(code)), outputBasedTestFolder);

VerifyAssembly(cecilifyResult.CecilifiedOutputAssemblyFilePath, null, new CecilifyTestOptions { CecilifiedCode = cecilifyResult.CecilifiedCode });

var refsToCopy = new List<string>
Expand All @@ -32,7 +31,17 @@ protected OutputBasedTestResult CecilifyAndExecute(string code)

CopyFilesNextToGeneratedExecutable(cecilifyResult.CecilifiedOutputAssemblyFilePath, refsToCopy);

var output = TestFramework.ExecuteWithOutput("dotnet", cecilifyResult.CecilifiedOutputAssemblyFilePath);
string output = null;
try
{
output = TestFramework.ExecuteWithOutput("dotnet", cecilifyResult.CecilifiedOutputAssemblyFilePath);
}
catch (Exception ex)

Check warning on line 39 in Cecilifier.Core.Tests/Framework/OutputBasedTestBase.cs

View workflow job for this annotation

GitHub Actions / RunTests

The variable 'ex' is declared but never used
{
Console.WriteLine($"Cecilified source:\n{cecilifyResult.CecilifiedCode}\nCecilified Output Assembly: {cecilifyResult.CecilifiedOutputAssemblyFilePath}");
throw;
}

return new OutputBasedTestResult(cecilifyResult, output.AsSpan()[..^NewLineLength].ToString()); // remove last new line
}

Expand Down
44 changes: 43 additions & 1 deletion Cecilifier.Core.Tests/Tests/OutputBased/RecordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ public void ToString_WhenInheritFromRecord_IncludesBaseRecordProperties()
public void ToString_WhenInheritFromRecord_IncludesBaseRecordProperties2()
{
AssertOutput(
"""System.Console.WriteLine(new D(42, true, "42")); record D(int Value, bool IsCool, string Str):B1(IsCool, Str); record B1(bool IsCool, string Str) : B2(Str); record B2(string Str);""",
"""
System.Console.WriteLine(new D(42, true, "42"));
record D(int Value, bool IsCool, string Str):B1(IsCool, Str);
record B1(bool IsCool, string Str) : B2(Str);
record B2(string Str);
""",
"D { Str = 42, IsCool = True, Value = 42 }");
}

Expand Down Expand Up @@ -134,6 +140,42 @@ record Derived(int Value, string Name) : Base(Name);
"True");
}
}

[TestFixture]
public class Deconstruct : OutputBasedTestBase
{
[Test]
public void Deconstruct_WhenInheritingFromObject_IncludesAllPrimaryConstructorParameters()
{
AssertOutput(
"""
var r = new Record(42, "Foo");
r.Deconstruct(out var i, out var s); // Cecilifier does not support deconstructing syntax so we just call the Deconstruct() method manually.
System.Console.WriteLine($"{i},{s}");
record Record(int Value, string Name);
""",
"42,Foo");
}

[Test]
public void Deconstruct_WhenInheritingFromRecord_IncludesAllPrimaryConstructorParameters()
{
AssertOutput(
"""
var r = new Derived2(42, "Foo", true);
r.Deconstruct(out var i, out var s, out var b); // Cecilifier does not support deconstructing syntax so we just call the Deconstruct() method manually.
System.Console.WriteLine($"{i},{s},{b}");
record Base(string Name);
record Derived(int Value, string Name) : Base(Name);
record Derived2(int Value, string Name, bool IsCool) : Derived(Value, Name);
""",
"42,Foo,True");
}
}

[Test]
public void Constructor_WhenInheritFromRecordWithProperties_CorrectArgumentsArePassed()
Expand Down
62 changes: 62 additions & 0 deletions Cecilifier.Core/CodeGeneration/Record.Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal void AddSyntheticMembers(IVisitorContext context, string recordTypeDefi
AddEqualsOverloads(context, recordTypeDefinitionVariable, record);
AddEqualityOperator(context, recordTypeDefinitionVariable, record);
AddInequalityOperator(context, recordTypeDefinitionVariable, record);
AddDeconstructMethod(context, recordTypeDefinitionVariable, record);
}

private void AddEqualityOperator(IVisitorContext context, string recordTypeDefinitionVariable, TypeDeclarationSyntax record)
Expand Down Expand Up @@ -685,6 +686,67 @@ [new ParameterSpec("other", context.TypeResolver.Resolve(recordSymbol.BaseType),
context.WriteCecilExpression($"{recordTypeDefinitionVariable}.Methods.Add({equalsBaseOverloadMethodVar});");
}

private void AddDeconstructMethod(IVisitorContext context, string recordTypeDefinitionVariable, TypeDeclarationSyntax record)
{
if (record.ParameterList?.Parameters.Count == 0)
return;

var recordSymbol = context.SemanticModel.GetDeclaredSymbol(record).EnsureNotNull<ISymbol, ITypeSymbol>();
const string methodName = "Deconstruct";

var parametersInfo = record.ParameterList!.Parameters.Select(p => (p.Identifier.ValueText, context.SemanticModel.GetTypeInfo(p.Type!).Type));
var parameterTypeParamSpec = parametersInfo.Select(parameterInfo =>
new ParameterSpec(parameterInfo.ValueText, context.TypeResolver.Resolve(parameterInfo.Type), RefKind.Out, Constants.ParameterAttributes.Out) { RegistrationTypeName = parameterInfo.Type.ToDisplayString()})
.ToArray();

context.WriteNewLine();
context.WriteComment($"Deconstruct({string.Join(',', parametersInfo.Select(parameterType => $"out {parameterType!.Type}"))})");
var deconstructMethodVar = context.Naming.SyntheticVariable(methodName, ElementKind.Method);
var deconstructMethodVarExps = CecilDefinitionsFactory.Method(
context,
recordSymbol.Name,
deconstructMethodVar,
methodName,
methodName,
Constants.Cecil.PublicInstanceMethod,
parameterTypeParamSpec,
[],
ctx => ctx.TypeResolver.Bcl.System.Void,
out _);

context.WriteCecilExpressions(deconstructMethodVarExps);

List<InstructionRepresentation> deconstructInstructions = new();
int argIndex = 1;
foreach (var p in parametersInfo)
{
deconstructInstructions.Add(OpCodes.Ldarg.WithOperand(argIndex.ToString()));
deconstructInstructions.Add(OpCodes.Ldarg_0);
deconstructInstructions.Add(OpCodes.Call.WithOperand(GetGetterMethodVar(recordSymbol, p.ValueText)));
deconstructInstructions.Add(p.Type.StindOpCodeFor());
argIndex++;
}
deconstructInstructions.Add(OpCodes.Ret);

var bodyExps = CecilDefinitionsFactory.MethodBody(context.Naming, methodName, deconstructMethodVar, [], deconstructInstructions.ToArray());
context.WriteCecilExpressions(bodyExps);
context.WriteCecilExpression($"{recordTypeDefinitionVariable}.Methods.Add({deconstructMethodVar});");

string GetGetterMethodVar(ITypeSymbol candidate, string propertyName)
{
var getterMethodVar = context.DefinitionVariables.GetMethodVariable(new MethodDefinitionVariable(candidate.Name, $"get_{propertyName}", [], 0));
if (getterMethodVar.IsValid)
return getterMethodVar.VariableName;

// getter is not defined in the declaring record; this means the primary constructor parameter
// was passed to its base type ctor, lets validate that and retrieve the getter from the base.
if (SymbolEqualityComparer.Default.Equals(recordSymbol.BaseType, context.RoslynTypeSystem.SystemObject))
throw new InvalidOperationException($"Variable for the getter method for {recordSymbol.Name}.{propertyName} could not be found.");

return GetGetterMethodVar(candidate.BaseType, propertyName);
}
}

private static bool HasBaseRecord(TypeDeclarationSyntax record)
{
if (record.BaseList?.Types.Count is 0 or null)
Expand Down
1 change: 1 addition & 0 deletions Cecilifier.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public struct Cecil
public const string DelegateMethodAttributes = $"MethodAttributes.Public | {HideBySigNewSlotVirtual}";
public const string PublicOverrideMethodAttributes = $"MethodAttributes.Public | {HideBySigVirtual}";
public const string PublicOverrideOperatorAttributes = $"MethodAttributes.Public | MethodAttributes.HideBySig | {MethodAttributesSpecialName} | {MethodAttributesStatic}";
public const string PublicInstanceMethod = $"MethodAttributes.Public | MethodAttributes.HideBySig";

public const string CtorAttributes = "MethodAttributes.RTSpecialName | MethodAttributes.SpecialName";
public const string InstanceConstructorName = "ctor";
Expand Down

0 comments on commit 67edeaf

Please sign in to comment.