From 1bc6f9d7bb66b2ff662233ec7c93bd1f312eede0 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Mon, 13 Jul 2026 23:00:19 -0700 Subject: [PATCH 1/2] Don't emit unnecessary unchecked scope for native-int literals A bare integer literal whose target type resolves to a native-sized integer (nint/nuint) was wrapped in an unchecked scope whenever its value fell outside the 32-bit range, even in expression contexts where the literal keeps its natural C# type and is never narrowed. The narrowing (nint)/(nuint) cast is only emitted for VarDecl initializers, so restrict the native-int range check to that context. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Predicates.cs | 17 +++++++++++++++++ ...inesRegressionTestUnix.CSharp.Latest.Unix.cs | 2 +- .../CTest.cs | 7 +++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs index 784738ac..f5904b1d 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs @@ -1159,6 +1159,23 @@ private bool IsUnchecked(string targetTypeName, Stmt stmt) { var integerLiteral = (IntegerLiteral)stmt; var signedValue = integerLiteral.Value; + + // A bare integer literal is emitted with its natural C# type (e.g. a value that only + // fits `ulong` gets a `U` suffix and is a `ulong` constant). For the native-sized + // integer targets (`nint`/`nuint`), the platform-dependent narrowing that could + // overflow on a 32-bit target only happens when the generator emits the explicit + // `(nint)`/`(nuint)` cast, which it only does for `VarDecl` initializers (see + // `UncheckStmt`). Outside of that context the literal keeps its natural type and + // never needs an `unchecked` scope, so evaluate against the widest equivalent. + if (!IsPrevContextDecl(out _, out _)) + { + targetTypeName = targetTypeName switch { + "nint" or "IntPtr" => "long", + "nuint" or "UIntPtr" => "ulong", + _ => targetTypeName, + }; + } + return IsUnchecked(targetTypeName, signedValue, integerLiteral.IsNegative, isHex: integerLiteral.ValueString.StartsWith("0x", StringComparison.Ordinal)); } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CLongDefinesRegressionTestUnix.CSharp.Latest.Unix.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CLongDefinesRegressionTestUnix.CSharp.Latest.Unix.cs index e1295a25..ef10a49a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CLongDefinesRegressionTestUnix.CSharp.Latest.Unix.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CLongDefinesRegressionTestUnix.CSharp.Latest.Unix.cs @@ -5,7 +5,7 @@ public static unsafe partial class Methods [return: NativeTypeName("_Bool")] public static bool SDL_size_add_check_overflow([NativeTypeName("size_t")] nuint a, [NativeTypeName("size_t")] nuint b, [NativeTypeName("size_t *")] nuint* ret) { - if (b > unchecked(18446744073709551615U) - a) + if (b > (18446744073709551615U) - a) { return (0) != 0; } diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs index 4b9bb199..f43e780a 100644 --- a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs +++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs @@ -382,7 +382,7 @@ public Task CLongDefinesTestUnix() public Task CLongDefinesRegressionTestUnix() { // This test is to catch a potential regression when changing how native integers are handled - // Specifically, (18446744073709551615U) became unchecked(18446744073709551615U) + // Specifically, (18446744073709551615U) must not be wrapped in an unnecessary unchecked scope // Macro values are taken from the Linux headers when using Clang // Some values are substituted for simplicity @@ -406,9 +406,8 @@ bool SDL_size_add_check_overflow(size_t a, size_t b, size_t *ret) } "; - // The expected below currently does not represent the ideal behavior. - // Ideally, the unchecked keywork is removed as it is unnecessary. - // This issue is tracked here: https://github.com/dotnet/ClangSharp/issues/709 + // The unnecessary unchecked scope around the `ulong` literal is now elided; see + // https://github.com/dotnet/ClangSharp/issues/709 return ValidateGeneratedCSharpLatestUnixBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard); } From 9bef050e2467dfb16f832ba399bfceb34e71591e Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 14 Jul 2026 15:14:08 -0700 Subject: [PATCH 2/2] Widen native-int target at the IsUnchecked entry for non-VarDecl contexts The literal-only check missed the constant-evaluation path (the CXEvalResult overload reached via ParenExpr/BinaryOperator .Handle.Evaluate), which still saw the un-widened nuint on an LP64 host and re-emitted the unchecked scope. Widen nint/nuint to long/ulong once at the single Stmt funnel so every recursive and constant-folded path is consistent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PInvokeGenerator.Predicates.cs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs index f5904b1d..03340794 100644 --- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs +++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.Predicates.cs @@ -946,6 +946,19 @@ private bool IsUnchecked(string targetTypeName, Stmt stmt) targetTypeName = transparentStruct.Name; } } + else + { + // The platform-dependent narrowing that could overflow on a 32-bit target only happens + // when the generator emits the explicit `(nint)`/`(nuint)` cast, which it only does for + // `VarDecl` initializers (see `UncheckStmt`). Outside of that context an expression keeps + // its natural C# type and never needs an `unchecked` scope, so evaluate native-sized + // integer targets against their widest equivalent. + targetTypeName = targetTypeName switch { + "nint" or "IntPtr" => "long", + "nuint" or "UIntPtr" => "ulong", + _ => targetTypeName, + }; + } switch (stmt.StmtClass) { @@ -1159,23 +1172,6 @@ private bool IsUnchecked(string targetTypeName, Stmt stmt) { var integerLiteral = (IntegerLiteral)stmt; var signedValue = integerLiteral.Value; - - // A bare integer literal is emitted with its natural C# type (e.g. a value that only - // fits `ulong` gets a `U` suffix and is a `ulong` constant). For the native-sized - // integer targets (`nint`/`nuint`), the platform-dependent narrowing that could - // overflow on a 32-bit target only happens when the generator emits the explicit - // `(nint)`/`(nuint)` cast, which it only does for `VarDecl` initializers (see - // `UncheckStmt`). Outside of that context the literal keeps its natural type and - // never needs an `unchecked` scope, so evaluate against the widest equivalent. - if (!IsPrevContextDecl(out _, out _)) - { - targetTypeName = targetTypeName switch { - "nint" or "IntPtr" => "long", - "nuint" or "UIntPtr" => "ulong", - _ => targetTypeName, - }; - } - return IsUnchecked(targetTypeName, signedValue, integerLiteral.IsNegative, isHex: integerLiteral.ValueString.StartsWith("0x", StringComparison.Ordinal)); }