yum-mirror/slang

Making it easier to work with shaders

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

Ellie Hermaszewskaformatf65d756bf

master
17.7 KiB578 linesraw
1#include "slang-json-source-map-util.h"
2
3#include "../core/slang-blob.h"
4#include "../core/slang-string-util.h"
5#include "slang-com-helper.h"
6#include "slang-json-native.h"
7
8namespace Slang
9{
10
11/*
12Support for source maps. Source maps provide a standardized mechanism to associate a location in one
13output file with another.
14
15* [Source Map
16Proposal](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1)
17* [Chrome Source Map post](https://developer.chrome.com/blog/sourcemaps/)
18* [Base64 VLQs in Source
19Maps](https://www.lucidchart.com/techblog/2019/08/22/decode-encoding-base64-vlqs-source-maps/)
20
21Example...
22
23{
24"version" : 3,
25"file": "out.js",
26"sourceRoot": "",
27"sources": ["foo.js", "bar.js"],
28"sourcesContent": [null, null],
29"names": ["src", "maps", "are", "fun"],
30"mappings": "A,AAAB;;ABCDE;"
31}
32*/
33
34namespace
35{ // anonymous
36
37struct JSONSourceMap
38{
39    /// File version (always the first entry in the object) and must be a positive integer.
40    int32_t version = 3;
41    /// An optional name of the generated code that this source map is associated with.
42    String file;
43    /// An optional source root, useful for relocating source files on a server or removing repeated
44    /// values in the “sources” entry.  This value is prepended to the individual entries in the
45    /// “source” field.
46    String sourceRoot;
47    /// A list of original sources used by the “mappings” entry.
48    List<UnownedStringSlice> sources;
49    /// An optional list of source content, useful when the “source” can’t be hosted. The contents
50    /// are listed in the same order as the sources in line 5. “null” may be used if some original
51    /// sources should be retrieved by name. Because could be a string or nullptr, we use JSONValue
52    /// to hold value.
53    List<JSONValue> sourcesContent;
54    /// A list of symbol names used by the “mappings” entry.
55    List<UnownedStringSlice> names;
56    /// A string with the encoded mapping data.
57    UnownedStringSlice mappings;
58
59    static const StructRttiInfo g_rttiInfo;
60};
61
62} // namespace
63
64static const StructRttiInfo _makeJSONSourceMap_Rtti()
65{
66    JSONSourceMap obj;
67
68    StructRttiBuilder builder(&obj, "SourceMap", nullptr);
69
70    builder.addField("version", &obj.version);
71    builder.addField("file", &obj.file);
72    builder.addField("sourceRoot", &obj.sourceRoot, StructRttiInfo::Flag::Optional);
73    builder.addField("sources", &obj.sources);
74    builder.addField("sourcesContent", &obj.sourcesContent, StructRttiInfo::Flag::Optional);
75    builder.addField("names", &obj.names, StructRttiInfo::Flag::Optional);
76    builder.addField("mappings", &obj.mappings);
77
78    return builder.make();
79}
80/* static */ const StructRttiInfo JSONSourceMap::g_rttiInfo = _makeJSONSourceMap_Rtti();
81
82// Encode a 6 bit value to VLQ encoding
83static const unsigned char g_vlqEncodeTable[] =
84    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
85
86struct VlqDecodeTable
87{
88    VlqDecodeTable()
89    {
90        ::memset(map, -1, sizeof(map));
91        for (Index i = 0; i < SLANG_COUNT_OF(g_vlqEncodeTable); ++i)
92        {
93            map[g_vlqEncodeTable[i]] = int8_t(i);
94        }
95    }
96    /// Returns a *negative* value if invalid
97    SLANG_FORCE_INLINE int8_t operator[](unsigned char c) const
98    {
99        return (c & ~char(0x7f)) ? -1 : map[c];
100    }
101
102    int8_t map[128];
103};
104
105static const VlqDecodeTable g_vlqDecodeTable;
106
107/*
108https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1#
109The VLQ is a Base64 value, where the most significant bit (the 6th bit) is used as the continuation
110bit, and the “digits” are encoded into the string least significant first, and where the least
111significant bit of the first digit is used as the sign bit. */
112
113static SlangResult _decode(UnownedStringSlice& ioEncoded, Index& out)
114{
115    Index v = 0;
116
117    const char* cur = ioEncoded.begin();
118    const char* end = ioEncoded.end();
119
120    {
121        Index shift = 0;
122        Index decodeValue = 0;
123        do
124        {
125            // Must have a char to decode
126            if (cur >= end)
127            {
128                return SLANG_FAIL;
129            }
130
131            decodeValue = g_vlqDecodeTable[*cur++];
132            if (decodeValue < 0)
133            {
134                return SLANG_FAIL;
135            }
136
137            v += (decodeValue & 0x1f) << shift;
138
139            shift += 5;
140        } while (decodeValue & 0x20);
141    }
142
143    // Save out the remaining part
144    ioEncoded = UnownedStringSlice(cur, end);
145
146    // Handle negating
147    out = (v & 1) ? -(v >> 1) : (v >> 1);
148    return SLANG_OK;
149}
150
151void _encode(Index v, StringBuilder& out)
152{
153    // Double to free up low bit to hold the sign
154    v += v;
155
156    // We want to make v always positive to encode
157    // we use the last bit to indicate negativity
158    v = (v < 0) ? (1 - v) : v;
159
160    // We'll use a simple buffer, so as to not have to constantly update he StringBuffer
161    char dst[8];
162    char* cur = dst;
163
164    do
165    {
166        const Index nextV = v >> 5;
167        const Index encodeValue = (v & 0x1f) + (nextV ? 0x20 : 0);
168
169        // Encode 5 bits, plus continuation bit
170        char c = g_vlqEncodeTable[encodeValue];
171
172        // Save the char
173        *cur++ = c;
174
175        v = nextV;
176    } while (v);
177
178    out.append(dst, cur);
179}
180
181/* static */ SlangResult JSONSourceMapUtil::decode(
182    JSONContainer* container,
183    JSONValue root,
184    DiagnosticSink* sink,
185    SourceMap& outSourceMap)
186{
187    outSourceMap.clear();
188
189    // Let's try and decode the JSON into native types to make this easier...
190    RttiTypeFuncsMap typeMap = JSONNativeUtil::getTypeFuncsMap();
191
192    // Convert to native
193    JSONSourceMap native;
194    {
195        JSONToNativeConverter converter(container, &typeMap, sink);
196
197        // Convert to the native type
198        SLANG_RETURN_ON_FAIL(converter.convert(root, GetRttiInfo<JSONSourceMap>::get(), &native));
199    }
200
201    outSourceMap.m_file = native.file;
202    outSourceMap.m_sourceRoot = native.sourceRoot;
203
204    const Count sourcesCount = native.sources.getCount();
205
206    // These should all be unique, but for simplicity, we build a table
207    outSourceMap.m_sources.setCount(sourcesCount);
208    for (Index i = 0; i < sourcesCount; ++i)
209    {
210        outSourceMap.m_sources[i] = outSourceMap.m_slicePool.add(native.sources[i]);
211    }
212
213    Count sourcesContentCount = native.sourcesContent.getCount();
214    sourcesContentCount = std::min(sourcesContentCount, sourcesCount);
215
216    outSourceMap.m_sourcesContent.setCount(sourcesContentCount);
217    for (auto& cur : outSourceMap.m_sourcesContent)
218    {
219        cur = StringSlicePool::kNullHandle;
220    }
221
222    // Special case sourcesContent, because needs to be able to handle null or string
223    for (Index i = 0; i < sourcesContentCount; ++i)
224    {
225        auto value = native.sourcesContent[i];
226
227        if (value.type != JSONValue::Type::Null)
228        {
229            if (value.getKind() == JSONValue::Kind::String)
230            {
231                auto stringValue = container->getString(value);
232                outSourceMap.m_sourcesContent[i] = outSourceMap.m_slicePool.add(stringValue);
233            }
234        }
235    }
236
237    // Copy over the names
238    {
239        const auto namesCount = native.names.getCount();
240        outSourceMap.m_names.setCount(namesCount);
241
242        for (Index i = 0; i < namesCount; ++i)
243        {
244            outSourceMap.m_names[i] = outSourceMap.m_slicePool.add(native.names[i]);
245        }
246    }
247
248    List<UnownedStringSlice> lines;
249    StringUtil::split(native.mappings, ';', lines);
250
251    List<UnownedStringSlice> segments;
252
253    // Index into sources
254    Index sourceFileIndex = 0;
255
256    Index sourceLine = 0;
257    Index sourceColumn = 0;
258    Index nameIndex = 0;
259
260    const Count linesCount = lines.getCount();
261
262    outSourceMap.m_lineStarts.setCount(linesCount + 1);
263
264    for (Index generatedLine = 0; generatedLine < linesCount; ++generatedLine)
265    {
266        const auto line = lines[generatedLine];
267
268        outSourceMap.m_lineStarts[generatedLine] = outSourceMap.m_lineEntries.getCount();
269
270        // If it's empty move to next line
271        if (line.getLength() == 0)
272        {
273            continue;
274        }
275
276        // Split the line into segments
277        segments.clear();
278        StringUtil::split(line, ',', segments);
279
280        Index generatedColumn = 0;
281
282        for (auto segment : segments)
283        {
284            Index colDelta;
285            SLANG_RETURN_ON_FAIL(_decode(segment, colDelta));
286
287            generatedColumn += colDelta;
288            SLANG_ASSERT(generatedColumn >= 0);
289
290            // It can be 4 or 5 parts
291            if (segment.getLength())
292            {
293                /* If present, an zero-based index into the "sources" list. This field is a base 64
294                   VLQ relative to the previous occurrence of this field, unless this is the first
295                   occurrence of this field, in which case the whole value is represented. If
296                   present, the zero-based starting line in the original source represented. This
297                   field is a base 64 VLQ relative to the previous occurrence of this field, unless
298                   this is the first occurrence of this field, in which case the whole value is
299                   represented. Always present if there is a source field. If present, the
300                   zero-based starting column of the line in the source represented. This field is a
301                   base 64 VLQ relative to the previous occurrence of this field, unless this is the
302                   first occurrence of this field, in which case the whole value is represented.
303                   Always present if there is a source field.
304                */
305
306                Index sourceFileDelta;
307                Index sourceLineDelta;
308                Index sourceColumnDelta;
309
310                SLANG_RETURN_ON_FAIL(_decode(segment, sourceFileDelta));
311                SLANG_RETURN_ON_FAIL(_decode(segment, sourceLineDelta));
312                SLANG_RETURN_ON_FAIL(_decode(segment, sourceColumnDelta));
313
314                sourceFileIndex += sourceFileDelta;
315                sourceLine += sourceLineDelta;
316                sourceColumn += sourceColumnDelta;
317
318                SLANG_ASSERT(sourceFileIndex >= 0);
319                SLANG_ASSERT(sourceLine >= 0);
320                SLANG_ASSERT(sourceColumn >= 0);
321
322                // 5 parts
323                if (segment.getLength() > 0)
324                {
325                    /* If present, the zero - based index into the "names" list associated with this
326                    segment. This field is a base 64 VLQ relative to the previous occurrence of this
327                    field, unless this is the first occurrence of this field, in which case the
328                    whole value is represented.
329                    */
330
331                    Index nameDelta;
332                    SLANG_RETURN_ON_FAIL(_decode(segment, nameDelta));
333
334                    nameIndex += nameDelta;
335                    SLANG_ASSERT(nameIndex >= 0);
336                }
337            }
338
339            SourceMap::Entry entry;
340            entry.generatedColumn = generatedColumn;
341            entry.sourceColumn = sourceColumn;
342            entry.sourceLine = sourceLine;
343            entry.sourceFileIndex = sourceFileIndex;
344            entry.nameIndex = nameIndex;
345
346            outSourceMap.m_lineEntries.add(entry);
347        }
348    }
349
350    // Mark the end
351    outSourceMap.m_lineStarts[linesCount] = outSourceMap.m_lineEntries.getCount();
352
353    return SLANG_OK;
354}
355
356SlangResult JSONSourceMapUtil::encode(
357    const SourceMap& sourceMap,
358    JSONContainer* container,
359    DiagnosticSink* sink,
360    JSONValue& outValue)
361{
362    // Convert to native
363    JSONSourceMap native;
364
365    native.file = sourceMap.m_file;
366    native.sourceRoot = sourceMap.m_sourceRoot;
367
368    // Copy over the sources
369    {
370        const auto count = sourceMap.m_sources.getCount();
371        native.sources.setCount(count);
372        for (Index i = 0; i < count; ++i)
373        {
374            native.sources[i] = sourceMap.m_slicePool.getSlice(sourceMap.m_sources[i]);
375        }
376    }
377
378    // Copy out the sourcesContent, care is needed around handling null
379    {
380        const auto count = sourceMap.m_sourcesContent.getCount();
381        native.sourcesContent.setCount(count);
382        for (Index i = 0; i < count; ++i)
383        {
384            const auto srcValue = sourceMap.m_sourcesContent[i];
385
386            const JSONValue dstValue =
387                (srcValue == StringSlicePool::kNullHandle)
388                    ? native.sourcesContent[i] = JSONValue::makeNull()
389                    : container->createString(sourceMap.m_slicePool.getSlice(srcValue));
390
391            native.sourcesContent[i] = dstValue;
392        }
393    }
394
395    // Copy out the names
396    {
397        const auto count = sourceMap.m_names.getCount();
398        native.names.setCount(count);
399        for (Index i = 0; i < count; ++i)
400        {
401            native.names[i] = sourceMap.m_slicePool.getSlice(sourceMap.m_names[i]);
402        }
403    }
404
405    StringBuilder mappings;
406
407    // Do the encoding!
408    {
409        const Count linesCount = sourceMap.getGeneratedLineCount();
410
411        Index sourceFileIndex = 0;
412
413        Index sourceLine = 0;
414        Index sourceColumn = 0;
415        Index nameIndex = 0;
416
417        for (Index i = 0; i < linesCount; ++i)
418        {
419            // Add the semicolon to start the line
420            if (i > 0)
421            {
422                mappings.appendChar(';');
423            }
424
425            const auto entries = sourceMap.getEntriesForLine(i);
426            const auto entriesCount = entries.getCount();
427
428            if (entriesCount == 0)
429            {
430                continue;
431            }
432
433            // We reset the generated column index at the start of each new generated line
434            Index generatedColumn = 0;
435
436            for (Index j = 0; j < entriesCount; ++j)
437            {
438                auto entry = entries[j];
439
440                if (j > 0)
441                {
442                    mappings.appendChar(',');
443                }
444
445                Index generatedDelta = entry.generatedColumn - generatedColumn;
446                generatedColumn = entry.generatedColumn;
447
448                _encode(generatedDelta, mappings);
449
450                // See if there any other deltas we need to handle
451                const Index sourceFileDelta = entry.sourceFileIndex - sourceFileIndex;
452                const Index sourceLineDelta = entry.sourceLine - sourceLine;
453                const Index sourceColumnDelta = entry.sourceColumn - sourceColumn;
454                const Index nameIndexDelta = entry.nameIndex - nameIndex;
455
456                if (sourceFileDelta || sourceLineDelta || sourceColumnDelta || nameIndex)
457                {
458                    // Okay we have to encode all these deltae
459                    _encode(sourceFileDelta, mappings);
460                    _encode(sourceLineDelta, mappings);
461                    _encode(sourceColumnDelta, mappings);
462
463                    // Update these values
464                    sourceFileIndex = entry.sourceFileIndex;
465                    sourceLine = entry.sourceLine;
466                    sourceColumn = entry.sourceColumn;
467
468                    if (nameIndexDelta)
469                    {
470                        _encode(nameIndexDelta, mappings);
471                        nameIndex = entry.nameIndex;
472                    }
473                }
474            }
475        }
476    }
477
478    // Set the mappings
479    native.mappings = mappings.getUnownedSlice();
480
481    // Write it out
482    {
483        RttiTypeFuncsMap typeMap = JSONNativeUtil::getTypeFuncsMap();
484
485        NativeToJSONConverter converter(container, &typeMap, sink);
486        SLANG_RETURN_ON_FAIL(
487            converter.convert(GetRttiInfo<JSONSourceMap>::get(), &native, outValue));
488    }
489
490    return SLANG_OK;
491}
492
493/* static */ SlangResult JSONSourceMapUtil::read(ISlangBlob* blob, SourceMap& outSourceMap)
494{
495    return read(blob, nullptr, outSourceMap);
496}
497
498SlangResult JSONSourceMapUtil::read(
499    ISlangBlob* blob,
500    DiagnosticSink* parentSink,
501    SourceMap& outSourceMap)
502{
503    outSourceMap.clear();
504
505    SourceManager sourceManager;
506    sourceManager.initialize(nullptr, nullptr);
507    DiagnosticSink sink(&sourceManager, nullptr);
508
509    sink.setParentSink(parentSink);
510
511    RefPtr<JSONContainer> container = new JSONContainer(&sourceManager);
512
513    JSONValue rootValue;
514    {
515        // Now need to parse as JSON
516        SourceFile* sourceFile =
517            sourceManager.createSourceFileWithBlob(PathInfo::makeUnknown(), blob);
518        SourceView* sourceView = sourceManager.createSourceView(sourceFile, nullptr, SourceLoc());
519
520        JSONLexer lexer;
521        lexer.init(sourceView, &sink);
522
523        JSONBuilder builder(container);
524
525        JSONParser parser;
526        SLANG_RETURN_ON_FAIL(parser.parse(&lexer, sourceView, &builder, &sink));
527
528        rootValue = builder.getRootValue();
529    }
530
531    SLANG_RETURN_ON_FAIL(decode(container, rootValue, &sink, outSourceMap));
532
533    return SLANG_OK;
534}
535
536
537/* static */ SlangResult JSONSourceMapUtil::write(
538    const SourceMap& sourceMap,
539    ComPtr<ISlangBlob>& outBlob)
540{
541    SourceManager sourceMapSourceManager;
542    sourceMapSourceManager.initialize(nullptr, nullptr);
543
544    // Create a sink
545    DiagnosticSink sourceMapSink(&sourceMapSourceManager, nullptr);
546
547    SLANG_RETURN_ON_FAIL(write(sourceMap, &sourceMapSink, outBlob));
548    return SLANG_OK;
549}
550
551/* static */ SlangResult JSONSourceMapUtil::write(
552    const SourceMap& sourceMap,
553    DiagnosticSink* sink,
554    ComPtr<ISlangBlob>& outBlob)
555{
556    auto sourceManager = sink->getSourceManager();
557
558    // Write it out
559    String json;
560    {
561        RefPtr<JSONContainer> jsonContainer(new JSONContainer(sourceManager));
562
563        JSONValue jsonValue;
564
565        SLANG_RETURN_ON_FAIL(JSONSourceMapUtil::encode(sourceMap, jsonContainer, sink, jsonValue));
566
567        // Convert into a string
568        JSONWriter writer(JSONWriter::IndentationStyle::Allman);
569        jsonContainer->traverseRecursively(jsonValue, &writer);
570
571        json = writer.getBuilder();
572    }
573
574    outBlob = StringBlob::moveCreate(json);
575    return SLANG_OK;
576}
577
578} // namespace Slang