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