vyperlang / vyper

Pythonic Smart Contract Language for the EVM
https://vyperlang.org
Other
4.87k stars 800 forks source link

VIP: Symbol Table #1950

Open fubuloubu opened 4 years ago

fubuloubu commented 4 years ago

Simple Summary

Output the storage slot and memory index mapping for Vyper variables

Motivation

A Symbol Table is a mapping between variables and their corresponding register locations. It is helpful to tools like debuggers to have this information so that it can use human-readable names (from the source code) when querying data from dynamic execution. For Vyper, because the EVM has several different memory spaces (storage slots, program memory, and the stack) it would be helpful to provide all of these in a table when compiling a program. Tools like https://tenderly.dev/ use this for advanced debugging.

Specification

TBD what format this would take, and the external API to access it.

Backwards Compatibility

No backwards compatibility issues.

Dependencies

No dependencies.

Copyright

Copyright and related rights waived via CC0

fubuloubu commented 4 years ago

Vyper is easy enough to obtain this info for from the source map (see: https://github.com/vyperlang/vyper-debug), but having this in place ensures we have support for this when more advanced optimizations of storage slots and/or memory (or using the stack) are in play.

fubuloubu commented 4 years ago

Meeting notes: @fubuloubu to define format

charles-cooper commented 3 years ago

We're going to need something a little more advanced than this if/when we implement tight packing of variables in storage, but this is a provisional format:

bar: uint256

@external
@nonreentrant("lock")
def foo():
    foo: uint256 = 0
{
  "foo.lock": {
    "type": "uint256",
    "location": "storage",
    "slot": 0
  },
  "bar": {
    "type": "uint256",
    "location": "storage",
    "slot": 1
  }
}
charles-cooper commented 3 years ago

Thinking through this a bit more, the format for nonreentrant keys should not reference the function name because the same nonreentrant lock can be used on multiple functions. Instead, to make sure it is disambiguated from variables with the same name (in the below case, a variable like baz: uint256), in the exported layout the key would be "nonreentrant.<lock name>".

bar: uint256

@external
@nonreentrant("baz")
def foo():
    foo: uint256 = 0
{
  "nonreentrant.baz": {
    "type": "nonreentrant lock",
    "location": "storage",
    "slot": 0
  },
  "bar": {
    "type": "uint256",
    "location": "storage",
    "slot": 1
  }
}
fubuloubu commented 3 years ago

should the type be an actual abi type, and then add a description field or something that let's us specify internal vs. user-defined storage?

charles-cooper commented 3 years ago

should the type be an actual abi type, and then add a description field or something that let's us specify internal vs. user-defined storage?

You mean the type of the lock?

I thought about this but I'm not sure how I feel about advertising it's some specific type as it's not like something you would ever .. ABI encode or pass to a function or do anything you would with a normal ABI type. As far as the user is concerned it's magic, and internally it can be a bool or a bit or a bytes32 or what have you, especially if we decide to change the packing.

fubuloubu commented 3 years ago

should the type be an actual abi type, and then add a description field or something that let's us specify internal vs. user-defined storage?

You mean the type of the lock?

I thought about this but I'm not sure how I feel about advertising it's some specific type as it's not like something you would ever .. ABI encode or pass to a function or do anything you would with a normal ABI type. As far as the user is concerned it's magic, and internally it can be a bool or a bit or a bytes32 or what have you, especially if we decide to change the packing.

so that type is the "internal type" then?