diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs index 354a100a..327b18c5 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs @@ -754,6 +754,32 @@ private bool IsReadonly(CXXMethodDecl? cxxMethodDecl) return false; } + // In C, relational and logical operators yield `int`, so the frontend inserts an + // `IntegralToBoolean` cast when such an expression is consumed as a `_Bool`. The equivalent + // C# operators already yield `bool`, so the surrounding `!= 0` coercion would be both + // redundant and non-compiling and must be omitted. + private static bool IsCSharpBooleanValuedExpr(Expr expr) + { + var asWritten = GetExprAsWritten(expr, removeParens: true); + + if (asWritten is BinaryOperator binaryOperator) + { + return binaryOperator.IsRelationalOp || binaryOperator.IsEqualityOp || binaryOperator.IsLogicalOp; + } + + return (asWritten is UnaryOperator unaryOperator) && (unaryOperator.Opcode == CXUnaryOperator_LNot); + } + + // The inverse of the `IntegralToBoolean` case: a C relational or logical operator (C# `bool`) + // stored in or returned as an integer needs a `? 1 : 0` coercion. This only applies when the + // expression is emitted as a bare `bool`; a wrapping implicit cast (e.g. `BooleanToSignedIntegral`) + // already performs the conversion, so peel parens but stop at casts to avoid coercing twice. + private static bool IsBareCSharpBooleanValuedExpr(Expr expr) + { + expr = expr.IgnoreParens; + return (expr is not ImplicitCastExpr) && IsCSharpBooleanValuedExpr(expr); + } + private static bool IsStmtAsWritten(Cursor cursor, [MaybeNullWhen(false)] out T value, bool removeParens = false) where T : Stmt { diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs index ba043b4f..133c0800 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs @@ -44,7 +44,12 @@ private void VisitBinaryOperator(BinaryOperator binaryOperator) // the surrounding context requires it. var isAdditive = binaryOperator.Opcode is CXBinaryOperator_Add or CXBinaryOperator_Sub; - VisitBinaryOperatorOperand(binaryOperator.LHS, isAdditive); + // C's `&&`/`||` yield `int` and promote a `_Bool` operand to `int`, which would otherwise + // render as a redundant `? 1 : 0` coercion. C#'s logical operators take `bool` operands + // directly, so emit the underlying boolean instead. + var isLogical = binaryOperator.IsLogicalOp; + + VisitBinaryOperatorOperand(binaryOperator.LHS, isAdditive, isLogical); outputBuilder.Write(' '); outputBuilder.Write(binaryOperator.OpcodeStr); outputBuilder.Write(' '); @@ -99,13 +104,13 @@ private void VisitBinaryOperator(BinaryOperator binaryOperator) } else { - VisitBinaryOperatorOperand(binaryOperator.RHS, isAdditive); + VisitBinaryOperatorOperand(binaryOperator.RHS, isAdditive, isLogical); } StopCSharpCode(); } - private void VisitBinaryOperatorOperand(Expr operand, bool isAdditive) + private void VisitBinaryOperatorOperand(Expr operand, bool isAdditive, bool isLogical) { if (isAdditive && IsTypeVoidPointer(operand, operand.Type)) { @@ -113,6 +118,12 @@ private void VisitBinaryOperatorOperand(Expr operand, bool isAdditive) StopCSharpCode(); } + if (isLogical && (operand is ImplicitCastExpr cast) && (cast.CastKind is CX_CK_IntegralCast or CX_CK_BooleanToSignedIntegral) && (cast.SubExprAsWritten.Type.CanonicalType.Kind == CXType_Bool)) + { + Visit(cast.SubExprAsWritten); + return; + } + Visit(operand); } @@ -1403,6 +1414,28 @@ private void VisitIfStmt(IfStmt ifStmt) StopCSharpCode(); } + // C# has no implicit conversion from `bool` to an integer, so a C#-bool-valued expression + // consumed where an integer is required must be coerced with a conditional. This mirrors the + // frontend's `BooleanToSignedIntegral` cast for the contexts where C leaves the conversion + // implicit (e.g. a relational or logical result stored in or returned as an integer). + private void WriteBooleanAsInteger(CSharpOutputBuilder outputBuilder, Expr expr, long targetSizeOf) + { + var needsCast = targetSizeOf < 4; + + if (needsCast) + { + outputBuilder.Write("(byte)("); + } + + ParenthesizeStmt(expr); + outputBuilder.Write(" ? 1 : 0"); + + if (needsCast) + { + outputBuilder.Write(')'); + } + } + private void VisitImplicitCastExpr(ImplicitCastExpr implicitCastExpr) { var outputBuilder = StartCSharpCode(); @@ -1444,7 +1477,7 @@ private void VisitImplicitCastExpr(ImplicitCastExpr implicitCastExpr) case CX_CK_IntegralToBoolean: { - if ((subExpr is UnaryOperator unaryOperator) && (unaryOperator.Opcode == CXUnaryOperator_LNot)) + if (IsCSharpBooleanValuedExpr(subExpr)) { Visit(subExpr); } @@ -1458,21 +1491,7 @@ private void VisitImplicitCastExpr(ImplicitCastExpr implicitCastExpr) case CX_CK_BooleanToSignedIntegral: { - var needsCast = implicitCastExpr.Type.Handle.SizeOf < 4; - - if (needsCast) - { - outputBuilder.Write("(byte)("); - } - - ParenthesizeStmt(subExpr); - outputBuilder.Write(" ? 1 : 0"); - - if (needsCast) - { - outputBuilder.Write(')'); - } - + WriteBooleanAsInteger(outputBuilder, subExpr, implicitCastExpr.Type.Handle.SizeOf); break; } @@ -2370,7 +2389,16 @@ private void VisitReturnStmt(ReturnStmt returnStmt) } } - Visit(retValue); + if (IsBareCSharpBooleanValuedExpr(retValue) + && IsType(returnStmt, functionDecl.ReturnType, out var returnBuiltinType) + && returnBuiltinType.IsIntegerType && (returnBuiltinType.Kind != CXType_Bool)) + { + WriteBooleanAsInteger(outputBuilder, retValue, functionDecl.ReturnType.Handle.SizeOf); + } + else + { + Visit(retValue); + } } } else if (returnStmt.RetValue != null) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs index bd966fa4..fe6cce57 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitVarDecl.cs @@ -345,7 +345,19 @@ void ForDeclStmt(VarDecl varDecl, DeclStmt declStmt) if (varDecl.HasInit) { outputBuilder.Write(" = "); - Visit(varDecl.Init); + + var init = varDecl.Init; + + if (IsBareCSharpBooleanValuedExpr(init) + && IsType(varDecl, varDecl.Type, out var builtinType) + && builtinType.IsIntegerType && (builtinType.Kind != CXType_Bool)) + { + WriteBooleanAsInteger(outputBuilder, init, varDecl.Type.Handle.SizeOf); + } + else + { + Visit(init); + } } StopCSharpCode(); diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanCoercionToIntegerTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanCoercionToIntegerTest.CSharp.Latest.cs new file mode 100644 index 00000000..47b0f90b --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanCoercionToIntegerTest.CSharp.Latest.cs @@ -0,0 +1,26 @@ +namespace ClangSharp.Test +{ + public static partial class Methods + { + public static int ReturnFromComparison(int a, int b) + { + return (a < b) ? 1 : 0; + } + + public static int ReturnFromLogical(int a, int b) + { + return (a < b || a > b) ? 1 : 0; + } + + public static int ReturnFromNegation(int a) + { + return (a == 0) ? 1 : 0; + } + + public static void Locals(int a, int b) + { + int x = (a < b) ? 1 : 0; + int y = (a == 0) ? 1 : 0; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanLogicalOperandsTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanLogicalOperandsTest.CSharp.Latest.cs new file mode 100644 index 00000000..150efe52 --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanLogicalOperandsTest.CSharp.Latest.cs @@ -0,0 +1,25 @@ +namespace ClangSharp.Test +{ + public static partial class Methods + { + public static int LogicalAnd([NativeTypeName("_Bool")] bool a, [NativeTypeName("_Bool")] bool b) + { + return (a && b) ? 1 : 0; + } + + public static int LogicalOr([NativeTypeName("_Bool")] bool a, [NativeTypeName("_Bool")] bool b) + { + return (a || b) ? 1 : 0; + } + + public static int MixedOperands([NativeTypeName("_Bool")] bool a, int b) + { + return (a && (b < 3)) ? 1 : 0; + } + + public static void LogicalLocal([NativeTypeName("_Bool")] bool a, [NativeTypeName("_Bool")] bool b) + { + int x = (a || b) ? 1 : 0; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanReturnFromComparisonTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanReturnFromComparisonTest.CSharp.Latest.cs new file mode 100644 index 00000000..47765bae --- /dev/null +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/BooleanReturnFromComparisonTest.CSharp.Latest.cs @@ -0,0 +1,23 @@ +namespace ClangSharp.Test +{ + public static partial class Methods + { + [return: NativeTypeName("_Bool")] + public static bool FromComparison(int a, int b) + { + return a < b; + } + + [return: NativeTypeName("_Bool")] + public static bool FromLogical(int a, int b) + { + return a < b || a > b; + } + + [return: NativeTypeName("_Bool")] + public static bool FromInteger(int a) + { + return (a) != 0; + } + } +} diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs index f43e780a..c4ce7c6a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs @@ -29,6 +29,97 @@ typedef struct MyStruct { return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); } + [Test] + public Task BooleanLogicalOperandsTest() + { + // In C, `&&`/`||` yield `int` and promote a `_Bool` operand to `int`; the equivalent C# + // operators take `bool` operands directly, so the operands must be emitted as `bool` rather + // than coerced with `? 1 : 0` (see https://github.com/dotnet/ClangSharp/issues/820). + var inputContents = @"int LogicalAnd(_Bool a, _Bool b) +{ + return a && b; +} + +int LogicalOr(_Bool a, _Bool b) +{ + return a || b; +} + +int MixedOperands(_Bool a, int b) +{ + return a && (b < 3); +} + +void LogicalLocal(_Bool a, _Bool b) +{ + int x = a || b; +} +"; + + return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); + } + + [Test] + public Task BooleanCoercionToIntegerTest() + { + // The inverse of #820: in C, relational and logical operators yield `int`, so their result + // can be stored in or returned as an integer without a cast. The equivalent C# operators + // yield `bool`, which has no implicit conversion to an integer, so a `? 1 : 0` coercion must + // be inserted (see https://github.com/dotnet/ClangSharp/issues/820). + var inputContents = @" +int ReturnFromComparison(int a, int b) +{ + return a < b; +} + +int ReturnFromLogical(int a, int b) +{ + return a < b || a > b; +} + +int ReturnFromNegation(int a) +{ + return !a; +} + +void Locals(int a, int b) +{ + int x = a < b; + int y = !a; +} +"; + + return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); + } + + [Test] + public Task BooleanReturnFromComparisonTest() + { + // In C, relational and logical operators yield `int`, so returning one as `_Bool` + // inserts an `IntegralToBoolean` cast. The equivalent C# operators already yield `bool`, + // so the `!= 0` coercion must be omitted (see https://github.com/dotnet/ClangSharp/issues/820). + var inputContents = @" +#define bool _Bool + +bool FromComparison(int a, int b) +{ + return a < b; +} + +bool FromLogical(int a, int b) +{ + return a < b || a > b; +} + +bool FromInteger(int a) +{ + return a; +} +"; + + return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); + } + [Test] public Task EnumTest() {