From 347fc8a57dcfd9361b05f271a7f2badc929500cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Sinan=20A=C4=9Facan?= Date: Tue, 1 Oct 2024 23:39:34 +0200 Subject: [PATCH] Source Maps: Support 5 segment mappings (#6795) Support 5-segment source mappings, which add a name. Reference: https://github.com/tc39/source-map/blob/main/source-map-rev3.md#proposed-format --- src/ir/module-utils.cpp | 61 +++++++++++++++++-- src/ir/module-utils.h | 21 ++++--- src/parser/contexts.h | 27 ++++++++- src/passes/Print.cpp | 10 ++- src/wasm-binary.h | 1 + src/wasm.h | 18 +++--- src/wasm/wasm-binary.cpp | 74 +++++++++++++++++++---- src/wasm/wasm.cpp | 5 +- test/lit/merge/sourcemap.wat | 22 +++---- test/lit/merge/sourcemap.wat.second | 6 +- test/lit/metadce/sourcemap.wat | 14 ++--- test/lit/passes/inlining_source-maps.wast | 36 +++++++++++ test/lit/source-map.wast | 17 ++++++ 13 files changed, 253 insertions(+), 59 deletions(-) create mode 100644 test/lit/passes/inlining_source-maps.wast diff --git a/src/ir/module-utils.cpp b/src/ir/module-utils.cpp index 490955866b0..2f26cfa779c 100644 --- a/src/ir/module-utils.cpp +++ b/src/ir/module-utils.cpp @@ -38,15 +38,35 @@ static void updateLocationSet(std::set& locations, std::swap(locations, updatedLocations); } +// Update the symbol name indices when moving a set of debug locations from one +// module to another. +static void updateSymbolSet(std::set& locations, + std::vector& symbolIndexMap) { + std::set updatedLocations; + + for (auto iter : locations) { + if (iter.symbolNameIndex) { + iter.symbolNameIndex = symbolIndexMap[*iter.symbolNameIndex]; + } + updatedLocations.insert(iter); + } + locations.clear(); + std::swap(locations, updatedLocations); +} + // Copies a function into a module. If newName is provided it is used as the // name of the function (otherwise the original name is copied). If fileIndexMap // is specified, it is used to rename source map filename indices when copying +// the function from one module to another one. If symbolNameIndexMap is +// specified, it is used to rename source map symbol name indices when copying // the function from one module to another one. Function* copyFunction(Function* func, Module& out, Name newName, - std::optional> fileIndexMap) { - auto ret = copyFunctionWithoutAdd(func, out, newName, fileIndexMap); + std::optional> fileIndexMap, + std::optional> symbolNameIndexMap) { + auto ret = copyFunctionWithoutAdd( + func, out, newName, fileIndexMap, symbolNameIndexMap); return out.addFunction(std::move(ret)); } @@ -54,7 +74,8 @@ std::unique_ptr copyFunctionWithoutAdd(Function* func, Module& out, Name newName, - std::optional> fileIndexMap) { + std::optional> fileIndexMap, + std::optional> symbolNameIndexMap) { auto ret = std::make_unique(); ret->name = newName.is() ? newName : func->name; ret->hasExplicitName = func->hasExplicitName; @@ -76,6 +97,18 @@ copyFunctionWithoutAdd(Function* func, updateLocationSet(ret->prologLocation, *fileIndexMap); updateLocationSet(ret->epilogLocation, *fileIndexMap); } + if (symbolNameIndexMap) { + for (auto& iter : ret->debugLocations) { + if (iter.second) { + if (iter.second->symbolNameIndex.has_value()) { + iter.second->symbolNameIndex = + (*symbolNameIndexMap)[*(iter.second->symbolNameIndex)]; + } + } + updateSymbolSet(ret->prologLocation, *symbolNameIndexMap); + updateSymbolSet(ret->epilogLocation, *symbolNameIndexMap); + } + } ret->module = func->module; ret->base = func->base; ret->noFullInline = func->noFullInline; @@ -199,8 +232,27 @@ void copyModuleItems(const Module& in, Module& out) { } } + std::optional> symbolNameIndexMap; + if (!in.debugInfoSymbolNames.empty()) { + std::unordered_map debugInfoSymbolNameIndices; + for (Index i = 0; i < out.debugInfoSymbolNames.size(); i++) { + debugInfoSymbolNameIndices[out.debugInfoSymbolNames[i]] = i; + } + symbolNameIndexMap.emplace(); + for (Index i = 0; i < in.debugInfoSymbolNames.size(); i++) { + std::string file = in.debugInfoSymbolNames[i]; + auto iter = debugInfoSymbolNameIndices.find(file); + if (iter == debugInfoSymbolNameIndices.end()) { + Index index = out.debugInfoSymbolNames.size(); + out.debugInfoSymbolNames.push_back(file); + debugInfoSymbolNameIndices[file] = index; + } + symbolNameIndexMap->push_back(debugInfoSymbolNameIndices[file]); + } + } + for (auto& curr : in.functions) { - copyFunction(curr.get(), out, Name(), fileIndexMap); + copyFunction(curr.get(), out, Name(), fileIndexMap, symbolNameIndexMap); } for (auto& curr : in.globals) { copyGlobal(curr.get(), out); @@ -241,6 +293,7 @@ void copyModule(const Module& in, Module& out) { out.start = in.start; out.customSections = in.customSections; out.debugInfoFileNames = in.debugInfoFileNames; + out.debugInfoSymbolNames = in.debugInfoSymbolNames; out.features = in.features; } diff --git a/src/ir/module-utils.h b/src/ir/module-utils.h index 46e52416538..bb8b6ae439d 100644 --- a/src/ir/module-utils.h +++ b/src/ir/module-utils.h @@ -25,21 +25,24 @@ namespace wasm::ModuleUtils { // Copies a function into a module. If newName is provided it is used as the -// name of the function (otherwise the original name is copied). If fileIndexMap -// is specified, it is used to rename source map filename indices when copying -// the function from one module to another one. -Function* -copyFunction(Function* func, - Module& out, - Name newName = Name(), - std::optional> fileIndexMap = std::nullopt); +// name of the function (otherwise the original name is copied). When specified, +// fileIndexMap and symbolNameIndexMap are used to rename source map filename +// and symbol name indices when copying the function from one module to another +// one. +Function* copyFunction( + Function* func, + Module& out, + Name newName = Name(), + std::optional> fileIndexMap = std::nullopt, + std::optional> symbolNameIndexMap = std::nullopt); // As above, but does not add the copy to the module. std::unique_ptr copyFunctionWithoutAdd( Function* func, Module& out, Name newName = Name(), - std::optional> fileIndexMap = std::nullopt); + std::optional> fileIndexMap = std::nullopt, + std::optional> symbolNameIndexMap = std::nullopt); Global* copyGlobal(Global* global, Module& out); diff --git a/src/parser/contexts.h b/src/parser/contexts.h index 387cb146eef..b0cd1458bb6 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -1398,6 +1398,7 @@ struct ParseDefsCtx : TypeParserCtx { typeNames; const std::unordered_map& implicitElemIndices; + std::unordered_map debugSymbolNameIndices; std::unordered_map debugFileIndices; // The index of the current module element. @@ -1777,12 +1778,32 @@ struct ParseDefsCtx : TypeParserCtx { } contents = contents.substr(lineSize + 1); - lexer = Lexer(contents); + auto colSize = contents.find(':'); + if (colSize == contents.npos) { + colSize = contents.size(); + if (colSize == 0) { + return; + } + } + lexer = Lexer(contents.substr(0, colSize)); auto col = lexer.takeU32(); - if (!col || !lexer.empty()) { + if (!col) { return; } + std::optional symbolNameIndex; + if (colSize != contents.size()) { + contents = contents.substr(colSize + 1); + auto symbolName = contents; + auto [it, inserted] = debugSymbolNameIndices.insert( + {symbolName, debugSymbolNameIndices.size()}); + if (inserted) { + assert(wasm.debugInfoSymbolNames.size() == it->second); + wasm.debugInfoSymbolNames.push_back(std::string(symbolName)); + } + symbolNameIndex = it->second; + } + // TODO: If we ever parallelize the parse, access to // `wasm.debugInfoFileNames` will have to be protected by a lock. auto [it, inserted] = @@ -1792,7 +1813,7 @@ struct ParseDefsCtx : TypeParserCtx { wasm.debugInfoFileNames.push_back(std::string(file)); } irBuilder.setDebugLocation( - Function::DebugLocation({it->second, *line, *col})); + Function::DebugLocation({it->second, *line, *col, symbolNameIndex})); } Result<> makeBlock(Index pos, diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 0533b97865c..106beac39be 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2516,7 +2516,15 @@ void PrintSExpression::printDebugLocation( } else { auto fileName = currModule->debugInfoFileNames[location->fileIndex]; o << ";;@ " << fileName << ":" << location->lineNumber << ":" - << location->columnNumber << '\n'; + << location->columnNumber; + + if (location->symbolNameIndex) { + auto symbolName = + currModule->debugInfoSymbolNames[*(location->symbolNameIndex)]; + o << ":" << symbolName; + } + + o << '\n'; } doIndent(o, indent); } diff --git a/src/wasm-binary.h b/src/wasm-binary.h index 35f13195260..3c59ed3aacb 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1674,6 +1674,7 @@ class WasmBinaryReader { // Debug information reading helpers void setDebugLocations(std::istream* sourceMap_) { sourceMap = sourceMap_; } std::unordered_map debugInfoFileIndices; + std::unordered_map debugInfoSymbolNameIndices; void readNextDebugLocation(); void readSourceMapHeader(); diff --git a/src/wasm.h b/src/wasm.h index ede3e50c1de..4901723fd59 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2073,22 +2073,25 @@ class Function : public Importable { std::unordered_map localNames; std::unordered_map localIndices; - // Source maps debugging info: map expression nodes to their file, line, col. + // Source maps debugging info: map expression nodes to their file, line, col, + // symbol name. struct DebugLocation { BinaryLocation fileIndex, lineNumber, columnNumber; + std::optional symbolNameIndex; bool operator==(const DebugLocation& other) const { return fileIndex == other.fileIndex && lineNumber == other.lineNumber && - columnNumber == other.columnNumber; + columnNumber == other.columnNumber && + symbolNameIndex == other.symbolNameIndex; } bool operator!=(const DebugLocation& other) const { return !(*this == other); } bool operator<(const DebugLocation& other) const { - return fileIndex != other.fileIndex - ? fileIndex < other.fileIndex - : lineNumber != other.lineNumber - ? lineNumber < other.lineNumber - : columnNumber < other.columnNumber; + return fileIndex != other.fileIndex ? fileIndex < other.fileIndex + : lineNumber != other.lineNumber ? lineNumber < other.lineNumber + : columnNumber != other.columnNumber + ? columnNumber < other.columnNumber + : symbolNameIndex < other.symbolNameIndex; } }; // One can explicitly set the debug location of an expression to @@ -2300,6 +2303,7 @@ class Module { // Source maps debug info. std::vector debugInfoFileNames; + std::vector debugInfoSymbolNames; // `features` are the features allowed to be used in this module and should be // respected regardless of the value of`hasFeaturesSection`. diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index 4667718034c..920baf9440f 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1189,7 +1189,7 @@ void WasmBinaryWriter::writeSymbolMap() { } void WasmBinaryWriter::initializeDebugInfo() { - lastDebugLocation = {0, /* lineNumber = */ 1, 0}; + lastDebugLocation = {0, /* lineNumber = */ 1, 0, std::nullopt}; } void WasmBinaryWriter::writeSourceMapProlog() { @@ -1225,7 +1225,17 @@ void WasmBinaryWriter::writeSourceMapProlog() { // TODO respect JSON string encoding, e.g. quotes and control chars. *sourceMap << "\"" << wasm->debugInfoFileNames[i] << "\""; } - *sourceMap << "],\"names\":[],\"mappings\":\""; + *sourceMap << "],\"names\":["; + + for (size_t i = 0; i < wasm->debugInfoSymbolNames.size(); i++) { + if (i > 0) { + *sourceMap << ","; + } + // TODO respect JSON string encoding, e.g. quotes and control chars. + *sourceMap << "\"" << wasm->debugInfoSymbolNames[i] << "\""; + } + + *sourceMap << "],\"mappings\":\""; } static void writeBase64VLQ(std::ostream& out, int32_t n) { @@ -1249,7 +1259,10 @@ static void writeBase64VLQ(std::ostream& out, int32_t n) { void WasmBinaryWriter::writeSourceMapEpilog() { // write source map entries size_t lastOffset = 0; - Function::DebugLocation lastLoc = {0, /* lineNumber = */ 1, 0}; + BinaryLocation lastFileIndex = 0; + BinaryLocation lastLineNumber = 1; + BinaryLocation lastColumnNumber = 0; + BinaryLocation lastSymbolNameIndex = 0; for (const auto& [offset, loc] : sourceMapLocations) { if (lastOffset > 0) { *sourceMap << ","; @@ -1257,13 +1270,20 @@ void WasmBinaryWriter::writeSourceMapEpilog() { writeBase64VLQ(*sourceMap, int32_t(offset - lastOffset)); lastOffset = offset; if (loc) { - // There is debug information for this location, so emit the next 3 - // fields and update lastLoc. - writeBase64VLQ(*sourceMap, int32_t(loc->fileIndex - lastLoc.fileIndex)); - writeBase64VLQ(*sourceMap, int32_t(loc->lineNumber - lastLoc.lineNumber)); - writeBase64VLQ(*sourceMap, - int32_t(loc->columnNumber - lastLoc.columnNumber)); - lastLoc = *loc; + writeBase64VLQ(*sourceMap, int32_t(loc->fileIndex - lastFileIndex)); + lastFileIndex = loc->fileIndex; + + writeBase64VLQ(*sourceMap, int32_t(loc->lineNumber - lastLineNumber)); + lastLineNumber = loc->lineNumber; + + writeBase64VLQ(*sourceMap, int32_t(loc->columnNumber - lastColumnNumber)); + lastColumnNumber = loc->columnNumber; + + if (loc->symbolNameIndex) { + writeBase64VLQ(*sourceMap, + int32_t(*loc->symbolNameIndex - lastSymbolNameIndex)); + lastSymbolNameIndex = *loc->symbolNameIndex; + } } } *sourceMap << "\"}"; @@ -1716,7 +1736,7 @@ WasmBinaryReader::WasmBinaryReader(Module& wasm, FeatureSet features, const std::vector& input) : wasm(wasm), allocator(wasm.allocator), input(input), sourceMap(nullptr), - nextDebugPos(0), nextDebugLocation{0, 0, 0}, + nextDebugPos(0), nextDebugLocation{0, 0, 0, std::nullopt}, nextDebugLocationHasDebugInfo(false), debugLocation() { wasm.features = features; } @@ -2885,6 +2905,21 @@ void WasmBinaryReader::readSourceMapHeader() { mustReadChar(']'); } + if (findField("names")) { + skipWhitespace(); + mustReadChar('['); + if (!maybeReadChar(']')) { + do { + std::string symbol; + readString(symbol); + Index index = wasm.debugInfoSymbolNames.size(); + wasm.debugInfoSymbolNames.push_back(symbol); + debugInfoSymbolNameIndices[symbol] = index; + } while (maybeReadChar(',')); + mustReadChar(']'); + } + } + if (!findField("mappings")) { throw MapParseException("cannot find the 'mappings' field in map"); } @@ -2911,7 +2946,12 @@ void WasmBinaryReader::readSourceMapHeader() { uint32_t lineNumber = readBase64VLQ(*sourceMap) + 1; // adjust zero-based line number uint32_t columnNumber = readBase64VLQ(*sourceMap); - nextDebugLocation = {fileIndex, lineNumber, columnNumber}; + std::optional symbolNameIndex; + peek = sourceMap->peek(); + if (!(peek == ',' || peek == '\"')) { + symbolNameIndex = readBase64VLQ(*sourceMap); + } + nextDebugLocation = {fileIndex, lineNumber, columnNumber, symbolNameIndex}; nextDebugLocationHasDebugInfo = true; } } @@ -2966,7 +3006,15 @@ void WasmBinaryReader::readNextDebugLocation() { int32_t columnNumberDelta = readBase64VLQ(*sourceMap); uint32_t columnNumber = nextDebugLocation.columnNumber + columnNumberDelta; - nextDebugLocation = {fileIndex, lineNumber, columnNumber}; + std::optional symbolNameIndex; + peek = sourceMap->peek(); + if (!(peek == ',' || peek == '\"')) { + int32_t symbolNameIndexDelta = readBase64VLQ(*sourceMap); + symbolNameIndex = + nextDebugLocation.symbolNameIndex.value_or(0) + symbolNameIndexDelta; + } + + nextDebugLocation = {fileIndex, lineNumber, columnNumber, symbolNameIndex}; nextDebugLocationHasDebugInfo = true; } } diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 84fd9a06ffc..e8de4572bd7 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1863,6 +1863,9 @@ void Module::updateMaps() { assert(tagsMap.size() == tags.size()); } -void Module::clearDebugInfo() { debugInfoFileNames.clear(); } +void Module::clearDebugInfo() { + debugInfoFileNames.clear(); + debugInfoSymbolNames.clear(); +} } // namespace wasm diff --git a/test/lit/merge/sourcemap.wat b/test/lit/merge/sourcemap.wat index 8ccb373998d..1eec0bcbe8c 100644 --- a/test/lit/merge/sourcemap.wat +++ b/test/lit/merge/sourcemap.wat @@ -9,11 +9,11 @@ ;; Test that sourcemap information is preserved (module - ;;@ a:1:1 + ;;@ a:1:2 (func (export "f") - ;;@ a:2:1 + ;;@ a:3:4:myFunction (nop) - ;;@ a:3:1 + ;;@ a:5:6 ) ) ;; CHECK-TEXT: (type $0 (func)) @@ -23,15 +23,15 @@ ;; CHECK-TEXT: (export "g" (func $0_1)) ;; CHECK-TEXT: (func $0 -;; CHECK-TEXT-NEXT: ;;@ a:2:1 +;; CHECK-TEXT-NEXT: ;;@ a:3:4:myFunction ;; CHECK-TEXT-NEXT: (nop) -;; CHECK-TEXT-NEXT: ;;@ a:3:1 +;; CHECK-TEXT-NEXT: ;;@ a:5:6 ;; CHECK-TEXT-NEXT: ) ;; CHECK-TEXT: (func $0_1 -;; CHECK-TEXT-NEXT: ;;@ b:2:2 +;; CHECK-TEXT-NEXT: ;;@ b:9:10:MyClass::g ;; CHECK-TEXT-NEXT: (nop) -;; CHECK-TEXT-NEXT: ;;@ b:3:2 +;; CHECK-TEXT-NEXT: ;;@ b:11:12 ;; CHECK-TEXT-NEXT: ) ;; CHECK-BIN: (type $0 (func)) @@ -41,13 +41,13 @@ ;; CHECK-BIN: (export "g" (func $1)) ;; CHECK-BIN: (func $0 -;; CHECK-BIN-NEXT: ;;@ a:2:1 +;; CHECK-BIN-NEXT: ;;@ a:3:4:myFunction ;; CHECK-BIN-NEXT: (nop) -;; CHECK-BIN-NEXT: ;;@ a:3:1 +;; CHECK-BIN-NEXT: ;;@ a:5:6 ;; CHECK-BIN-NEXT: ) ;; CHECK-BIN: (func $1 -;; CHECK-BIN-NEXT: ;;@ b:2:2 +;; CHECK-BIN-NEXT: ;;@ b:9:10:MyClass::g ;; CHECK-BIN-NEXT: (nop) -;; CHECK-BIN-NEXT: ;;@ b:3:2 +;; CHECK-BIN-NEXT: ;;@ b:11:12 ;; CHECK-BIN-NEXT: ) diff --git a/test/lit/merge/sourcemap.wat.second b/test/lit/merge/sourcemap.wat.second index 0ea7c75fa03..3f1c110da8f 100644 --- a/test/lit/merge/sourcemap.wat.second +++ b/test/lit/merge/sourcemap.wat.second @@ -1,8 +1,8 @@ (module - ;;@ b:1:2 + ;;@ b:7:8 (func (export "g") - ;;@ b:2:2 + ;;@ b:9:10:MyClass::g (nop) - ;;@ b:3:2 + ;;@ b:11:12 ) ) diff --git a/test/lit/metadce/sourcemap.wat b/test/lit/metadce/sourcemap.wat index 8a73a01da57..0190fe557da 100644 --- a/test/lit/metadce/sourcemap.wat +++ b/test/lit/metadce/sourcemap.wat @@ -7,20 +7,20 @@ ;; Test that sourcemap information is preserved (module - ;;@ a:1:1 + ;;@ a:1:2 ;; TXT: (type $0 (func)) ;; TXT: (export "f" (func $f)) ;; TXT: (func $f - ;; TXT-NEXT: ;;@ a:2:1 + ;; TXT-NEXT: ;;@ a:7:8:someSymbol ;; TXT-NEXT: (nop) - ;; TXT-NEXT: ;;@ a:3:1 + ;; TXT-NEXT: ;;@ a:9:10 ;; TXT-NEXT: ) (func $f (export "f") - ;;@ a:2:1 + ;;@ a:7:8:someSymbol (nop) - ;;@ a:3:1 + ;;@ a:9:10 ) ) ;; BIN: (type $0 (func)) @@ -28,7 +28,7 @@ ;; BIN: (export "f" (func $0)) ;; BIN: (func $0 -;; BIN-NEXT: ;;@ a:2:1 +;; BIN-NEXT: ;;@ a:7:8:someSymbol ;; BIN-NEXT: (nop) -;; BIN-NEXT: ;;@ a:3:1 +;; BIN-NEXT: ;;@ a:9:10 ;; BIN-NEXT: ) diff --git a/test/lit/passes/inlining_source-maps.wast b/test/lit/passes/inlining_source-maps.wast new file mode 100644 index 00000000000..5b5549986b9 --- /dev/null +++ b/test/lit/passes/inlining_source-maps.wast @@ -0,0 +1,36 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --inlining --all-features -S -o - | filecheck %s + +(module + ;; CHECK: (type $0 (func)) + + ;; CHECK: (start $2) + (start $2) + + (func $0 + ;;@ foo.cpp:1:2 + (nop) + ) + + (func $1 + ;;@ bar.cpp:3:4:MyFunction + (nop) + ) + + ;; CHECK: (func $2 (type $0) + ;; CHECK-NEXT: (block $__inlined_func$0 + ;; CHECK-NEXT: ;;@ foo.cpp:1:2 + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ;;@ + ;; CHECK-NEXT: (block $__inlined_func$1$1 + ;; CHECK-NEXT: ;;@ bar.cpp:3:4:MyFunction + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $2 + (call $0) + (call $1) + ) +) diff --git a/test/lit/source-map.wast b/test/lit/source-map.wast index f8ef07c65cc..b4fc55374a5 100644 --- a/test/lit/source-map.wast +++ b/test/lit/source-map.wast @@ -110,4 +110,21 @@ ;;@ src.cpp:3 (nop) ) + + ;; CHECK: (func $symbolNames + ;; CHECK-NEXT: ;;@ /tmp/src.cpp:1:2:MyClass.myMethod + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ;;@ ../src.cpp:3:4:MyClass::myMethod + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ;;@ café.cpp:5:6:topLevelFunction + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $symbolNames + ;;@ /tmp/src.cpp:1:2:MyClass.myMethod + (nop) + ;;@ ../src.cpp:3:4:MyClass::myMethod + (nop) + ;;@ café.cpp:5:6:topLevelFunction + (nop) + ) )