From eab6cd85cf594c2c806d826df43cb8fa077dfbb2 Mon Sep 17 00:00:00 2001 From: Iain Buclaw Date: Fri, 4 Jul 2025 20:29:10 +0200 Subject: [PATCH] fix #21504 - __traits(compiles) affects inference of @nogc in functions Adds test for `isRootTraitsCompilesScope` within `setGC`, so that a compile-time evaluation of a GC action does not alter the inferred attributes of a function and its function type. Closes: #21504 --- compiler/src/dmd/expressionsem.d | 4 +- compiler/src/dmd/nogc.d | 34 ++++++++++---- compiler/src/dmd/semantic3.d | 2 +- compiler/src/dmd/statementsem.d | 2 +- compiler/src/dmd/typesem.d | 2 +- compiler/test/compilable/test21504a.d | 68 +++++++++++++++++++++++++++ compiler/test/compilable/test21504b.d | 17 +++++++ 7 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 compiler/test/compilable/test21504a.d create mode 100644 compiler/test/compilable/test21504b.d diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index f18f61bde222..c834afa87249 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -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 @@ -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()); diff --git a/compiler/src/dmd/nogc.d b/compiler/src/dmd/nogc.d index 5e3c164acbb4..604f45ed6afb 100644 --- a/compiler/src/dmd/nogc.d +++ b/compiler/src/dmd/nogc.d @@ -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; @@ -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; @@ -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; @@ -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. @@ -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) @@ -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; @@ -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; @@ -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; } diff --git a/compiler/src/dmd/semantic3.d b/compiler/src/dmd/semantic3.d index bafb2e7d8719..53f389822b05 100644 --- a/compiler/src/dmd/semantic3.d +++ b/compiler/src/dmd/semantic3.d @@ -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 diff --git a/compiler/src/dmd/statementsem.d b/compiler/src/dmd/statementsem.d index 523ed7b39781..8a71c709cc60 100644 --- a/compiler/src/dmd/statementsem.d +++ b/compiler/src/dmd/statementsem.d @@ -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` diff --git a/compiler/src/dmd/typesem.d b/compiler/src/dmd/typesem.d index ada16b8f79bc..8b677521c59f 100644 --- a/compiler/src/dmd/typesem.d +++ b/compiler/src/dmd/typesem.d @@ -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; diff --git a/compiler/test/compilable/test21504a.d b/compiler/test/compilable/test21504a.d new file mode 100644 index 000000000000..8303fd72e77b --- /dev/null +++ b/compiler/test/compilable/test21504a.d @@ -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)); diff --git a/compiler/test/compilable/test21504b.d b/compiler/test/compilable/test21504b.d new file mode 100644 index 000000000000..152448f9d65e --- /dev/null +++ b/compiler/test/compilable/test21504b.d @@ -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); +}