yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

ArielG-NVFix `slang-generate` segfault when parsing `$(()...)` (#7683)73e9987c9

master
22.0 KiB912 linesraw
1// main.cpp
2
3#include "../../source/core/slang-io.h"
4#include "../../source/core/slang-list.h"
5#include "../../source/core/slang-secure-crt.h"
6#include "../../source/core/slang-string-util.h"
7#include "../../source/core/slang-string.h"
8
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12
13using namespace Slang;
14
15typedef Slang::UnownedStringSlice StringSpan;
16
17struct Node
18{
19    enum class Flavor
20    {
21        text,   // Ordinary text to write to output
22        escape, // Meta-level code (statements)
23        splice, // Meta-level expression to splice into output
24    };
25
26    // What sort of node is this?
27    Flavor flavor;
28
29    // The text of this node for `Flavor::text`
30    StringSpan span;
31
32    // The body of this node for other flavors
33    Node* body = nullptr;
34
35    // The next node in the document
36    Node* next = nullptr;
37
38    Node() = default;
39    ~Node()
40    {
41        if (body)
42            delete body;
43        if (next)
44            delete next;
45    }
46};
47
48// Information about a source file
49struct SourceFile : public RefObject
50{
51    String inputPath;
52    String linePath; ///< The path to this file for #line output
53
54    StringSpan text;
55    Node* node = nullptr;
56    SourceFile() = default;
57    ~SourceFile()
58    {
59        if (text.begin())
60            free((void*)text.begin());
61
62        // To avoid deep recursion in the Node destructor,
63        // we delete the first level of the node tree iteratively.
64        while (node)
65        {
66            Node* next = node->next;
67            node->next = nullptr;
68            delete node;
69            node = next;
70        }
71    }
72};
73
74void addNode(Node**& ioLink, Node::Flavor flavor, char const* spanBegin, char const* spanEnd)
75{
76    Node* node = new Node();
77    node->flavor = flavor;
78    node->span = StringSpan(spanBegin, spanEnd);
79    node->next = nullptr;
80
81    *ioLink = node;
82    ioLink = &node->next;
83}
84
85void addNode(Node**& ioLink, Node::Flavor flavor, Node* body)
86{
87    Node* node = new Node();
88    node->flavor = flavor;
89    node->body = body;
90    node->next = nullptr;
91
92    *ioLink = node;
93    ioLink = &node->next;
94}
95
96bool isAlpha(int c)
97{
98    return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || (c == '_');
99}
100
101void addTextSpan(Node**& ioLink, char const* spanBegin, char const* spanEnd)
102{
103    // Don't add an empty text span.
104    if (spanBegin == spanEnd)
105        return;
106
107    addNode(ioLink, Node::Flavor::text, spanBegin, spanEnd);
108}
109
110void addSpliceSpan(Node**& ioLink, Node* body)
111{
112    addNode(ioLink, Node::Flavor::splice, body);
113}
114
115void addEscapeSpan(Node**& ioLink, Node* body)
116{
117    addNode(ioLink, Node::Flavor::escape, body);
118}
119
120void addEscapeSpan(Node**& ioLink, char const* spanBegin, char const* spanEnd)
121{
122    Node* body = nullptr;
123    Node** link = &body;
124
125    addTextSpan(link, spanBegin, spanEnd);
126
127    return addEscapeSpan(ioLink, body);
128}
129
130bool isIdentifierChar(int c)
131{
132    if (c >= 'a' && c <= 'z')
133        return true;
134    if (c >= 'A' && c <= 'Z')
135        return true;
136    if (c == '_')
137        return true;
138
139    return false;
140}
141
142struct Reader
143{
144    char const* cursor;
145    char const* end;
146};
147
148int peek(Reader const& reader)
149{
150    if (reader.cursor == reader.end)
151        return EOF;
152
153    return *reader.cursor;
154}
155
156int get(Reader& reader)
157{
158    if (reader.cursor == reader.end)
159        return -1;
160
161    return *reader.cursor++;
162}
163
164void handleNewline(Reader& reader, int c)
165{
166    int d = peek(reader);
167    if ((c ^ d) == ('\r' ^ '\n'))
168    {
169        get(reader);
170    }
171}
172
173bool isHorizontalSpace(int c)
174{
175    return (c == ' ') || (c == '\t');
176}
177
178void skipHorizontalSpace(Reader& reader)
179{
180    while (isHorizontalSpace(peek(reader)))
181        get(reader);
182}
183
184void skipOptionalNewline(Reader& reader)
185{
186    switch (peek(reader))
187    {
188    default:
189        break;
190
191    case '\r':
192    case '\n':
193        {
194            int c = get(reader);
195            handleNewline(reader, c);
196        }
197        break;
198    }
199}
200
201typedef unsigned int NodeReadFlags;
202enum
203{
204    kNodeReadFlag_AllowEscape = 1 << 0,
205};
206
207template<bool onlyReadFirstOpenChar>
208Node* readBody(Reader& reader, NodeReadFlags flags, char openChar, int openCount, char closeChar)
209{
210    while (peek(reader) == openChar)
211    {
212        get(reader);
213        openCount++;
214
215        // This case allows parsing `myFunc($((int)val))` correctly, else we parse
216        // body as `int)val`, causing non obvious segfault.
217        if constexpr (onlyReadFirstOpenChar)
218            break;
219    }
220
221    Node* nodes = nullptr;
222    Node** link = &nodes;
223
224    bool atStartOfLine = true;
225    int depth = 0;
226
227    char const* spanBegin = reader.cursor;
228    char const* lineBegin = reader.cursor;
229    for (;;)
230    {
231        int c = get(reader);
232
233        switch (c)
234        {
235        default:
236            atStartOfLine = false;
237            break;
238
239        case EOF:
240            {
241                addTextSpan(link, spanBegin, reader.cursor);
242                return nodes;
243            }
244
245        case '{':
246        case '(':
247            if (c == openChar)
248            {
249                depth++;
250            }
251            atStartOfLine = false;
252            break;
253
254        case ')':
255        case '}':
256            if (c == closeChar)
257            {
258                char const* spanEnd = reader.cursor - 1;
259
260                if (openCount == 1)
261                {
262                    if (depth == 0)
263                    {
264                        // We are at the end of the body.
265                        addTextSpan(link, spanBegin, spanEnd);
266                        return nodes;
267                    }
268
269                    depth--;
270                }
271                else
272                {
273                    // Count how many closing chars are stacked up
274
275                    int closeCount = 1;
276                    while (peek(reader) == closeChar)
277                    {
278                        get(reader);
279                        closeCount++;
280                    }
281
282                    if (closeCount == openCount)
283                    {
284                        // We are at the end of the body.
285                        addTextSpan(link, spanBegin, spanEnd);
286                        return nodes;
287                    }
288                }
289            }
290            atStartOfLine = false;
291            break;
292
293
294        case ' ':
295        case '\t':
296            break;
297
298        case '\r':
299        case '\n':
300            {
301                addTextSpan(link, spanBegin, reader.cursor);
302
303                handleNewline(reader, c);
304
305                lineBegin = reader.cursor;
306                spanBegin = reader.cursor;
307                atStartOfLine = true;
308            }
309            break;
310
311        case '$':
312            {
313                // If this is the start of a splice, then
314                // the end of the preceding raw-text space
315                // will be the byte before `$`
316                char const* spanEnd = reader.cursor - 1;
317
318                if (peek(reader) == '(')
319                {
320                    // This appears to be an expression splice.
321                    //
322                    // We must end the preceding span.
323                    //
324                    addTextSpan(link, spanBegin, spanEnd);
325
326                    Node* body = readBody<true>(reader, 0, '(', 0, ')');
327
328                    addSpliceSpan(link, body);
329
330                    spanBegin = reader.cursor;
331                    atStartOfLine = false;
332                }
333                else if (peek(reader) == '{')
334                {
335                    // This is the start of a block-structured escape, which will
336                    // end at a matching `}`.
337
338                    addTextSpan(link, spanBegin, lineBegin);
339
340                    Node* body = readBody<false>(reader, 0, '{', 0, '}');
341
342                    addEscapeSpan(link, body);
343
344                    spanBegin = reader.cursor;
345                    atStartOfLine = false;
346                }
347                else if (atStartOfLine && peek(reader) == ':')
348                {
349                    // This is a statement escape, which will
350                    // continue to the end of the line.
351                    //
352                    // The spliced text begins *after* the `:`
353                    get(reader);
354                    char const* spliceBegin = reader.cursor;
355
356                    // The preceding text span will end at the
357                    // start of this line.
358                    addTextSpan(link, spanBegin, lineBegin);
359
360                    // Any indentation on this line will be ignored.
361
362                    // Read up to end of line.
363                    for (;;)
364                    {
365                        int c = get(reader);
366                        switch (c)
367                        {
368                        default:
369                            continue;
370
371                        case EOF:
372                            break;
373
374                        case '\r':
375                        case '\n':
376                            handleNewline(reader, c);
377                            break;
378                        }
379
380                        break;
381                    }
382
383                    addEscapeSpan(link, spliceBegin, reader.cursor);
384
385                    spanBegin = reader.cursor;
386                    lineBegin = reader.cursor;
387                }
388                else if (atStartOfLine && isIdentifierChar(peek(reader)))
389                {
390                    // This is a statement splice, which will use a {}-enclosed
391                    // body for the template to generate.
392
393                    // Consume an optional identifier
394                    while (isIdentifierChar(peek(reader)))
395                        get(reader);
396
397                    // Consume optional horizontal space
398                    skipHorizontalSpace(reader);
399
400                    // Consume an optional `()`-enclosed block (strip
401                    // all but the outer-most `()`.
402
403                    // optional space/newline/space before `{`
404                    skipHorizontalSpace(reader);
405                    skipOptionalNewline(reader);
406                    skipHorizontalSpace(reader);
407
408                    throw 99;
409                }
410                else
411                {
412                    // Doesn't seem to be a splice at all, just
413                    // a literal `$` in the output.
414                    atStartOfLine = false;
415                }
416            }
417            break;
418        }
419    }
420}
421
422Node* readInput(char const* inputBegin, char const* inputEnd)
423{
424    Reader reader;
425    reader.cursor = inputBegin;
426    reader.end = inputEnd;
427
428    return readBody<false>(reader, kNodeReadFlag_AllowEscape, -2, 0, -2);
429}
430
431void emitRaw(FILE* stream, char const* begin, char const* end)
432{
433    // We will write the raw text to our output file.
434
435    // TODO: need to output `#line` directives as well
436
437    fputs("sb << \"", stream);
438    for (char const* cc = begin; cc != end; ++cc)
439    {
440        int c = *cc;
441        switch (c)
442        {
443        case '\\':
444            fputs("\\\\", stream);
445            break;
446
447        case '\r':
448            break;
449        case '\t':
450            fputs("\\t", stream);
451            break;
452        case '\"':
453            fputs("\\\"", stream);
454            break;
455        case '\n':
456            fputs("\\n\";\n", stream);
457            fputs("sb << \"", stream);
458            break;
459
460        default:
461            if ((c >= 32) && (c <= 126))
462            {
463                fputc(c, stream);
464            }
465            else
466            {
467                assert(false);
468            }
469        }
470    }
471    fprintf(stream, "\";\n");
472}
473
474void emitCode(FILE* stream, char const* begin, char const* end)
475{
476    for (auto cc = begin; cc != end; ++cc)
477    {
478        if (*cc == '\r')
479            continue;
480
481        fputc(*cc, stream);
482    }
483}
484
485void emit(FILE* stream, char const* text)
486{
487    fprintf(stream, "%s", text);
488}
489
490void emit(FILE* stream, StringSpan const& span)
491{
492    fprintf(stream, "%.*s", int(span.end() - span.begin()), span.begin());
493}
494
495bool isASCIIPrintable(int c)
496{
497    return (c >= 0x20) && (c <= 0x7E);
498}
499
500void emitStringLiteralText(FILE* stream, StringSpan const& span)
501{
502    char const* cursor = span.begin();
503    char const* end = span.end();
504
505    while (cursor != end)
506    {
507        int c = *cursor++;
508        switch (c)
509        {
510        case '\r':
511        case '\n':
512            fprintf(stream, "\\n");
513            break;
514
515        case '\t':
516            fprintf(stream, "\\t");
517            break;
518
519        case ' ':
520            fprintf(stream, " ");
521            break;
522
523        case '"':
524            fprintf(stream, "\\\"");
525            break;
526
527        case '\\':
528            fprintf(stream, "\\\\");
529            break;
530
531        default:
532            if (isASCIIPrintable(c))
533            {
534                fprintf(stream, "%c", c);
535            }
536            else
537            {
538                fprintf(stream, "%03u", c);
539            }
540            break;
541        }
542    }
543}
544
545void emitSimpleText(FILE* stream, StringSpan const& span)
546{
547    UnownedStringSlice content(span), line;
548    while (StringUtil::extractLine(content, line))
549    {
550        // Write the line
551        fwrite(line.begin(), 1, line.getLength(), stream);
552
553        // Specially handle the 'final line', excluding an empty line after \n.
554        // We can detect, as if input ends with 'cr/lf' combination, content.begin == span.end(),
555        // else if content.begin() == nullptr.
556        if (content.begin() == nullptr || content.begin() == span.end())
557        {
558            break;
559        }
560
561        fprintf(stream, "\n");
562    }
563}
564
565void emitCodeNodes(FILE* stream, Node* node)
566{
567    for (auto nn = node; nn; nn = nn->next)
568    {
569        switch (nn->flavor)
570        {
571        case Node::Flavor::text:
572            emitSimpleText(stream, nn->span);
573            emit(stream, "\n");
574            break;
575
576        default:
577            throw "unexpected";
578            break;
579        }
580    }
581}
582
583// Given line starts and a location, find the line number. Returns -1 if not found
584static Index _findLineIndex(const List<UnownedStringSlice>& lineBreaks, const char* location)
585{
586    if (location == nullptr)
587    {
588        return -1;
589    }
590
591    // Use a binary chop to find the associated line
592    Index lo = 0;
593    Index hi = lineBreaks.getCount();
594
595    while (lo + 1 < hi)
596    {
597        const auto mid = (hi + lo) >> 1;
598        const auto midOffset = lineBreaks[mid].begin();
599        if (midOffset <= location)
600        {
601            lo = mid;
602        }
603        else
604        {
605            hi = mid;
606        }
607    }
608
609    return lo;
610}
611
612void emitTemplateNodes(SourceFile* sourceFile, FILE* stream, Node* node)
613{
614    // Work out
615    List<UnownedStringSlice> lineBreaks;
616    StringUtil::calcLines(sourceFile->text, lineBreaks);
617
618    Node* prev = nullptr;
619    for (auto nn = node; nn; prev = nn, nn = nn->next)
620    {
621        // If we transition from escape to text, insert line number directive
622        bool enable = true;
623        if (enable && prev && prev->flavor == Node::Flavor::escape &&
624            nn->flavor == Node::Flavor::text)
625        {
626            // Find the line
627            Index lineIndex = _findLineIndex(lineBreaks, nn->span.begin());
628            // If found, output the directive
629            if (lineIndex >= 0)
630            {
631                StringBuilder buf;
632                buf << "SLANG_RAW(\"#line " << (lineIndex + 1) << " \\\"" << sourceFile->linePath
633                    << "\\\"\")\n";
634
635                emit(stream, buf.getUnownedSlice());
636            }
637        }
638
639        switch (nn->flavor)
640        {
641        case Node::Flavor::text:
642            emit(stream, "SLANG_RAW(\"");
643            emitStringLiteralText(stream, nn->span);
644            emit(stream, "\")\n");
645            break;
646
647        case Node::Flavor::splice:
648            emit(stream, "SLANG_SPLICE(");
649            emitCodeNodes(stream, nn->body);
650            emit(stream, ")\n");
651            break;
652
653        case Node::Flavor::escape:
654            emitCodeNodes(stream, nn->body);
655            break;
656        }
657    }
658}
659
660void usage(char const* appName)
661{
662    fprintf(stderr, "usage: %s [FILE]... [--target-directory FILE]\n", appName);
663}
664
665SlangResult readAllText(char const* fileName, String& outString)
666{
667    FILE* f;
668    fopen_s(&f, fileName, "rb");
669    if (!f)
670    {
671        outString = "";
672        return SLANG_FAIL;
673    }
674    else
675    {
676        fseek(f, 0, SEEK_END);
677        auto size = ftell(f);
678
679        StringRepresentation* stringRep =
680            StringRepresentation::createWithCapacityAndLength(size, size);
681        outString = String(stringRep);
682
683        char* buffer = stringRep->getData();
684
685        // Seems unnecessary
686        // memset(buffer, 0, size);
687
688        fseek(f, 0, SEEK_SET);
689        size_t readCount = fread(buffer, sizeof(char), size, f);
690        fclose(f);
691
692        return (readCount == size) ? SLANG_OK : SLANG_FAIL;
693    }
694}
695
696void writeAllText(char const* srcFileName, char const* fileName, const char* content)
697{
698    FILE* f = nullptr;
699    fopen_s(&f, fileName, "wb");
700    if (!f)
701    {
702        printf("%s(0): error G0001: cannot write file %s\n", srcFileName, fileName);
703    }
704    else
705    {
706        fwrite(content, 1, strlen(content), f);
707        fclose(f);
708    }
709}
710
711#define PARSE_HANDLER(NAME) Node* NAME(StringSpan const& text)
712
713typedef PARSE_HANDLER((*ParseHandler));
714
715PARSE_HANDLER(parseTemplateFile)
716{
717    // Read a template node!
718    return readInput(text.begin(), text.end());
719}
720
721PARSE_HANDLER(parseCxxFile)
722{
723    // TODO: "scrape" the source file for metadata
724    return nullptr;
725}
726
727PARSE_HANDLER(parseUnknownFile)
728{
729    // Don't process files we don't know how to handle.
730    return nullptr;
731}
732
733
734Node* parseSourceFile(SourceFile* file)
735{
736    auto path = file->inputPath;
737    auto text = file->text;
738
739    static const struct
740    {
741        char const* extension;
742        ParseHandler handler;
743    } kHandlers[] = {
744        {".meta.slang", &parseTemplateFile},
745        {".meta.cpp", &parseTemplateFile},
746        {".cpp", &parseCxxFile},
747        {"", &parseUnknownFile},
748    };
749
750    for (auto hh : kHandlers)
751    {
752        if (path.endsWith(hh.extension))
753        {
754            return hh.handler(text);
755        }
756    }
757
758    return nullptr;
759}
760
761
762SourceFile* parseSourceFile(const String& path)
763{
764    FILE* inputStream;
765    fopen_s(&inputStream, path.getBuffer(), "rb");
766    if (!inputStream)
767    {
768        fprintf(stderr, "unable to read input file: %s\n", path.getBuffer());
769        return nullptr;
770    }
771    fseek(inputStream, 0, SEEK_END);
772    size_t inputSize = ftell(inputStream);
773    fseek(inputStream, 0, SEEK_SET);
774
775    char* input = (char*)malloc(inputSize + 1);
776    if (fread(input, inputSize, 1, inputStream) != 1)
777    {
778        fprintf(stderr, "unable to read input file: %s\n", path.getBuffer());
779        return nullptr;
780    }
781    input[inputSize] = 0;
782
783    char const* inputEnd = input + inputSize;
784    StringSpan span = StringSpan(input, inputEnd);
785
786    SourceFile* sourceFile = new SourceFile();
787
788    sourceFile->inputPath = path;
789
790    // We use the fileName as the line path, as the path as passed to the command could contain a
791    // complicated depending on the project location.
792    sourceFile->linePath = Path::getFileName(path);
793
794    sourceFile->text = span;
795
796    Node* node = parseSourceFile(sourceFile);
797
798    sourceFile->node = node;
799
800    fclose(inputStream);
801    return sourceFile;
802}
803
804List<RefPtr<SourceFile>> gSourceFiles;
805
806int main(int argc, const char* const* argv)
807{
808    // Parse command-line arguments.
809    List<String> inputPaths;
810    String outputDir;
811    char const* appName = "slang-generate";
812
813    {
814        const char* const* argCursor = argv;
815        const char* const* argEnd = argv + argc;
816        // Copy the app name
817        if (argCursor != argEnd)
818        {
819            appName = *argCursor++;
820        }
821        // Parse arguments
822        for (; argCursor != argEnd; ++argCursor)
823        {
824            const auto arg = UnownedStringSlice(*argCursor);
825            if (arg == "--target-directory")
826            {
827                argCursor++;
828                if (argCursor == argEnd)
829                {
830                    usage(appName);
831                    fprintf(stderr, "--target-directory expects an argument\n");
832                    exit(1);
833                }
834                outputDir = Path::simplify(UnownedStringSlice(*argCursor));
835            }
836            else
837            {
838                // We simplify here because doing so also means paths separators are set to /
839                // and that makes path emitting work correctly
840                inputPaths.add(Path::simplify(arg));
841            }
842        }
843    }
844
845    if (inputPaths.getCount() == 0)
846    {
847        usage(appName);
848        exit(1);
849    }
850
851    // Read each input file and process it according
852    // to the type of treatment it requires.
853    for (auto& inputPath : inputPaths)
854    {
855        SourceFile* sourceFile = parseSourceFile(inputPath);
856        gSourceFiles.add(sourceFile);
857    }
858
859    for (auto sourceFile : gSourceFiles)
860    {
861        if (!sourceFile)
862        {
863            fprintf(stderr, "failed to parse source files\n");
864            exit(1);
865        }
866    }
867
868    // Once all inputs have been read, we can start
869    // to produce output files by expanding templates.
870    for (auto sourceFile : gSourceFiles)
871    {
872        auto inputPath = sourceFile->inputPath;
873        auto node = sourceFile->node;
874
875        // write output to a temporary file first
876        StringBuilder outputPath;
877        outputPath << inputPath << ".temp.h";
878
879        FILE* outputStream;
880        fopen_s(&outputStream, outputPath.getBuffer(), "w");
881        if (!outputStream)
882        {
883            fprintf(stderr, "unable to open file for writing: %s.\n", outputPath.getBuffer());
884            exit(1);
885        }
886
887        emitTemplateNodes(sourceFile, outputStream, node);
888
889        fclose(outputStream);
890
891        // update final output only when content has changed
892        StringBuilder outputPathFinal;
893        if (outputDir.getLength())
894            outputPathFinal << outputDir << "/" << Slang::Path::getFileName(inputPath) << ".h";
895        else
896            outputPathFinal << inputPath << ".h";
897
898        String allTextOld, allTextNew;
899        readAllText(outputPathFinal.getBuffer(), allTextOld);
900        readAllText(outputPath.getBuffer(), allTextNew);
901        if (allTextOld != allTextNew)
902        {
903            writeAllText(
904                inputPath.getBuffer(),
905                outputPathFinal.getBuffer(),
906                allTextNew.getBuffer());
907        }
908        remove(outputPath.getBuffer());
909    }
910
911    return 0;
912}