diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs index 5b3af0ed..8e8a6633 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs @@ -666,6 +666,16 @@ private void VisitFunctionDecl(FunctionDecl functionDecl) var returnType = functionDecl.ReturnType; var returnTypeName = GetRemappedTypeName(functionDecl, cxxRecordDecl, returnType, out var nativeTypeName); + var isCompoundAssignmentOperator = IsLatestCompoundAssignmentOperator(functionDecl, out var compoundAssignmentOperatorToken); + + if (isCompoundAssignmentOperator) + { + // Project onto a C# 14 user-defined compound-assignment operator; the trailing + // `return this`/`*this` is dropped when the body is emitted below. + escapedName = $"operator {compoundAssignmentOperatorToken}"; + returnTypeName = "void"; + } + if (isManualImport && !_config.WithClasses.ContainsKey(name)) { var parameters = functionDecl.Parameters; @@ -690,11 +700,11 @@ private void VisitFunctionDecl(FunctionDecl functionDecl) entryPoint = functionDecl.IsExternC ? GetCursorName(functionDecl) : functionDecl.Handle.Mangling.CString; } - var needsReturnFixup = (cxxMethodDecl is not null) && NeedsReturnFixup(cxxMethodDecl); + var needsReturnFixup = !isCompoundAssignmentOperator && (cxxMethodDecl is not null) && NeedsReturnFixup(cxxMethodDecl); var desc = new FunctionOrDelegateDesc { AccessSpecifier = accessSpecifier, - NativeTypeName = nativeTypeName, + NativeTypeName = isCompoundAssignmentOperator ? null! : nativeTypeName, EscapedName = escapedName, ParentName = parentName, EntryPoint = entryPoint, @@ -880,7 +890,17 @@ private void VisitFunctionDecl(FunctionDecl functionDecl) var currentContext = _context.AddLast((compoundStmt, null)); _outputBuilder.BeginConstructorInitializers(); - VisitStmts(compoundStmt.Body); + + if (isCompoundAssignmentOperator) + { + // Drop the trailing `return this`/`*this`; the C# operator returns void. + VisitStmts([.. compoundStmt.Body.SkipLast(1)]); + } + else + { + VisitStmts(compoundStmt.Body); + } + _outputBuilder.EndConstructorInitializers(); Debug.Assert(_context.Last == currentContext); @@ -1841,7 +1861,13 @@ private bool IsConstant(string targetTypeName, Expr initExpr) } // case CX_StmtClass_ChooseExpr: - // case CX_StmtClass_CompoundLiteralExpr: + + case CX_StmtClass_CompoundLiteralExpr: + { + var compoundLiteralExpr = (CompoundLiteralExpr)initExpr; + return IsConstant(targetTypeName, compoundLiteralExpr.Initializer); + } + // case CX_StmtClass_ConceptSpecializationExpr: // case CX_StmtClass_ConvertVectorExpr: // case CX_StmtClass_CoawaitExpr: diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs index 133c0800..99d5eb64 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs @@ -1570,6 +1570,13 @@ private void VisitImplicitValueInitExpr(ImplicitValueInitExpr implicitValueInitE StopCSharpCode(); } + private void VisitCompoundLiteralExpr(CompoundLiteralExpr compoundLiteralExpr) + { + // A C compound literal `(T){ ... }` has no distinct C# spelling; it projects as the + // object/collection initializer emitted by its inner initializer list. + Visit(compoundLiteralExpr.Initializer); + } + private void VisitInitListExpr(InitListExpr initListExpr) { var outputBuilder = StartCSharpCode(); @@ -2736,7 +2743,13 @@ private void VisitStmt(Stmt stmt) } // case CX_StmtClass_ChooseExpr: - // case CX_StmtClass_CompoundLiteralExpr: + + case CX_StmtClass_CompoundLiteralExpr: + { + VisitCompoundLiteralExpr((CompoundLiteralExpr)stmt); + break; + } + // case CX_StmtClass_ConvertVectorExpr: // case CX_StmtClass_CoawaitExpr: // case CX_StmtClass_CoyieldExpr: diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs index 330a51e0..d9d1140a 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs @@ -2311,6 +2311,69 @@ private bool TryRemapOperatorName(ref string name, FunctionDecl functionDecl) return true; } + // C++ compound-assignment operators have no friendly C# projection, so they are + // emitted under their .NET metadata names, consistent with `operator=` above. + + case "operator+=": + { + name = "op_AdditionAssignment"; + return true; + } + + case "operator-=": + { + name = "op_SubtractionAssignment"; + return true; + } + + case "operator*=": + { + name = "op_MultiplicationAssignment"; + return true; + } + + case "operator/=": + { + name = "op_DivisionAssignment"; + return true; + } + + case "operator%=": + { + name = "op_ModulusAssignment"; + return true; + } + + case "operator&=": + { + name = "op_BitwiseAndAssignment"; + return true; + } + + case "operator|=": + { + name = "op_BitwiseOrAssignment"; + return true; + } + + case "operator^=": + { + name = "op_ExclusiveOrAssignment"; + return true; + } + + case "operator<<=": + { + name = "op_LeftShiftAssignment"; + return true; + } + + case "operator>>=": + { + name = "op_RightShiftAssignment"; + return true; + } + default: { return false; @@ -2318,6 +2381,76 @@ private bool TryRemapOperatorName(ref string name, FunctionDecl functionDecl) } } + private bool IsLatestCompoundAssignmentOperator(FunctionDecl functionDecl, [NotNullWhen(true)] out string? operatorToken) + { + operatorToken = null; + + // C# 14 lets a compound-assignment operator be user-defined as an instance member with a + // void return that mutates in place. The C++ operator can project onto it only when it + // returns a pointer/reference to the declaring type and its body just returns `this`/`*this`, + // which is then dropped (see https://github.com/dotnet/ClangSharp/issues/821). + if (!_config.GenerateLatestCode || functionDecl is not CXXMethodDecl cxxMethodDecl || !cxxMethodDecl.IsInstance) + { + return false; + } + + var parent = cxxMethodDecl.Parent; + + if (parent is null) + { + return false; + } + + var name = GetCursorName(functionDecl); + + operatorToken = name switch { + "operator+=" or "operator-=" or "operator*=" or "operator/=" or "operator%=" or + "operator&=" or "operator|=" or "operator^=" or "operator<<=" or "operator>>=" => name["operator".Length..], + _ => null, + }; + + if (operatorToken is null) + { + return false; + } + + var returnType = functionDecl.ReturnType.CanonicalType; + + if (returnType is not (PointerType or ReferenceType) || (returnType.PointeeType.CanonicalType.AsCXXRecordDecl != parent)) + { + operatorToken = null; + return false; + } + + if (functionDecl.Body is not CompoundStmt compoundStmt || compoundStmt.BodyBack is not ReturnStmt returnStmt) + { + operatorToken = null; + return false; + } + + var retValue = returnStmt.RetValue; + + if (retValue is null || !ReturnsThis(retValue)) + { + operatorToken = null; + return false; + } + + return true; + + static bool ReturnsThis(Expr retValue) + { + var expr = retValue.IgnoreParenCasts; + + if (expr is UnaryOperator unaryOperator && (unaryOperator.Opcode == CXUnaryOperator_Deref)) + { + expr = unaryOperator.SubExpr.IgnoreParenCasts; + } + + return expr is CXXThisExpr; + } + } + private void UncheckStmt(string targetTypeName, Stmt stmt) { Debug.Assert(_outputBuilder is not null); diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs new file mode 100644 index 00000000..bba464c8 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs @@ -0,0 +1,25 @@ +namespace ClangSharp.Test +{ + public partial struct b3Vec3 + { + public float x; + + public float y; + + public float z; + } + + public static partial class Methods + { + [return: NativeTypeName("struct b3Vec3")] + public static b3Vec3 b3Negate([NativeTypeName("struct b3Vec3")] b3Vec3 a) + { + return new b3Vec3 + { + x = -a.x, + y = -a.y, + z = -a.z, + }; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs new file mode 100644 index 00000000..f11dc0e4 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace ClangSharp.Test +{ + public unsafe partial struct MyStruct + { + public int value; + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_AdditionAssignment(MyStruct rhs) + { + value += rhs.value; + return (MyStruct*)Unsafe.AsPointer(ref this); + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_MultiplicationAssignment(int scale) + { + value *= scale; + return (MyStruct*)Unsafe.AsPointer(ref this); + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other) + { + value /= other->value; + return other; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs new file mode 100644 index 00000000..f11dc0e4 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace ClangSharp.Test +{ + public unsafe partial struct MyStruct + { + public int value; + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_AdditionAssignment(MyStruct rhs) + { + value += rhs.value; + return (MyStruct*)Unsafe.AsPointer(ref this); + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_MultiplicationAssignment(int scale) + { + value *= scale; + return (MyStruct*)Unsafe.AsPointer(ref this); + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other) + { + value /= other->value; + return other; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs new file mode 100644 index 00000000..4c898971 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs @@ -0,0 +1,24 @@ +namespace ClangSharp.Test +{ + public unsafe partial struct MyStruct + { + public int value; + + public void operator +=(MyStruct rhs) + { + value += rhs.value; + } + + public void operator *=(int scale) + { + value *= scale; + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other) + { + value /= other->value; + return other; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs new file mode 100644 index 00000000..4c898971 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs @@ -0,0 +1,24 @@ +namespace ClangSharp.Test +{ + public unsafe partial struct MyStruct + { + public int value; + + public void operator +=(MyStruct rhs) + { + value += rhs.value; + } + + public void operator *=(int scale) + { + value *= scale; + } + + [return: NativeTypeName("MyStruct &")] + public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other) + { + value /= other->value; + return other; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml new file mode 100644 index 00000000..55fca8a3 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml @@ -0,0 +1,34 @@ + + + + + + int + + + MyStruct* + + MyStruct + + value += rhs.value; + return (MyStruct*)Unsafe.AsPointer(ref this); + + + MyStruct* + + int + + value *= scale; + return (MyStruct*)Unsafe.AsPointer(ref this); + + + MyStruct* + + MyStruct* + + value /= other->value; + return other; + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml new file mode 100644 index 00000000..55fca8a3 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml @@ -0,0 +1,34 @@ + + + + + + int + + + MyStruct* + + MyStruct + + value += rhs.value; + return (MyStruct*)Unsafe.AsPointer(ref this); + + + MyStruct* + + int + + value *= scale; + return (MyStruct*)Unsafe.AsPointer(ref this); + + + MyStruct* + + MyStruct* + + value /= other->value; + return other; + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml new file mode 100644 index 00000000..f5d26d76 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml @@ -0,0 +1,32 @@ + + + + + + int + + + void + + MyStruct + + value += rhs.value; + + + void + + int + + value *= scale; + + + MyStruct* + + MyStruct* + + value /= other->value; + return other; + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml new file mode 100644 index 00000000..f5d26d76 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml @@ -0,0 +1,32 @@ + + + + + + int + + + void + + MyStruct + + value += rhs.value; + + + void + + int + + value *= scale; + + + MyStruct* + + MyStruct* + + value /= other->value; + return other; + + + + diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs index c737ac4e..194c916a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs @@ -295,6 +295,32 @@ MyStruct MyFunction2(MyStruct lhs, MyStruct rhs) { return lhs - rhs; } +"); + + [Test] + public Task CompoundAssignmentOperatorTest() + => ValidateAsync(nameof(CompoundAssignmentOperatorTest), @"struct MyStruct +{ + int value; + + MyStruct& operator+=(MyStruct rhs) + { + value += rhs.value; + return *this; + } + + MyStruct& operator*=(int scale) + { + value *= scale; + return *this; + } + + MyStruct& operator/=(MyStruct& other) + { + value /= other.value; + return other; + } +}; "); [Test] diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs index c4ce7c6a..22efd4b6 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs @@ -120,6 +120,27 @@ bool FromInteger(int a) return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); } + [Test] + public Task CompoundLiteralExprTest() + { + // C compound literals `(T){ ... }` project as the equivalent C# object initializer + // (see https://github.com/dotnet/ClangSharp/issues/819). + var inputContents = @"struct b3Vec3 +{ + float x; + float y; + float z; +}; + +struct b3Vec3 b3Negate(struct b3Vec3 a) +{ + return (struct b3Vec3){ -a.x, -a.y, -a.z }; +} +"; + + return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); + } + [Test] public Task EnumTest() {