1/**
2 * Text macro processor for Ddoc.
3 *
4 * Copyright:   Copyright (C) 1999-2022 by The D Language Foundation, All Rights Reserved
5 * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
6 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/dmacro.d, _dmacro.d)
8 * Documentation:  https://dlang.org/phobos/dmd_dmacro.html
9 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/dmacro.d
10 */
11
12module dmd.dmacro;
13
14import core.stdc.ctype;
15import core.stdc.string;
16import dmd.doc;
17import dmd.errors;
18import dmd.globals;
19import dmd.common.outbuffer;
20import dmd.root.rmem;
21
22extern (C++) struct MacroTable
23{
24    /**********************************
25     * Define name=text macro.
26     * If macro `name` already exists, replace the text for it.
27     * Params:
28     *  name = name of macro
29     *  text = text of macro
30     */
31    extern (D) void define(const(char)[] name, const(char)[] text)
32    {
33        //printf("MacroTable::define('%.*s' = '%.*s')\n", cast(int)name.length, name.ptr, text.length, text.ptr);
34        if (auto table = name in mactab)
35        {
36            (*table).text = text;
37            return;
38        }
39        mactab[name] = new Macro(name, text);
40    }
41
42    /*****************************************************
43     * Look for macros in buf and expand them in place.
44     * Only look at the text in buf from start to pend.
45     */
46    extern (D) void expand(ref OutBuffer buf, size_t start, ref size_t pend, const(char)[] arg)
47    {
48        version (none)
49        {
50            printf("Macro::expand(buf[%d..%d], arg = '%.*s')\n", start, pend, cast(int)arg.length, arg.ptr);
51            printf("Buf is: '%.*s'\n", cast(int)(pend - start), buf.data + start);
52        }
53        // limit recursive expansion
54        __gshared int nest;
55        if (nest > global.recursionLimit)
56        {
57            error(Loc.initial, "DDoc macro expansion limit exceeded; more than %d expansions.",
58                  global.recursionLimit);
59            return;
60        }
61        nest++;
62        size_t end = pend;
63        assert(start <= end);
64        assert(end <= buf.length);
65        /* First pass - replace $0
66         */
67        arg = memdup(arg);
68        for (size_t u = start; u + 1 < end;)
69        {
70            char* p = cast(char*)buf[].ptr; // buf.data is not loop invariant
71            /* Look for $0, but not $$0, and replace it with arg.
72             */
73            if (p[u] == '$' && (isdigit(p[u + 1]) || p[u + 1] == '+'))
74            {
75                if (u > start && p[u - 1] == '$')
76                {
77                    // Don't expand $$0, but replace it with $0
78                    buf.remove(u - 1, 1);
79                    end--;
80                    u += 1; // now u is one past the closing '1'
81                    continue;
82                }
83                char c = p[u + 1];
84                int n = (c == '+') ? -1 : c - '0';
85                const(char)[] marg;
86                if (n == 0)
87                {
88                    marg = arg;
89                }
90                else
91                    extractArgN(arg, marg, n);
92                if (marg.length == 0)
93                {
94                    // Just remove macro invocation
95                    //printf("Replacing '$%c' with '%.*s'\n", p[u + 1], cast(int)marg.length, marg.ptr);
96                    buf.remove(u, 2);
97                    end -= 2;
98                }
99                else if (c == '+')
100                {
101                    // Replace '$+' with 'arg'
102                    //printf("Replacing '$%c' with '%.*s'\n", p[u + 1], cast(int)marg.length, marg.ptr);
103                    buf.remove(u, 2);
104                    buf.insert(u, marg);
105                    end += marg.length - 2;
106                    // Scan replaced text for further expansion
107                    size_t mend = u + marg.length;
108                    expand(buf, u, mend, null);
109                    end += mend - (u + marg.length);
110                    u = mend;
111                }
112                else
113                {
114                    // Replace '$1' with '\xFF{arg\xFF}'
115                    //printf("Replacing '$%c' with '\xFF{%.*s\xFF}'\n", p[u + 1], cast(int)marg.length, marg.ptr);
116                    ubyte[] slice = cast(ubyte[])buf[];
117                    slice[u] = 0xFF;
118                    slice[u + 1] = '{';
119                    buf.insert(u + 2, marg);
120                    buf.insert(u + 2 + marg.length, "\xFF}");
121                    end += -2 + 2 + marg.length + 2;
122                    // Scan replaced text for further expansion
123                    size_t mend = u + 2 + marg.length;
124                    expand(buf, u + 2, mend, null);
125                    end += mend - (u + 2 + marg.length);
126                    u = mend;
127                }
128                //printf("u = %d, end = %d\n", u, end);
129                //printf("#%.*s#\n", cast(int)end, &buf.data[0]);
130                continue;
131            }
132            u++;
133        }
134        /* Second pass - replace other macros
135         */
136        for (size_t u = start; u + 4 < end;)
137        {
138            char* p = cast(char*)buf[].ptr; // buf.data is not loop invariant
139            /* A valid start of macro expansion is $(c, where c is
140             * an id start character, and not $$(c.
141             */
142            if (p[u] == '$' && p[u + 1] == '(' && isIdStart(p + u + 2))
143            {
144                //printf("\tfound macro start '%c'\n", p[u + 2]);
145                char* name = p + u + 2;
146                size_t namelen = 0;
147                const(char)[] marg;
148                size_t v;
149                /* Scan forward to find end of macro name and
150                 * beginning of macro argument (marg).
151                 */
152                for (v = u + 2; v < end; v += utfStride(p + v))
153                {
154                    if (!isIdTail(p + v))
155                    {
156                        // We've gone past the end of the macro name.
157                        namelen = v - (u + 2);
158                        break;
159                    }
160                }
161                v += extractArgN(p[v .. end], marg, 0);
162                assert(v <= end);
163                if (v < end)
164                {
165                    // v is on the closing ')'
166                    if (u > start && p[u - 1] == '$')
167                    {
168                        // Don't expand $$(NAME), but replace it with $(NAME)
169                        buf.remove(u - 1, 1);
170                        end--;
171                        u = v; // now u is one past the closing ')'
172                        continue;
173                    }
174                    Macro* m = search(name[0 .. namelen]);
175                    if (!m)
176                    {
177                        immutable undef = "DDOC_UNDEFINED_MACRO";
178                        m = search(undef);
179                        if (m)
180                        {
181                            // Macro was not defined, so this is an expansion of
182                            //   DDOC_UNDEFINED_MACRO. Prepend macro name to args.
183                            // marg = name[ ] ~ "," ~ marg[ ];
184                            if (marg.length)
185                            {
186                                char* q = cast(char*)mem.xmalloc(namelen + 1 + marg.length);
187                                assert(q);
188                                memcpy(q, name, namelen);
189                                q[namelen] = ',';
190                                memcpy(q + namelen + 1, marg.ptr, marg.length);
191                                marg = q[0 .. marg.length + namelen + 1];
192                            }
193                            else
194                            {
195                                marg = name[0 .. namelen];
196                            }
197                        }
198                    }
199                    if (m)
200                    {
201                        if (m.inuse && marg.length == 0)
202                        {
203                            // Remove macro invocation
204                            buf.remove(u, v + 1 - u);
205                            end -= v + 1 - u;
206                        }
207                        else if (m.inuse && ((arg.length == marg.length && memcmp(arg.ptr, marg.ptr, arg.length) == 0) ||
208                                             (arg.length + 4 == marg.length && marg[0] == 0xFF && marg[1] == '{' && memcmp(arg.ptr, marg.ptr + 2, arg.length) == 0 && marg[marg.length - 2] == 0xFF && marg[marg.length - 1] == '}')))
209                        {
210                            /* Recursive expansion:
211                             *   marg is same as arg (with blue paint added)
212                             * Just leave in place.
213                             */
214                        }
215                        else
216                        {
217                            //printf("\tmacro '%.*s'(%.*s) = '%.*s'\n", cast(int)m.namelen, m.name, cast(int)marg.length, marg.ptr, cast(int)m.textlen, m.text);
218                            marg = memdup(marg);
219                            // Insert replacement text
220                            buf.spread(v + 1, 2 + m.text.length + 2);
221                            ubyte[] slice = cast(ubyte[])buf[];
222                            slice[v + 1] = 0xFF;
223                            slice[v + 2] = '{';
224                            slice[v + 3 .. v + 3 + m.text.length] = cast(ubyte[])m.text[];
225                            slice[v + 3 + m.text.length] = 0xFF;
226                            slice[v + 3 + m.text.length + 1] = '}';
227                            end += 2 + m.text.length + 2;
228                            // Scan replaced text for further expansion
229                            m.inuse++;
230                            size_t mend = v + 1 + 2 + m.text.length + 2;
231                            expand(buf, v + 1, mend, marg);
232                            end += mend - (v + 1 + 2 + m.text.length + 2);
233                            m.inuse--;
234                            buf.remove(u, v + 1 - u);
235                            end -= v + 1 - u;
236                            u += mend - (v + 1);
237                            mem.xfree(cast(char*)marg.ptr);
238                            //printf("u = %d, end = %d\n", u, end);
239                            //printf("#%.*s#\n", cast(int)(end - u), &buf.data[u]);
240                            continue;
241                        }
242                    }
243                    else
244                    {
245                        // Replace $(NAME) with nothing
246                        buf.remove(u, v + 1 - u);
247                        end -= (v + 1 - u);
248                        continue;
249                    }
250                }
251            }
252            u++;
253        }
254        mem.xfree(cast(char*)arg);
255        pend = end;
256        nest--;
257    }
258
259  private:
260
261    extern (D) Macro* search(const(char)[] name)
262    {
263        //printf("Macro::search(%.*s)\n", cast(int)name.length, name.ptr);
264        if (auto table = name in mactab)
265        {
266            //printf("\tfound %d\n", table.textlen);
267            return *table;
268        }
269        return null;
270    }
271
272    private Macro*[const(char)[]] mactab;
273}
274
275/* ************************************************************************ */
276
277private:
278
279struct Macro
280{
281    const(char)[] name;     // macro name
282    const(char)[] text;     // macro replacement text
283    int inuse;              // macro is in use (don't expand)
284
285    this(const(char)[] name, const(char)[] text)
286    {
287        this.name = name;
288        this.text = text;
289    }
290}
291
292/************************
293 * Make mutable copy of slice p.
294 * Params:
295 *      p = slice
296 * Returns:
297 *      copy allocated with mem.xmalloc()
298 */
299
300char[] memdup(const(char)[] p)
301{
302    size_t len = p.length;
303    return (cast(char*)memcpy(mem.xmalloc(len), p.ptr, len))[0 .. len];
304}
305
306/**********************************************************
307 * Given buffer buf[], extract argument marg[].
308 * Params:
309 *      buf = source string
310 *      marg = set to slice of buf[]
311 *      n =     0:      get entire argument
312 *              1..9:   get nth argument
313 *              -1:     get 2nd through end
314 */
315size_t extractArgN(const(char)[] buf, out const(char)[] marg, int n)
316{
317    /* Scan forward for matching right parenthesis.
318     * Nest parentheses.
319     * Skip over "..." and '...' strings inside HTML tags.
320     * Skip over <!-- ... --> comments.
321     * Skip over previous macro insertions
322     * Set marg.
323     */
324    uint parens = 1;
325    ubyte instring = 0;
326    uint incomment = 0;
327    uint intag = 0;
328    uint inexp = 0;
329    uint argn = 0;
330    size_t v = 0;
331    const p = buf.ptr;
332    const end = buf.length;
333Largstart:
334    // Skip first space, if any, to find the start of the macro argument
335    if (n != 1 && v < end && isspace(p[v]))
336        v++;
337    size_t vstart = v;
338    for (; v < end; v++)
339    {
340        char c = p[v];
341        switch (c)
342        {
343        case ',':
344            if (!inexp && !instring && !incomment && parens == 1)
345            {
346                argn++;
347                if (argn == 1 && n == -1)
348                {
349                    v++;
350                    goto Largstart;
351                }
352                if (argn == n)
353                    break;
354                if (argn + 1 == n)
355                {
356                    v++;
357                    goto Largstart;
358                }
359            }
360            continue;
361        case '(':
362            if (!inexp && !instring && !incomment)
363                parens++;
364            continue;
365        case ')':
366            if (!inexp && !instring && !incomment && --parens == 0)
367            {
368                break;
369            }
370            continue;
371        case '"':
372        case '\'':
373            if (!inexp && !incomment && intag)
374            {
375                if (c == instring)
376                    instring = 0;
377                else if (!instring)
378                    instring = c;
379            }
380            continue;
381        case '<':
382            if (!inexp && !instring && !incomment)
383            {
384                if (v + 6 < end && p[v + 1] == '!' && p[v + 2] == '-' && p[v + 3] == '-')
385                {
386                    incomment = 1;
387                    v += 3;
388                }
389                else if (v + 2 < end && isalpha(p[v + 1]))
390                    intag = 1;
391            }
392            continue;
393        case '>':
394            if (!inexp)
395                intag = 0;
396            continue;
397        case '-':
398            if (!inexp && !instring && incomment && v + 2 < end && p[v + 1] == '-' && p[v + 2] == '>')
399            {
400                incomment = 0;
401                v += 2;
402            }
403            continue;
404        case 0xFF:
405            if (v + 1 < end)
406            {
407                if (p[v + 1] == '{')
408                    inexp++;
409                else if (p[v + 1] == '}')
410                    inexp--;
411            }
412            continue;
413        default:
414            continue;
415        }
416        break;
417    }
418    if (argn == 0 && n == -1)
419        marg = p[v .. v];
420    else
421        marg = p[vstart .. v];
422    //printf("extractArg%d('%.*s') = '%.*s'\n", n, cast(int)end, p, cast(int)marg.length, marg.ptr);
423    return v;
424}
425