Skip to content

Commit 45a6cff

Browse files
authored
Map code without a DWARF line to no location in wasm source maps (#28033)
`wasm-sourcemap.py` drops DWARF rows with line 0, which LLVM uses for code that does not come from a single source line, for example when it merges several calls to `abort()` into one. The previous mapping then covers that code too, so it is reported at an unrelated line. This writes an entry with only the wasm offset at these rows instead. An entry without a source location ends the previous mapping, so the code gets no location. Binaryen does the same when it writes source maps (WebAssembly/binaryen#5906): one such entry for each run of code without a location, and none before the first mapping. Binaryen and emsymbolizer already read these entries. When this tool was written, Binaryen could not, which is likely why the rows were dropped. Each entry adds about two bytes. The new test checks that code at line 0 maps to no location and that consecutive line 0 rows share one entry.
1 parent 1c31a61 commit 45a6cff

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

‎src/source_map_support.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ class WasmSourceMap {
6262
lookup(offset) {
6363
const normalized = this.normalizeOffset(offset);
6464
const info = this.mapping[normalized];
65-
if (!info) {
65+
// A segment with only an offset marks code that has no source location.
66+
if (!info || info.source === undefined) {
6667
return null;
6768
}
6869
return {

‎test/test_other.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10112,6 +10112,41 @@ def test_wasm_sourcemap_dead(self):
1011210112
# has only two entries
1011310113
self.assertRegex(output, r'"mappings":\s*"[A-Za-z0-9+/]+,[A-Za-z0-9+/]+"')
1011410114

10115+
def test_wasm_sourcemap_line_zero(self):
10116+
# code at DWARF line 0 should map to no location, not to the row before it
10117+
wasm_sourcemap = importlib.import_module('tools.wasm-sourcemap')
10118+
emsymbolizer = importlib.import_module('tools.emsymbolizer')
10119+
entries = [
10120+
{'address': 0x10, 'line': 5, 'column': 3, 'file': 'a.c', 'eos': False},
10121+
{'address': 0x18, 'line': 0, 'column': 0, 'file': 'a.c', 'eos': False},
10122+
{'address': 0x1a, 'line': 0, 'column': 0, 'file': 'a.c', 'eos': False},
10123+
{'address': 0x20, 'line': 6, 'column': 1, 'file': 'a.c', 'eos': False},
10124+
]
10125+
options = wasm_sourcemap.parse_args(['unused.wasm'])
10126+
write_file('a.wasm.map', json.dumps(wasm_sourcemap.build_sourcemap(entries, [], 0, options)))
10127+
source_map = emsymbolizer.WasmSourceMap()
10128+
source_map.parse('a.wasm.map')
10129+
# consecutive line 0 rows share one segment
10130+
self.assertEqual(source_map.offsets, [0x10, 0x18, 0x20])
10131+
self.assertEqual(source_map.lookup(0x14).line, 5)
10132+
self.assertIsNone(source_map.lookup(0x18).source)
10133+
self.assertIsNone(source_map.lookup(0x1c).source)
10134+
self.assertEqual(source_map.lookup(0x20).line, 6)
10135+
# the runtime's reader in source_map_support.js should agree
10136+
js = read_file(path_from_root('src/source_map_support.js'))
10137+
js = js[js.index('class WasmSourceMap'):js.index('let wasmSourceMap;')]
10138+
create_file('lookup.js', js + '''
10139+
const map = JSON.parse(require('fs').readFileSync('a.wasm.map', 'utf8'));
10140+
const sm = new WasmSourceMap(map);
10141+
console.log(JSON.stringify([0x14, 0x18, 0x1c, 0x20].map((pc) => sm.lookup(pc))));
10142+
''')
10143+
self.assertEqual(json.loads(self.run_js('lookup.js')), [
10144+
{'file': 'a.c', 'line': 5, 'column': 3},
10145+
None,
10146+
None,
10147+
{'file': 'a.c', 'line': 6, 'column': 1},
10148+
])
10149+
1011510150
def test_wasm_sourcemap_relative_paths(self):
1011610151
ensure_dir('build')
1011710152

‎tools/wasm-sourcemap.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ def build_sourcemap(entries, func_ranges, code_section_offset, options):
522522
last_line = 1
523523
last_column = 1
524524
last_func_id = 0
525+
last_was_unmapped = False
525526

526527
active_funcs = []
527528
next_func_range_id = 0
@@ -550,9 +551,17 @@ def get_function_id(address):
550551
for entry in entries:
551552
line = entry['line']
552553
column = entry['column']
553-
# ignore entries with line 0
554+
# line 0 means no source location; end the previous mapping with a
555+
# segment that has only the offset, which the spec uses for unmapped
556+
# code. The other fields are deltas, so they keep their last values.
554557
if line == 0:
558+
if mappings and not last_was_unmapped:
559+
address = entry['address'] + code_section_offset
560+
mappings.append(encode_vlq(address - last_address))
561+
last_address = address
562+
last_was_unmapped = True
555563
continue
564+
last_was_unmapped = False
556565
# start at least at column 1
557566
if column == 0:
558567
column = 1

0 commit comments

Comments
 (0)