Repository: vyperlang/vyper

Issue:
Source map contains an extra instruction
### Version Information

* vyper Version (output of `vyper --version`): 0.3.4+commit.f31f0ec
* OS: Linux Mint
* Python Version (output of `python --version`): Python 3.8.10

### What's your issue about?

It looks to me like source maps in this version extend one instruction beyond where they're supposed to.

Let's take as an example the following extremely simple contract:

```vyper
@external
@payable
def __init__():
    pass

@external
@payable
def __default__():
    pass
```

It compiles to the following (deployed) bytecode: (we'll ignore the constructor bytecode, due to #1998 still being an outstanding issue)

```
0x6003361161000c57610014565b60003560e01c505b00a165767970657283000304
```

Now, the last eleven bytes of this are metadata, but if we remove those we get the following decompilation:

```
0x00: PUSH1 0x03
0x02: CALLDATASIZE 
0x03: GT 
0x04: PUSH2 0x000c
0x07: JUMPI 
0x08: PUSH2 0x0014
0x0b: JUMP 
0x0c: JUMPDEST 
0x0d: PUSH1 0x00
0x0f: CALLDATALOAD 
0x10: PUSH1 0xe0
0x12: SHR 
0x13: POP 
0x14: JUMPDEST 
0x15: STOP
```

Which has 15 instructions.

Meanwhile, the source map is as follows:

```
-1:-1:4:-;;;;:::-;;:::-;:::-;;;;;;:::-;;
```

This source map contains 15 semicolons -- which is to say, *16* instructions.  That's one more instruction than actually exists; the source map extends into the metadata area.  A disassembler that looked at the source map to know where to stop would report a `LOG1` just beyond the final `STOP`, when in fact that `0xa1` isn't intended as a `LOG1` at all.


Discussion:
@haltman-at interesting, is this behavior different from solidity? vyper has done it this way since at least v0.2.0, but maybe before it was moot (since there were no trailing opcodes)

https://github.com/vyperlang/vyper/blob/8b8cff233d766f2fda55d82cbf9a8d3160ab4e7a/vyper/compiler/output.py#L237

Resolving patch:
diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py
--- a/vyper/compiler/output.py
+++ b/vyper/compiler/output.py
@@ -214,7 +214,7 @@ def build_source_map_output(compiler_data: CompilerData) -> OrderedDict:
 
 def _compress_source_map(code, pos_map, jump_map, source_id):
     linenos = asttokens.LineNumbers(code)
-    compressed_map = f"-1:-1:{source_id}:-;"
+    ret = [f"-1:-1:{source_id}:-"]
     last_pos = [-1, -1, source_id]
 
     for pc in sorted(pos_map)[1:]:
@@ -234,9 +234,9 @@ def _compress_source_map(code, pos_map, jump_map, source_id):
             else:
                 current_pos[i] = ""
 
-        compressed_map += ":".join(str(i) for i in current_pos) + ";"
+        ret.append(":".join(str(i) for i in current_pos))
 
-    return compressed_map
+    return ";".join(ret)
 
 
 def build_bytecode_output(compiler_data: CompilerData) -> str:


Test patch:
diff --git a/tests/compiler/test_source_map.py b/tests/compiler/test_source_map.py
--- a/tests/compiler/test_source_map.py
+++ b/tests/compiler/test_source_map.py
@@ -90,7 +90,7 @@ def foo() -> uint256:
     compressed = _compress_source_map(
         code, {"0": None, "2": (2, 0, 4, 13), "3": (2, 0, 2, 8), "5": (2, 0, 2, 8)}, {"3": "o"}, 2
     )
-    assert compressed == "-1:-1:2:-;1:45;:8::o;;"
+    assert compressed == "-1:-1:2:-;1:45;:8::o;"
 
 
 def test_expand_source_map():