Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion Coder.Test/Languages/CSharpGeneratedSourceCompilesTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

Expand Down Expand Up @@ -86,4 +86,59 @@ public void ScopeMembers_Compile()
Assert.AreEqual(0, exitCode, $"C# rejected the generated source:{Environment.NewLine}{output}");
});
}

/// <summary>
/// The consumer of the generated operators, which uses each the way C# code would.
/// </summary>
/// <remarks>
/// <c>a + b</c> is what catches a binary operator written as a unary one: the declaration
/// compiles either way, and only a call site with two operands refuses it.
/// </remarks>
private const string OperatorDriver = """
public static class OperatorDriver
{
public static double Use()
{
Point a = new Point(1, 2);
Point b = new Point(3);
Point sum = a + b;
Point negated = -a;
double converted = a;
return converted + (double)sum + (double)negated;
}
}
""";

/// <summary>
/// Tests that a type's binary, unary and conversion operators compile, and that a caller can use
/// each with the operands the declaration describes.
/// </summary>
[TestMethod]
public void Operators_Compile()
{
if (ToolchainHarness.FindOnPath("--version", "dotnet") is null)
{
Assert.Inconclusive("No dotnet on the path, so nothing was compiled.");
return;
}

ClassDeclaration point = CompiledExemplar.Point();
point.Members.Add(CompiledExemplar.Plus());
point.Members.Add(CompiledExemplar.Negate());
point.Members.Add(CompiledExemplar.ToDouble("self.x"));

SourceFile file = new("Generated");
file.Members.Add(point);

ToolchainHarness.InTemporaryDirectory(directory =>
{
string source = new CSharpGenerator().Generate(file);
File.WriteAllText(Path.Combine(directory, "Generated.csproj"), Project);
File.WriteAllText(Path.Combine(directory, "Driver.cs"), OperatorDriver);
File.WriteAllText(Path.Combine(directory, "Generated.cs"), source);

(int exitCode, string output) = ToolchainHarness.Run("dotnet", "build -nologo", directory);
Assert.AreEqual(0, exitCode, $"C# rejected the generated source:{Environment.NewLine}{output}{Environment.NewLine}{source}");
});
}
}
40 changes: 37 additions & 3 deletions Coder/Languages/CSharpGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
/// A member is emitted through the same dispatch as any other node, so a nested class, a method
/// and a field each come out through the emitter for their own shape.
/// </remarks>
private void GenerateClass(ClassDeclaration classDecl, CodeBlocker code)

Check warning on line 176 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 176 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 176 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 176 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.
{
bool aliasesWereHoisted = memberAliasesHoisted;
if (!aliasesWereHoisted)
Expand Down Expand Up @@ -368,7 +368,7 @@
/// The constants on <see cref="double"/>, which every file can name without a <c>using</c>.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "double.NaN" : value > 0 ? "double.PositiveInfinity" : "double.NegativeInfinity";

Check warning on line 371 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 371 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 371 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand All @@ -376,7 +376,7 @@
/// <c>float</c> implicitly.
/// </remarks>
protected override string SpellNonFiniteSingle(float value) =>
float.IsNaN(value) ? "float.NaN" : value > 0 ? "float.PositiveInfinity" : "float.NegativeInfinity";

Check warning on line 379 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 379 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 379 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Emits a namespace and its members.
Expand Down Expand Up @@ -883,9 +883,15 @@
code.Write(SpellTypeParameters(function.TypeParameters));
code.Write("(");

bool takesOperand = TakesImplicitOperand(function, enclosingType);
if (takesOperand)
{
code.Write($"{enclosingType!.Name}{SpellTypeParameters(enclosingType.TypeParameters)} {OperandParameterName}");
}

for (int i = 0; i < function.Parameters.Count; i++)
{
if (i > 0)
if (i > 0 || takesOperand)
{
code.Write(", ");
}
Expand Down Expand Up @@ -1042,6 +1048,30 @@
}
}

/// <summary>
/// What the operand an operator acts on is called, once C# has to name it.
/// </summary>
/// <remarks>
/// <c>self</c>, as the C lowering names its receiver, so a body that reads <c>self.x</c> means the
/// same thing in both.
/// </remarks>
private const string OperandParameterName = "self";

/// <summary>
/// Determines whether a declaration's operand is one C# has to write as a parameter.
/// </summary>
/// <param name="function">The declaration being emitted.</param>
/// <param name="enclosingType">The type it belongs to, when it belongs to one.</param>
/// <returns>True when it is an operator or a conversion that belongs to a type.</returns>
/// <remarks>
/// The AST's operators are members, as C++'s and Rust's are: <c>operator+(rhs)</c> is binary, and
/// the instance it belongs to is the left-hand operand. A C# operator is static and takes every
/// operand explicitly, so written as declared a binary operator would become a unary one and a
/// unary operator or a conversion would take nothing at all (CS1019).
/// </remarks>
private static bool TakesImplicitOperand(FunctionDeclaration function, ClassDeclaration? enclosingType) =>
enclosingType is not null && function.Kind is FunctionKind.Operator or FunctionKind.ConversionOperator;

/// <summary>
/// Determines whether a declaration's <see cref="FunctionDeclaration.IsReadOnly"/> is spelled as
/// C#'s <c>readonly</c>.
Expand All @@ -1051,10 +1081,14 @@
/// <returns>True when the declaration is an instance member of a struct that claims it.</returns>
/// <remarks>
/// The claim is C++'s trailing <c>const</c>, which any member function may make. C# lets only an
/// instance member of a struct make it, so everywhere else <c>readonly</c> fails with CS0106.
/// instance member of a struct make it, so everywhere else <c>readonly</c> fails with CS0106. An
/// operator is static in C# whether or not the declaration says so, so it never can.
/// </remarks>
private static bool IsReadOnlySpelled(FunctionDeclaration function, ClassDeclaration? enclosingType) =>
function.IsReadOnly && !function.IsStatic && enclosingType?.Kind == TypeDeclarationKind.Struct;
function.IsReadOnly
&& !function.IsStatic
&& function.Kind is not (FunctionKind.Operator or FunctionKind.ConversionOperator)
&& enclosingType?.Kind == TypeDeclarationKind.Struct;

/// <summary>
/// Determines whether a class is written <c>abstract</c>.
Expand Down
Loading