diff --git a/Coder.Test/Languages/CSharpGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CSharpGeneratedSourceCompilesTests.cs index 405f296..9182329 100644 --- a/Coder.Test/Languages/CSharpGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/CSharpGeneratedSourceCompilesTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2026 ktsu-dev contributors +// Copyright (c) 2023-2026 ktsu-dev contributors namespace ktsu.Coder.Test.Languages; @@ -86,4 +86,59 @@ public void ScopeMembers_Compile() Assert.AreEqual(0, exitCode, $"C# rejected the generated source:{Environment.NewLine}{output}"); }); } + + /// + /// The consumer of the generated operators, which uses each the way C# code would. + /// + /// + /// a + b 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. + /// + 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; + } + } + """; + + /// + /// Tests that a type's binary, unary and conversion operators compile, and that a caller can use + /// each with the operands the declaration describes. + /// + [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}"); + }); + } } diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index 152022b..bb2a428 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -883,9 +883,15 @@ private void GenerateFunction(FunctionDeclaration function, CodeBlocker code, Cl 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(", "); } @@ -1042,6 +1048,30 @@ private void WriteFunctionModifiers(FunctionDeclaration function, CodeBlocker co } } + /// + /// What the operand an operator acts on is called, once C# has to name it. + /// + /// + /// self, as the C lowering names its receiver, so a body that reads self.x means the + /// same thing in both. + /// + private const string OperandParameterName = "self"; + + /// + /// Determines whether a declaration's operand is one C# has to write as a parameter. + /// + /// The declaration being emitted. + /// The type it belongs to, when it belongs to one. + /// True when it is an operator or a conversion that belongs to a type. + /// + /// The AST's operators are members, as C++'s and Rust's are: operator+(rhs) 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). + /// + private static bool TakesImplicitOperand(FunctionDeclaration function, ClassDeclaration? enclosingType) => + enclosingType is not null && function.Kind is FunctionKind.Operator or FunctionKind.ConversionOperator; + /// /// Determines whether a declaration's is spelled as /// C#'s readonly. @@ -1051,10 +1081,14 @@ private void WriteFunctionModifiers(FunctionDeclaration function, CodeBlocker co /// True when the declaration is an instance member of a struct that claims it. /// /// The claim is C++'s trailing const, which any member function may make. C# lets only an - /// instance member of a struct make it, so everywhere else readonly fails with CS0106. + /// instance member of a struct make it, so everywhere else readonly fails with CS0106. An + /// operator is static in C# whether or not the declaration says so, so it never can. /// 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; /// /// Determines whether a class is written abstract.