Skip to content
This repository was archived by the owner on May 4, 2024. It is now read-only.

Commit 11eefea

Browse files
committed
[stdlib] add type_name module for type reflection
Move allows type-safe implementations of key types like fungible tokens using the Coin<T> standard. However, distinguishing coins at the type level only is sometimes limiting--e.g., you might want to write a DEX that has at most one pool for a given pair of currencies (need to compare types + reify the result in a value) or write a lending protocol that supports an extensible pool of currencies (need to reify the currency types + map them to the reserve coin supply). This PR seeks to add the weakest form of reflection that supports use-cases like the ones above in order to keep static reasoning as simple/predictable as possible. Similar modules exist in the Starcoin and Aptos frameworks, and Sui users have also been requesting this feature. - Add opaque `TypeName` value that can be produced from a Move type via `get<T>(): TypeName` - Add functions for converting a `TypeName`'s into its inner representation as an ASCII strings so it can be compared or inspected. This is safe because the Move binary format enforces ASCII encoding for all identifiers One issue worth discussing is whether we want to use the full-length representation of addresses (e.g., always produce a 16, 20, or 32 byte string, depending on the platform address length) or the "short" representation which strips leading 0's (e.g., the convention `0x1` for the Move stdlib). I think the former is slightly cleaner/more predictable, but I went with the latter because it already seems to be the standard in the `TypeTag` implementation of `Display`, as well as in the Aptos/Starcoin `TypeInfo` module that provides similar functionality to this code.
1 parent 9b85f4e commit 11eefea

File tree

8 files changed

+327
-1
lines changed

8 files changed

+327
-1
lines changed

language/move-core/types/src/account_address.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ impl AccountAddress {
4545
Self(buf)
4646
}
4747

48-
pub fn short_str_lossless(&self) -> String {
48+
/// Return a canonical string representation of the address
49+
/// Addresses are 0x-prefixed, hex-encoded lowercase values with leading 0's stripped
50+
/// e.g., 0xa, *not* 0x00000000a, 0xA, or 0x00000000A
51+
/// Note: this function is guaranteed to be stable, and this is suitable for use inside
52+
/// Move native functions or the VM.
53+
pub fn to_canonical_string(&self) -> String {
4954
let hex_str = hex::encode(&self.0).trim_start_matches('0').to_string();
5055
if hex_str.is_empty() {
5156
"0".to_string()
@@ -54,6 +59,10 @@ impl AccountAddress {
5459
}
5560
}
5661

62+
pub fn short_str_lossless(&self) -> String {
63+
self.to_canonical_string()
64+
}
65+
5766
pub fn to_vec(&self) -> Vec<u8> {
5867
self.0.to_vec()
5968
}

language/move-core/types/src/language_storage.rs

+55
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@ pub enum TypeTag {
4242
Struct(StructTag),
4343
}
4444

45+
impl TypeTag {
46+
/// Return a canonical string representation of the type. All types are represented
47+
/// using their source syntax:
48+
/// "u8", "u64", "u128", "bool", "address", "vector", "signer" for ground types.
49+
/// Struct types are represented as fully qualified, `0x`-prefixed short type names
50+
/// `0x1::string::String` or
51+
/// `0xabcd::module_name1::type_name1<0x1234::module_name2::type_name2<u64>>`
52+
/// Addresses are 0x-prefixed, hex-encoded lowercase values with leading 0's stripped
53+
/// e.g., 0xa, *not* 0x00000000a, 0xA, or 0x00000000A
54+
/// Note: this function is guaranteed to be stable, and this is suitable for use inside
55+
/// Move native functions or the VM. By contrast, the `Display` implementation is subject
56+
/// to change and should not be used inside stable code.
57+
pub fn to_canonical_string(&self) -> String {
58+
use TypeTag::*;
59+
match self {
60+
Bool => "bool".to_owned(),
61+
U8 => "u8".to_owned(),
62+
U64 => "u64".to_owned(),
63+
U128 => "u128".to_owned(),
64+
Address => "address".to_owned(),
65+
Signer => "signer".to_owned(),
66+
Vector(t) => format!("vector<{}>", t.to_canonical_string()),
67+
Struct(s) => s.to_canonical_string(),
68+
}
69+
}
70+
}
71+
4572
impl FromStr for TypeTag {
4673
type Err = anyhow::Error;
4774

@@ -70,6 +97,34 @@ impl StructTag {
7097
pub fn module_id(&self) -> ModuleId {
7198
ModuleId::new(self.address, self.module.to_owned())
7299
}
100+
101+
/// Return a canonical string representation of the struct.
102+
/// Struct types are represented as fully qualified, `0x`-prefixed short type names
103+
/// `0x1::string::String` or
104+
/// `0xabcd::module_name1::type_name1<0x1234::module_name2::type_name2<u64>>`
105+
/// Addresses are 0x-prefixed, hex-encoded lowercase values with leading 0's stripped
106+
/// e.g., 0xa, *not* 0x00000000a, 0xA, or 0x00000000A
107+
/// Note: this function is guaranteed to be stable, and this is suitable for use inside
108+
/// Move native functions or the VM. By contrast, the `Display` implementation is subject
109+
/// to change and should not be used inside stable code.
110+
pub fn to_canonical_string(&self) -> String {
111+
let mut generics = String::new();
112+
if let Some(first_ty) = self.type_params.first() {
113+
generics.push_str("<");
114+
generics.push_str(&first_ty.to_canonical_string());
115+
for ty in self.type_params.iter().skip(1) {
116+
generics.push_str(&ty.to_canonical_string())
117+
}
118+
generics.push_str(">");
119+
}
120+
format!(
121+
"0x{}::{}::{}{}",
122+
self.address.to_canonical_string(),
123+
self.module,
124+
self.name,
125+
generics
126+
)
127+
}
73128
}
74129

75130
impl FromStr for StructTag {

language/move-stdlib/docs/overview.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This is the root document for the Move stdlib module documentation. The Move std
2121
- [`0x1::option`](option.md#0x1_option)
2222
- [`0x1::signer`](signer.md#0x1_signer)
2323
- [`0x1::string`](string.md#0x1_string)
24+
- [`0x1::type_name`](type_name.md#0x1_type_name)
2425
- [`0x1::vector`](vector.md#0x1_vector)
2526

2627

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
2+
<a name="0x1_type_name"></a>
3+
4+
# Module `0x1::type_name`
5+
6+
Functionality for converting Move types into values. Use with care!
7+
8+
9+
- [Struct `TypeName`](#0x1_type_name_TypeName)
10+
- [Function `get`](#0x1_type_name_get)
11+
- [Function `borrow_string`](#0x1_type_name_borrow_string)
12+
- [Function `into_string`](#0x1_type_name_into_string)
13+
14+
15+
<pre><code><b>use</b> <a href="ascii.md#0x1_ascii">0x1::ascii</a>;
16+
</code></pre>
17+
18+
19+
20+
<a name="0x1_type_name_TypeName"></a>
21+
22+
## Struct `TypeName`
23+
24+
25+
26+
<pre><code><b>struct</b> <a href="type_name.md#0x1_type_name_TypeName">TypeName</a> <b>has</b> <b>copy</b>, drop, store
27+
</code></pre>
28+
29+
30+
31+
<details>
32+
<summary>Fields</summary>
33+
34+
35+
<dl>
36+
<dt>
37+
<code>name: <a href="ascii.md#0x1_ascii_String">ascii::String</a></code>
38+
</dt>
39+
<dd>
40+
String representation of the type. All types are represented
41+
using their source syntax:
42+
"u8", "u64", "u128", "bool", "address", "vector", "signer" for ground types.
43+
Struct types are represented as fully qualified, <code>0x</code>-prefixed short type names
44+
<code><a href="string.md#0x1_string_String">0x1::string::String</a></code> or
45+
<code>0xabcd::module_name1::type_name1&lt;0x1234::module_name2::type_name2&lt;u64&gt;&gt;</code>
46+
Addresses are 0x-prefixed, hex-encoded lowercase values with leading 0's stripped
47+
e.g., 0xa, *not* 0x00000000a, 0xA, or 0x00000000A
48+
</dd>
49+
</dl>
50+
51+
52+
</details>
53+
54+
<a name="0x1_type_name_get"></a>
55+
56+
## Function `get`
57+
58+
Return a value representation of the type <code>T</code>.
59+
60+
61+
<pre><code><b>public</b> <b>fun</b> <a href="type_name.md#0x1_type_name_get">get</a>&lt;T&gt;(): <a href="type_name.md#0x1_type_name_TypeName">type_name::TypeName</a>
62+
</code></pre>
63+
64+
65+
66+
<details>
67+
<summary>Implementation</summary>
68+
69+
70+
<pre><code><b>public</b> <b>native</b> <b>fun</b> <a href="type_name.md#0x1_type_name_get">get</a>&lt;T&gt;(): <a href="type_name.md#0x1_type_name_TypeName">TypeName</a>;
71+
</code></pre>
72+
73+
74+
75+
</details>
76+
77+
<a name="0x1_type_name_borrow_string"></a>
78+
79+
## Function `borrow_string`
80+
81+
Get the String representation of <code>self</code>
82+
83+
84+
<pre><code><b>public</b> <b>fun</b> <a href="type_name.md#0x1_type_name_borrow_string">borrow_string</a>(self: &<a href="type_name.md#0x1_type_name_TypeName">type_name::TypeName</a>): &<a href="ascii.md#0x1_ascii_String">ascii::String</a>
85+
</code></pre>
86+
87+
88+
89+
<details>
90+
<summary>Implementation</summary>
91+
92+
93+
<pre><code><b>public</b> <b>fun</b> <a href="type_name.md#0x1_type_name_borrow_string">borrow_string</a>(self: &<a href="type_name.md#0x1_type_name_TypeName">TypeName</a>): &String {
94+
&self.name
95+
}
96+
</code></pre>
97+
98+
99+
100+
</details>
101+
102+
<a name="0x1_type_name_into_string"></a>
103+
104+
## Function `into_string`
105+
106+
Convert <code>self</code> into its inner String
107+
108+
109+
<pre><code><b>public</b> <b>fun</b> <a href="type_name.md#0x1_type_name_into_string">into_string</a>(self: <a href="type_name.md#0x1_type_name_TypeName">type_name::TypeName</a>): <a href="ascii.md#0x1_ascii_String">ascii::String</a>
110+
</code></pre>
111+
112+
113+
114+
<details>
115+
<summary>Implementation</summary>
116+
117+
118+
<pre><code><b>public</b> <b>fun</b> <a href="type_name.md#0x1_type_name_into_string">into_string</a>(self: <a href="type_name.md#0x1_type_name_TypeName">TypeName</a>): String {
119+
self.name
120+
}
121+
</code></pre>
122+
123+
124+
125+
</details>
126+
127+
128+
[//]: # ("File containing references which can be used from documentation")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/// Functionality for converting Move types into values. Use with care!
2+
module std::type_name {
3+
use std::ascii::String;
4+
5+
struct TypeName has copy, drop, store {
6+
/// String representation of the type. All types are represented
7+
/// using their source syntax:
8+
/// "u8", "u64", "u128", "bool", "address", "vector", "signer" for ground types.
9+
/// Struct types are represented as fully qualified, `0x`-prefixed short type names
10+
/// `0x1::string::String` or
11+
/// `0xabcd::module_name1::type_name1<0x1234::module_name2::type_name2<u64>>`
12+
/// Addresses are 0x-prefixed, hex-encoded lowercase values with leading 0's stripped
13+
/// e.g., 0xa, *not* 0x00000000a, 0xA, or 0x00000000A
14+
name: String
15+
}
16+
17+
/// Return a value representation of the type `T`.
18+
public native fun get<T>(): TypeName;
19+
20+
/// Get the String representation of `self`
21+
public fun borrow_string(self: &TypeName): &String {
22+
&self.name
23+
}
24+
25+
/// Convert `self` into its inner String
26+
public fun into_string(self: TypeName): String {
27+
self.name
28+
}
29+
}

language/move-stdlib/src/natives/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod event;
88
pub mod hash;
99
pub mod signer;
1010
pub mod string;
11+
pub mod type_name;
1112
#[cfg(feature = "testing")]
1213
pub mod unit_test;
1314
pub mod vector;
@@ -23,6 +24,7 @@ pub struct GasParameters {
2324
pub hash: hash::GasParameters,
2425
pub signer: signer::GasParameters,
2526
pub string: string::GasParameters,
27+
pub type_name: type_name::GasParameters,
2628
pub vector: vector::GasParameters,
2729

2830
#[cfg(feature = "testing")]
@@ -52,6 +54,12 @@ impl GasParameters {
5254
legacy_min_input_len: 0.into(),
5355
},
5456
},
57+
type_name: type_name::GasParameters {
58+
get: type_name::GetGasParameters {
59+
base: 0.into(),
60+
per_byte: 0.into(),
61+
},
62+
},
5563
signer: signer::GasParameters {
5664
borrow_address: signer::BorrowAddressGasParameters { base: 0.into() },
5765
},
@@ -112,6 +120,7 @@ pub fn all_natives(
112120
add_natives!("hash", hash::make_all(gas_params.hash));
113121
add_natives!("signer", signer::make_all(gas_params.signer));
114122
add_natives!("string", string::make_all(gas_params.string));
123+
add_natives!("type_name", type_name::make_all(gas_params.type_name));
115124
add_natives!("vector", vector::make_all(gas_params.vector));
116125
#[cfg(feature = "testing")]
117126
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) The Move Contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use move_binary_format::errors::PartialVMResult;
5+
use move_core_types::gas_algebra::{InternalGas, InternalGasPerByte, NumBytes};
6+
use move_vm_runtime::native_functions::{NativeContext, NativeFunction};
7+
use move_vm_types::{
8+
loaded_data::runtime_types::Type,
9+
natives::function::NativeResult,
10+
values::{Struct, Value},
11+
};
12+
13+
use smallvec::smallvec;
14+
use std::{collections::VecDeque, sync::Arc};
15+
16+
#[derive(Debug, Clone)]
17+
pub struct GetGasParameters {
18+
pub base: InternalGas,
19+
pub per_byte: InternalGasPerByte,
20+
}
21+
22+
fn native_get(
23+
gas_params: &GetGasParameters,
24+
context: &mut NativeContext,
25+
ty_args: Vec<Type>,
26+
arguments: VecDeque<Value>,
27+
) -> PartialVMResult<NativeResult> {
28+
debug_assert_eq!(ty_args.len(), 1);
29+
debug_assert!(arguments.is_empty());
30+
31+
let type_tag = context.type_to_type_tag(&ty_args[0])?;
32+
let type_name = type_tag.to_canonical_string();
33+
// make a std::string::String
34+
let string_val = Value::struct_(Struct::pack(vec![Value::vector_u8(
35+
type_name.as_bytes().to_vec(),
36+
)]));
37+
// make a std::type_name::TypeName
38+
let type_name_val = Value::struct_(Struct::pack(vec![string_val]));
39+
40+
let cost = gas_params.base + gas_params.per_byte * NumBytes::new(type_name.len() as u64);
41+
42+
Ok(NativeResult::ok(cost, smallvec![type_name_val]))
43+
}
44+
45+
pub fn make_native_get(gas_params: GetGasParameters) -> NativeFunction {
46+
Arc::new(move |context, ty_args, args| native_get(&gas_params, context, ty_args, args))
47+
}
48+
49+
#[derive(Debug, Clone)]
50+
pub struct GasParameters {
51+
pub get: GetGasParameters,
52+
}
53+
54+
pub fn make_all(gas_params: GasParameters) -> impl Iterator<Item = (String, NativeFunction)> {
55+
let natives = [("get", make_native_get(gas_params.get))];
56+
57+
crate::natives::helpers::make_module_natives(natives)
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// note: intentionally using 0xa here to test non-0x1 module addresses
2+
module 0xA::type_name_tests {
3+
#[test_only]
4+
use std::type_name::{get, into_string};
5+
#[test_only]
6+
use std::ascii::string;
7+
8+
struct TestStruct {}
9+
10+
struct TestGenerics<phantom T> { }
11+
12+
#[test]
13+
fun test_ground_types() {
14+
assert!(into_string(get<u8>()) == string(b"u8"), 0);
15+
assert!(into_string(get<u64>()) == string(b"u64"), 0);
16+
assert!(into_string(get<u128>()) == string(b"u128"), 0);
17+
assert!(into_string(get<address>()) == string(b"address"), 0);
18+
assert!(into_string(get<signer>()) == string(b"signer"), 0);
19+
assert!(into_string(get<vector<u8>>()) == string(b"vector<u8>"), 0)
20+
}
21+
22+
#[test]
23+
fun test_structs() {
24+
assert!(into_string(get<TestStruct>()) == string(b"0xa::type_name_tests::TestStruct"), 0);
25+
assert!(into_string(get<std::ascii::String>()) == string(b"0x1::ascii::String"), 0);
26+
assert!(into_string(get<std::option::Option<u64>>()) == string(b"0x1::option::Option<u64>"), 0);
27+
assert!(into_string(get<std::string::String>()) == string(b"0x1::string::String"), 0);
28+
}
29+
30+
#[test]
31+
fun test_generics() {
32+
assert!(into_string(get<TestGenerics<std::string::String>>()) == string(b"0xa::type_name_tests::TestGenerics<0x1::string::String>"), 0);
33+
assert!(into_string(get<vector<TestGenerics<u64>>>()) == string(b"vector<0xa::type_name_tests::TestGenerics<u64>>"), 0);
34+
assert!(into_string(get<std::option::Option<TestGenerics<u8>>>()) == string(b"0x1::option::Option<0xa::type_name_tests::TestGenerics<u8>>"), 0);
35+
}
36+
37+
}

0 commit comments

Comments
 (0)