Skip to content

fix #21504 - __traits(compiles) affects inference of @nogc in functions #21505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 5, 2025
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
4 changes: 2 additions & 2 deletions compiler/src/dmd/expressionsem.d
Original file line number Diff line number Diff line change
Expand Up @@ -2341,7 +2341,7 @@ private bool checkNogc(FuncDeclaration f, ref Loc loc, Scope* sc)
if (f.isNogc())
return false;

if (isRootTraitsCompilesScope(sc) ? !sc.func.isNogcBypassingInference() : !sc.func.setGCCall(f))
if (isRootTraitsCompilesScope(sc) ? !sc.func.isNogcBypassingInference() : !sc.setGCCall(sc.func, f))
return false;

if (loc == Loc.initial) // e.g. implicitly generated dtor
Expand Down Expand Up @@ -6792,7 +6792,7 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor
sc.func.kind(), sc.func.toPrettyChars(), p, exp.e1.toErrMsg());
err = true;
}
if (!tf.isNogc && sc.func.setGC(exp.loc, "calling non-@nogc `%s`", exp.e1))
if (!tf.isNogc && sc.setGC(sc.func, exp.loc, "calling non-@nogc `%s`", exp.e1))
{
error(exp.loc, "`@nogc` %s `%s` cannot call non-@nogc %s `%s`",
sc.func.kind(), sc.func.toPrettyChars(), p, exp.e1.toErrMsg());
Expand Down
34 changes: 25 additions & 9 deletions compiler/src/dmd/nogc.d
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import dmd.errors;
import dmd.escape;
import dmd.expression;
import dmd.func;
import dmd.funcsem : isRootTraitsCompilesScope;
import dmd.globals;
import dmd.init;
import dmd.location;
Expand Down Expand Up @@ -96,7 +97,7 @@ public:
err = true;
return true;
}
if (f.setGC(e.loc, msg))
if (sc.setGC(f, e.loc, msg))
{
error(e.loc, "%s causes a GC allocation in `@nogc` %s `%s`", msg, f.kind(), f.toChars());
err = true;
Expand Down Expand Up @@ -151,7 +152,7 @@ public:
{
if (e.placement)
return; // placement new doesn't use the GC
if (e.member && !e.member.isNogc() && f.setGC(e.loc, null))
if (e.member && e.member !is f && !e.member.isNogc() && sc.setGC(f, e.loc, null))
{
// @nogc-ness is already checked in NewExp::semantic
return;
Expand Down Expand Up @@ -288,6 +289,7 @@ private FuncDeclaration stripHookTraceImpl(FuncDeclaration fd)
* so mark it as not nogc (not no-how).
*
* Params:
* sc = scope that the GC action is in
* fd = function
* loc = location of GC action
* fmt = format string for error message. Must include "%s `%s`" for the function kind and name.
Expand All @@ -296,7 +298,7 @@ private FuncDeclaration stripHookTraceImpl(FuncDeclaration fd)
* Returns:
* true if function is marked as @nogc, meaning a user error occurred
*/
extern (D) bool setGC(FuncDeclaration fd, Loc loc, const(char)* fmt, RootObject[] args...)
extern (D) bool setGC(Scope* sc, FuncDeclaration fd, Loc loc, const(char)* fmt, RootObject[] args...)
{
//printf("setGC() %s\n", toChars());
if (fd.nogcInprocess && fd.semanticRun < PASS.semantic3 && fd._scope)
Expand All @@ -305,6 +307,19 @@ extern (D) bool setGC(FuncDeclaration fd, Loc loc, const(char)* fmt, RootObject[
fd.semantic3(fd._scope);
}

if (sc && isRootTraitsCompilesScope(sc)) // __traits(compiles, x)
{
if (sc.func.isNogcBypassingInference())
{
// Message wil be gagged, but still call error() to update global.errors and for
// -verrors=spec
string action = AttributeViolation(loc, fmt, args).action;
.error(loc, "%.*s is not allowed in a `@nogc` function", action.fTuple.expand);
return true;
}
return false;
}

if (fd.nogcInprocess)
{
fd.nogcInprocess = false;
Expand All @@ -323,7 +338,7 @@ extern (D) bool setGC(FuncDeclaration fd, Loc loc, const(char)* fmt, RootObject[

fd.type.toTypeFunction().isNogc = false;
if (fd.fes)
fd.fes.func.setGC(Loc.init, null, null);
sc.setGC(fd.fes.func, Loc.init, null, null);
}
else if (fd.isNogc())
return true;
Expand All @@ -333,21 +348,22 @@ extern (D) bool setGC(FuncDeclaration fd, Loc loc, const(char)* fmt, RootObject[
/**************************************
* The function calls non-`@nogc` function f, mark it as not nogc.
* Params:
* fd = function doin the call
* sc = scope that the GC action is in
* fd = function doing the call
* f = function being called
* Returns:
* true if function is marked as @nogc, meaning a user error occurred
*/
extern (D) bool setGCCall(FuncDeclaration fd, FuncDeclaration f)
extern (D) bool setGCCall(Scope* sc, FuncDeclaration fd, FuncDeclaration f)
{
return fd.setGC(fd.loc, null, f);
return sc.setGC(fd, fd.loc, null, f);
}

bool isNogc(FuncDeclaration fd)
bool isNogc(FuncDeclaration fd)
{
//printf("isNogc() %s, inprocess: %d\n", toChars(), !!(flags & FUNCFLAG.nogcInprocess));
if (fd.nogcInprocess)
fd.setGC(fd.loc, null);
setGC(null, fd, fd.loc, null);
return fd.type.toTypeFunction().isNogc;
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dmd/semantic3.d
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,7 @@ extern (D) bool checkClosure(FuncDeclaration fd)
if (!fd.needsClosure())
return false;

if (fd.setGC(fd.loc, "allocating a closure for `%s()`", fd))
if (setGC(null, fd, fd.loc, "allocating a closure for `%s()`", fd))
{
.error(fd.loc, "%s `%s` is `@nogc` yet allocates closure for `%s()` with the GC", fd.kind, fd.toPrettyChars(), fd.toChars());
if (global.gag) // need not report supplemental errors
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dmd/statementsem.d
Original file line number Diff line number Diff line change
Expand Up @@ -3645,7 +3645,7 @@ Statement statementSemanticVisit(Statement s, Scope* sc)
assert(sc.func);
if (!(cas.stc & STC.pure_) && sc.func.setImpure(cas.loc, "executing an `asm` statement without `pure` annotation"))
error(cas.loc, "`asm` statement is assumed to be impure - mark it with `pure` if it is not");
if (!(cas.stc & STC.nogc) && sc.func.setGC(cas.loc, "executing an `asm` statement without `@nogc` annotation"))
if (!(cas.stc & STC.nogc) && sc.setGC(sc.func, cas.loc, "executing an `asm` statement without `@nogc` annotation"))
error(cas.loc, "`asm` statement is assumed to use the GC - mark it with `@nogc` if it does not");
// @@@DEPRECATED_2.114@@@
// change deprecation() to error(), add `else` and remove `| STC.safe`
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dmd/typesem.d
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ private extern(D) bool isCopyConstructorCallable (StructDeclaration argStruct,

bool bpure = !f.isPure && sc.func.setImpure(arg.loc, null);
bool bsafe = !f.isSafe() && !f.isTrusted() && sc.setUnsafe(false, arg.loc, null);
bool bnogc = !f.isNogc && sc.func.setGC(arg.loc, null);
bool bnogc = !f.isNogc && sc.setGC(sc.func, arg.loc, null);
if (bpure | bsafe | bnogc)
{
const nullptr = "".ptr;
Expand Down
68 changes: 68 additions & 0 deletions compiler/test/compilable/test21504a.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// https://github.com/dlang/dmd/issues/21504

// Test that:
// 1. __traits(compiles) tests fail when violating an explicitly set attribute.
// 2. __traits(compiles) tests pass when attribute is inferred (see functions
// isPureBypassingInference, isSafeBypassingInference, isNogcBypassingInference).
// 3. __traits(compiles) tests do not affect the result of attribute inference,
// as the they are only evaluated at compile-time, not run-time.

__gshared int testPure;

auto testPure1() pure
{
return __traits(compiles, testPure = 1);
}
auto testPure2()
{
return __traits(compiles, testPure = 1);
}

static assert(!testPure1() && testPure2());
static assert(__traits(getFunctionAttributes, testPure1) == __traits(getFunctionAttributes, testPure2));

////////////////////////////

__gshared Exception testNothrow;

auto testNothrow1() nothrow
{
return __traits(compiles, throw testNothrow);
}
auto testNothrow2()
{
return __traits(compiles, throw testNothrow);
}

static assert(!testNothrow1() && testNothrow2());
static assert(__traits(getFunctionAttributes, testNothrow1) == __traits(getFunctionAttributes, testNothrow2));

////////////////////////////

__gshared int* testSafe;

auto testSafe1() @safe
{
return __traits(compiles, testSafe + 1);
}
auto testSafe2()
{
return __traits(compiles, testSafe + 1);
}

static assert(!testSafe1() && testSafe2());
static assert(__traits(getFunctionAttributes, testSafe1) == __traits(getFunctionAttributes, testSafe2));

////////////////////////////

auto testNogc1() @nogc
{
return __traits(compiles, [1, 2]);
}
auto testNogc2()
{
return __traits(compiles, [1, 2]);
}

static assert(!testNogc1() && testNogc2());
static assert(__traits(getFunctionAttributes, testNogc1) == __traits(getFunctionAttributes, testNogc2));
17 changes: 17 additions & 0 deletions compiler/test/compilable/test21504b.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// https://github.com/dlang/dmd/issues/21504

// Test case where `@safe` triggers attribute inference of all nested
// declarations, and the NOGCVisitor for `new` expressions encounters a self
// referencing GC action.

void test21504() @nogc @safe
{
static struct Nest
{
this(int i)
{
static assert(__traits(compiles, new Nest(i)));
}
}
auto s = Nest(1);
}
Loading