1 /**
2  * Checks that a function marked `@nogc` does not invoke the Garbage Collector.
3  *
4  * Specification: $(LINK2 https://dlang.org/spec/function.html#nogc-functions, No-GC Functions)
5  *
6  * Copyright:   Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
7  * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
8  * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
9  * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/nogc.d, _nogc.d)
10  * Documentation:  https://dlang.org/phobos/dmd_nogc.html
11  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/nogc.d
12  */
13 
14 module dmd.nogc;
15 
16 import core.stdc.stdio;
17 
18 import dmd.aggregate;
19 import dmd.astenums;
20 import dmd.declaration;
21 import dmd.dscope;
22 import dmd.dtemplate : isDsymbol;
23 import dmd.errors;
24 import dmd.expression;
25 import dmd.func;
26 import dmd.globals;
27 import dmd.init;
28 import dmd.mtype;
29 import dmd.postordervisitor;
30 import dmd.tokens;
31 import dmd.visitor;
32 
33 /**************************************
34  * Look for GC-allocations
35  */
36 extern (C++) final class NOGCVisitor : StoppableVisitor
37 {
38     alias visit = typeof(super).visit;
39 public:
40     FuncDeclaration f;
41     bool checkOnly;     // don't print errors
42     bool err;
43 
44     extern (D) this(FuncDeclaration f) scope @safe
45     {
46         this.f = f;
47     }
48 
49     void doCond(Expression exp)
50     {
51         if (exp)
52             walkPostorder(exp, this);
53     }
54 
55     override void visit(Expression e)
56     {
57     }
58 
59     override void visit(DeclarationExp e)
60     {
61         // Note that, walkPostorder does not support DeclarationExp today.
62         VarDeclaration v = e.declaration.isVarDeclaration();
63         if (v && !(v.storage_class & STC.manifest) && !v.isDataseg() && v._init)
64         {
65             if (ExpInitializer ei = v._init.isExpInitializer())
66             {
67                 doCond(ei.exp);
68             }
69         }
70     }
71 
72     /**
73      * Register that expression `e` requires the GC
74      * Params:
75      *   e = expression that uses GC
76      *   format = error message when `e` is used in a `@nogc` function.
77      *            Must contain format strings "`@nogc` %s `%s`" referring to the function.
78      * Returns: `true` if `err` was set, `false` if it's not in a `@nogc` and not checkonly (-betterC)
79      */
80     private bool setGC(Expression e, const(char)* format)
81     {
82         if (checkOnly)
83         {
84             err = true;
85             return true;
86         }
87         if (f.setGC(e.loc, format))
88         {
89             error(e.loc, format, f.kind(), f.toPrettyChars());
90             err = true;
91             return true;
92         }
93         return false;
94     }
95 
96     override void visit(CallExp e)
97     {
98         import dmd.id : Id;
99         import core.stdc.stdio : printf;
100         if (!e.f)
101             return;
102 
103         // Treat lowered hook calls as their original expressions.
104         auto fd = stripHookTraceImpl(e.f);
105         if (fd.ident == Id._d_arraysetlengthT)
106         {
107             if (setGC(e, "setting `length` in `@nogc` %s `%s` may cause a GC allocation"))
108                 return;
109             f.printGCUsage(e.loc, "setting `length` may cause a GC allocation");
110         }
111         else if (fd.ident == Id._d_arrayappendT || fd.ident == Id._d_arrayappendcTX)
112         {
113             if (setGC(e, "cannot use operator `~=` in `@nogc` %s `%s`"))
114                 return;
115             f.printGCUsage(e.loc, "operator `~=` may cause a GC allocation");
116         }
117     }
118 
119     override void visit(ArrayLiteralExp e)
120     {
121         if (e.type.ty != Tarray || !e.elements || !e.elements.length || e.onstack)
122             return;
123         if (setGC(e, "array literal in `@nogc` %s `%s` may cause a GC allocation"))
124             return;
125         f.printGCUsage(e.loc, "array literal may cause a GC allocation");
126     }
127 
128     override void visit(AssocArrayLiteralExp e)
129     {
130         if (!e.keys.length)
131             return;
132         if (setGC(e, "associative array literal in `@nogc` %s `%s` may cause a GC allocation"))
133             return;
134         f.printGCUsage(e.loc, "associative array literal may cause a GC allocation");
135     }
136 
137     override void visit(NewExp e)
138     {
139         if (e.member && !e.member.isNogc() && f.setGC(e.loc, null))
140         {
141             // @nogc-ness is already checked in NewExp::semantic
142             return;
143         }
144         if (e.onstack)
145             return;
146         if (global.params.ehnogc && e.thrownew)
147             return;                     // separate allocator is called for this, not the GC
148 
149         if (setGC(e, "cannot use `new` in `@nogc` %s `%s`"))
150             return;
151         f.printGCUsage(e.loc, "`new` causes a GC allocation");
152     }
153 
154     override void visit(DeleteExp e)
155     {
156         if (VarExp ve = e.e1.isVarExp())
157         {
158             VarDeclaration v = ve.var.isVarDeclaration();
159             if (v && v.onstack)
160                 return; // delete for scope allocated class object
161         }
162 
163         // Semantic should have already handled this case.
164         assert(0);
165     }
166 
167     override void visit(IndexExp e)
168     {
169         Type t1b = e.e1.type.toBasetype();
170         if (e.modifiable && t1b.ty == Taarray)
171         {
172             if (setGC(e, "assigning an associative array element in `@nogc` %s `%s` may cause a GC allocation"))
173                 return;
174             f.printGCUsage(e.loc, "assigning an associative array element may cause a GC allocation");
175         }
176     }
177 
178     override void visit(AssignExp e)
179     {
180         if (e.e1.op == EXP.arrayLength)
181         {
182             if (setGC(e, "setting `length` in `@nogc` %s `%s` may cause a GC allocation"))
183                 return;
184             f.printGCUsage(e.loc, "setting `length` may cause a GC allocation");
185         }
186     }
187 
188     override void visit(CatAssignExp e)
189     {
190         /* CatAssignExp will exist in `__traits(compiles, ...)` and in the `.e1` branch of a `__ctfe ? :` CondExp.
191          * The other branch will be `_d_arrayappendcTX(e1, 1), e1[$-1]=e2` which will generate the warning about
192          * GC usage. See visit(CallExp).
193          */
194         if (checkOnly)
195         {
196             err = true;
197             return;
198         }
199         if (f.setGC(e.loc, null))
200         {
201             err = true;
202             return;
203         }
204     }
205 
206     override void visit(CatExp e)
207     {
208         if (setGC(e, "cannot use operator `~` in `@nogc` %s `%s`"))
209             return;
210         f.printGCUsage(e.loc, "operator `~` may cause a GC allocation");
211     }
212 }
213 
214 Expression checkGC(Scope* sc, Expression e)
215 {
216     if (sc.flags & SCOPE.ctfeBlock)     // ignore GC in ctfe blocks
217         return e;
218 
219     /* If betterC, allow GC to happen in non-CTFE code.
220      * Just don't generate code for it.
221      * Detect non-CTFE use of the GC in betterC code.
222      */
223     const betterC = !global.params.useGC;
224     FuncDeclaration f = sc.func;
225     if (e && e.op != EXP.error && f && sc.intypeof != 1 &&
226            (!(sc.flags & SCOPE.ctfe) || betterC) &&
227            (f.type.ty == Tfunction &&
228             (cast(TypeFunction)f.type).isnogc || f.nogcInprocess || global.params.v.gc) &&
229            !(sc.flags & SCOPE.debug_))
230     {
231         scope NOGCVisitor gcv = new NOGCVisitor(f);
232         gcv.checkOnly = betterC;
233         walkPostorder(e, gcv);
234         if (gcv.err)
235         {
236             if (betterC)
237             {
238                 /* Allow ctfe to use the gc code, but don't let it into the runtime
239                  */
240                 f.skipCodegen = true;
241             }
242             else
243                 return ErrorExp.get();
244         }
245     }
246     return e;
247 }
248 
249 /**
250  * Removes `_d_HookTraceImpl` if found from `fd`.
251  * This is needed to be able to find hooks that are called though the hook's `*Trace` wrapper.
252  * Parameters:
253  *  fd = The function declaration to remove `_d_HookTraceImpl` from
254  */
255 private FuncDeclaration stripHookTraceImpl(FuncDeclaration fd)
256 {
257     import dmd.id : Id;
258     import dmd.dsymbol : Dsymbol;
259     import dmd.rootobject : RootObject, DYNCAST;
260 
261     if (fd.ident != Id._d_HookTraceImpl)
262         return fd;
263 
264     // Get the Hook from the second template parameter
265     auto templateInstance = fd.parent.isTemplateInstance;
266     RootObject hook = (*templateInstance.tiargs)[1];
267     Dsymbol s = hook.isDsymbol();
268     assert(s, "Expected _d_HookTraceImpl's second template parameter to be an alias to the hook!");
269     return s.isFuncDeclaration;
270 }