move-language / move

Apache License 2.0
2.25k stars 679 forks source link

Replace Or Supplement std::type_name with std::struct_tag #966

Open PaulFidika opened 1 year ago

PaulFidika commented 1 year ago

So currently if you want to do type introspection, you use the std::type_name::get() function to get a TypeName, then convert it into a string, and then parse that string to the type-name up into one of its component pieces (i.e., package-id or module-name). We've written a bunch of utility functions that do this parsing:

https://github.com/capsule-craft/capsules/blob/master/packages/sui_utils/sources/encode.move

However, what would be simpler and more useful would be to skip strings altogether, and simply return StructTags, like this:

struct StructTag has store, copy, drop {
        package_id: address,
        module_name: string::String,
        struct_name: string::String,
        generics: vector<string::String>
    }

Note that it might have been interesting to make make struct_tag.generics be of type vector instead, but this sort of recursive type definition is not allowed, so the generics will just have to be strings.

For Sui Move, it might be interesting to have package_id be a sui::object::ID instead of an address, but for the Move core IDs do not exist.

We've implemented StructTags here:

https://github.com/capsule-craft/capsules/blob/master/packages/sui_utils/sources/struct_tag.move

Currently these are created using string-parsing, but if StructTags were created natively within Move it would presumably be a lot cheaper to construct them than having to parse ascii strings. These StructTags can be used to compare object-types rather easily; i.e., are they from the same package, the same module, are they the same struct but with different generics, etc.

In addition to being faster and simpler to work with, than TypeNames StructTags are also smaller to store. Addresses are encoded as hex in ascii, meaning that a 32 byte address uses up 64 bytes of storage as a hex-ascii string. I honestly can't really think of any downsides.

As an edge-case, for primitive types like address, bool, or u64, we could return this:

StructTag {
   package_id: 0x0,
   module_name: "",
   struct_name: "u64",
   generics: [ ]
}

Or we could make package_id be optional and just leave it empty in this case.

sblackshear commented 1 year ago

Quick thoughts:

PaulFidika commented 1 year ago

Sure, usually I use it like:

  1. finding the package-ID of a type, and then storing that somewhere. For example creating a Type<T> object (Damir calls these Display<T>); knowing the package from which T originated is key because you need to present a publish-receipt from T in order to create it.

  2. For Type T, I figure out its package id, module name, and then construct a string and take the hash of <package-id>::<module_name>::Witness and convert it into an address. This is that module's authority-address. This allows modules to "sign" transactions. I.e., if I have a tx_authority which contains a module's authority address, then that module must have produced an instance of Witness and used it to sign the transaction. This is like a more general, dynamic way to produce 'proofs' that a module is signing off on a certain action, rather than presenting the Witness directly itself.