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
26 changes: 26 additions & 0 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(Cursor cursor, [MaybeNullWhen(false)] out T value, bool removeParens = false)
where T : Stmt
{
Expand Down
68 changes: 48 additions & 20 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ');
Expand Down Expand Up @@ -99,20 +104,26 @@ 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))
{
StartCSharpCode().Write("(byte*)");
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);
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -2370,7 +2389,16 @@ private void VisitReturnStmt(ReturnStmt returnStmt)
}
}

Visit(retValue);
if (IsBareCSharpBooleanValuedExpr(retValue)
&& IsType<BuiltinType>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BuiltinType>(varDecl, varDecl.Type, out var builtinType)
&& builtinType.IsIntegerType && (builtinType.Kind != CXType_Bool))
{
WriteBooleanAsInteger(outputBuilder, init, varDecl.Type.Handle.SizeOf);
}
else
{
Visit(init);
}
}

StopCSharpCode();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
91 changes: 91 additions & 0 deletions tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading