1 
2 //          Copyright Tim Schendekehl 2023.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          https://www.boost.org/LICENSE_1_0.txt)
6 
7 module dparsergen.generator.codewriter;
8 import std.algorithm;
9 import std.array;
10 import std.conv;
11 import std.string;
12 
13 struct CodeWriter
14 {
15     private char[] buffer;
16     size_t dataSize;
17     size_t indent;
18     string indentStr = "\t";
19     string customIndent;
20     bool inLine;
21     size_t currentLine;
22     size_t currentOffset;
23     size_t lineStart;
24 
25     void ensureAddable(size_t n)
26     {
27         if (dataSize + n > buffer.length)
28         {
29             size_t newSize = dataSize;
30             if (newSize < 1024)
31                 newSize = 1024;
32             while (dataSize + n > newSize)
33                 newSize *= 2;
34             buffer.length = newSize;
35         }
36     }
37 
38     void put(string data)
39     {
40         ensureAddable(data.length);
41         buffer[dataSize .. dataSize + data.length] = data;
42         dataSize += data.length;
43     }
44 
45     void put(char c)
46     {
47         ensureAddable(1);
48         buffer[dataSize] = c;
49         dataSize++;
50     }
51 
52     const(char)[] data() const
53     {
54         return buffer[0 .. dataSize];
55     }
56 
57     void endLine()
58     {
59         if (!inLine)
60         {
61             lineStart = data.length;
62         }
63         put('\n');
64         inLine = false;
65         currentLine++;
66         currentOffset = 0;
67     }
68 
69     void startLine(bool afterNewline = false)
70     {
71         if (!inLine)
72         {
73             if (!afterNewline)
74             {
75                 lineStart = data.length;
76             }
77             foreach (i; 0 .. indent)
78                 put(indentStr);
79             currentOffset += indent;
80             put(customIndent);
81             currentOffset += customIndent.length;
82             inLine = true;
83         }
84     }
85 
86     ref CodeWriter write(T...)(T args) return
87     {
88         bool afterNewline;
89         foreach (arg; args)
90         {
91             foreach (char c; text(arg))
92             {
93                 if (c == '\n')
94                 {
95                     endLine();
96                     afterNewline = true;
97                 }
98                 else
99                 {
100                     startLine(afterNewline);
101                     put(c);
102                     currentOffset++;
103                 }
104             }
105         }
106         return this;
107     }
108 
109     ref CodeWriter writeln(T...)(T args) return
110     {
111         write(args);
112         endLine();
113         return this;
114     }
115 
116     ref CodeWriter incIndent(size_t n = 1) return
117     {
118         indent += n;
119         return this;
120     }
121 
122     ref CodeWriter decIndent(size_t n = 1, string func = __FUNCTION__,
123             string file = __FILE__, size_t line = __LINE__) return
124     {
125         assert(indent >= n, text("CodeWrite.decIndent indent=", indent, " n=",
126                 n, "  ", func, " (", file, ":", line, ")"));
127         indent -= n;
128         return this;
129     }
130 }
131 
132 void splitInput(alias onNormalText, alias onMacro)(string input)
133 {
134     uint status;
135     size_t start;
136     size_t varstart;
137     foreach (i, char c; input)
138     {
139         if (status == 0)
140         {
141             if (c == '$')
142             {
143                 status = 1;
144                 varstart = i;
145             }
146         }
147         else if (status == 1)
148         {
149             if (c == '(')
150             {
151                 status = 2;
152             }
153             else
154             {
155                 status = 0;
156             }
157         }
158         else if (status == 2)
159         {
160             if (c == ')')
161             {
162                 status = 0;
163                 onNormalText(input[start .. varstart]);
164                 onMacro(input[varstart + 2 .. i], "");
165                 start = i + 1;
166             }
167             if (c == '(')
168             {
169                 status++;
170             }
171         }
172         else if (status > 2)
173         {
174             if (c == '(')
175                 status++;
176             if (c == ')')
177                 status--;
178         }
179         else
180             assert(false);
181     }
182     onNormalText(input[start .. $]);
183 }
184 
185 string escapeDString(string s)
186 {
187     char[] buffer;
188     buffer.length = 2 * s.length;
189     size_t i;
190     foreach (char c; s)
191     {
192         if (c == '\"')
193         {
194             buffer[i] = '\\';
195             i++;
196             buffer[i] = '\"';
197             i++;
198         }
199         else if (c == '\t')
200         {
201             buffer[i] = '\\';
202             i++;
203             buffer[i] = 't';
204             i++;
205         }
206         else if (c == '\\')
207         {
208             buffer[i] = '\\';
209             i++;
210             buffer[i] = '\\';
211             i++;
212         }
213         else
214         {
215             buffer[i] = c;
216             i++;
217         }
218     }
219     return buffer[0 .. i].idup;
220 }
221 
222 size_t startWhitespace(string s, size_t spacesPerTab = 4)
223 {
224     size_t numWS;
225     size_t spaces;
226     foreach (char c; s)
227     {
228         if (c == '\t')
229         {
230             numWS++;
231             spaces = 0;
232         }
233         else if (c == ' ')
234         {
235             spaces++;
236             if (spaces == spacesPerTab)
237             {
238                 numWS++;
239                 spaces = 0;
240             }
241         }
242         else
243             break;
244     }
245     return numWS;
246 }
247 
248 string removeStartWhitespace(string s, size_t startWS, size_t spacesPerTab = 4)
249 {
250     size_t numWS;
251     size_t spaces;
252     foreach (i, char c; s)
253     {
254         if (numWS >= startWS)
255             break;
256         if (c == '\t')
257         {
258             numWS++;
259             s = s[1 .. $];
260             spaces = 0;
261         }
262         else if (c == ' ')
263         {
264             spaces++;
265             if (spaces == spacesPerTab)
266             {
267                 numWS++;
268                 s = s[spaces .. $];
269                 spaces = 0;
270             }
271         }
272         else
273             break;
274     }
275     return s;
276 }
277 
278 string intToStr(long l)
279 {
280     char[20] buffer;
281     size_t i;
282     bool negative;
283     if (l < 0)
284     {
285         negative = true;
286         l = -l;
287     }
288     while (l != 0)
289     {
290         i++;
291         buffer[$ - i] = '0' + l % 10;
292         l = l / 10;
293     }
294     if (negative)
295     {
296         i++;
297         buffer[$ - 1] = '-';
298     }
299     return buffer[$ - i .. $].idup;
300 }
301 
302 string genCode(string code, string str, string callingFunction = __FUNCTION__,
303         string callingFilename = __FILE__, size_t callingLine = __LINE__)
304 {
305     CodeWriter app;
306     string[] lines = str.splitLines;
307     if (lines[0] == "")
308     {
309         lines = lines[1 .. $];
310         callingLine++;
311         app.put("\n");
312     }
313     if (lines[$ - 1].all!((c) => c == '\t' || c == ' '))
314         lines = lines[0 .. $ - 1];
315 
316     size_t minNumWhitespace = size_t.max;
317     foreach (i, l; lines)
318     {
319         if (l.all!((c) => c == '\t' || c == ' '))
320             continue;
321         size_t numWhitespace = l.startWhitespace;
322         if (numWhitespace < minNumWhitespace)
323             minNumWhitespace = numWhitespace;
324     }
325 
326     static struct IgnoreInfo
327     {
328         size_t indent;
329         size_t lineNr;
330     }
331 
332     Appender!(IgnoreInfo[]) ignoreInfos;
333 
334     size_t currentStartWhitespace = 0;
335     bool inCodeWriteStmt = false;
336     void startCode()
337     {
338         assert(!inCodeWriteStmt);
339         inCodeWriteStmt = true;
340         app.put(code);
341     }
342 
343     void endCode()
344     {
345         if (!inCodeWriteStmt)
346             return;
347         inCodeWriteStmt = false;
348         app.put(text(";\n"));
349     }
350 
351     void adjustStartWhitespace(size_t n, size_t debugLine, bool doEndCode = false,
352             bool doStartCode = false)
353     {
354         assert(n >= ignoreInfos.data.length, text("n=", n, "  ignoreWhitespace=",
355                 ignoreInfos.data.length, " line ", debugLine, ":\n", lines[debugLine]));
356         if (n < ignoreInfos.data.length)
357         {
358             n = ignoreInfos.data.length;
359         }
360         n -= ignoreInfos.data.length;
361         if (n < currentStartWhitespace)
362         {
363             if (!inCodeWriteStmt)
364                 app.put(code);
365             app.put(text(".decIndent(", intToStr(currentStartWhitespace - n), ", \"", callingFunction,
366                     "\", \"", callingFilename.escapeDString, "\", ", callingLine + debugLine, ")"));
367             if (!inCodeWriteStmt)
368                 app.put(";");
369         }
370         if (doEndCode)
371             endCode();
372         if (doStartCode)
373             startCode();
374         if (n > currentStartWhitespace)
375         {
376             if (!inCodeWriteStmt)
377                 app.put(code);
378             app.put(text(".incIndent(", intToStr(n - currentStartWhitespace), ")"));
379             if (!inCodeWriteStmt)
380                 app.put(";");
381         }
382         currentStartWhitespace = n;
383     }
384 
385     foreach (lineNr, l; lines)
386     {
387         if (l.startWhitespace < minNumWhitespace)
388         {
389             foreach (char c; l)
390                 assert(c == '\t' || c == ' ');
391             endCode();
392             startCode();
393             app.put(text(".writeln()"));
394             continue;
395         }
396         auto line = l.removeStartWhitespace(minNumWhitespace);
397         auto st = line.startWhitespace;
398         line = line.removeStartWhitespace(st);
399         if (line.startsWith("$$"))
400         {
401             string errorMessage;
402             foreach (char c; line)
403             {
404                 if (c == '}')
405                 {
406                     assert(ignoreInfos.data.length, text(callingFunction, " (",
407                             callingFilename, ":", callingLine + lineNr, ")"));
408                     if (st != ignoreInfos.data[$ - 1].indent)
409                     {
410                         errorMessage = text("Braces don't match: ", callingFunction,
411                                 " indent = ", ignoreInfos.data[$ - 1].indent,
412                                 " (", callingFilename, ":",
413                                 callingLine + ignoreInfos.data[$ - 1].lineNr, ")", " indent = ",
414                                 st, " (", callingFilename, ":", callingLine + lineNr, ")");
415                         st = ignoreInfos.data[$ - 1].indent;
416                     }
417                     ignoreInfos.shrinkTo(ignoreInfos.data.length - 1);
418                 }
419             }
420             adjustStartWhitespace(st, lineNr);
421             endCode();
422             if (errorMessage.length)
423                 app.put(text("pragma(msg, \"", errorMessage.escapeDString, "\");"));
424             app.put(line[2 .. $]);
425             app.put("\n");
426             foreach (char c; line)
427             {
428                 if (c == '{')
429                     ignoreInfos.put(IgnoreInfo(st, lineNr));
430             }
431         }
432         else
433         {
434             adjustStartWhitespace(st, lineNr, true, true);
435             bool addNewLine = true;
436             if (line.endsWith("  _"))
437             {
438                 addNewLine = false;
439                 line = line[0 .. $ - 3];
440             }
441 
442             line.splitInput!(
443                 (t) { app.put(text(".write(\"", t.escapeDString, "\")")); },
444                 (m, p) { app.put(text(".write(", m, ")")); });
445             if (addNewLine)
446                 app.put(text(".writeln()"));
447         }
448     }
449     adjustStartWhitespace(0, lines.length - 1, true, false);
450 
451     return app.data.idup;
452 }