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

Commit f06ba4a

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 ae770c0 commit f06ba4a

File tree

8 files changed

+323
-0
lines changed

8 files changed

+323
-0
lines changed

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

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ impl AccountAddress {
4545
Self(buf)
4646
}
4747

48+
/// Return a canonical string representation of the address
49+
/// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform)
50+
/// e.g., 0000000000000000000000000000000a, *not* 0x0000000000000000000000000000000a, 0xa, or 0xA
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 {
54+
hex::encode(&self.0)
55+
}
56+
4857
pub fn short_str_lossless(&self) -> String {
4958
let hex_str = hex::encode(&self.0).trim_start_matches('0').to_string();
5059
if hex_str.is_empty() {

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

+53
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,32 @@ 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 type names; e.g.
50+
/// `00000000000000000000000000000001::string::String` or
51+
/// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2<u64>>`
52+
/// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform)
53+
/// Note: this function is guaranteed to be stable, and this is suitable for use inside
54+
/// Move native functions or the VM. By contrast, the `Display` implementation is subject
55+
/// to change and should not be used inside stable code.
56+
pub fn to_canonical_string(&self) -> String {
57+
use TypeTag::*;
58+
match self {
59+
Bool => "bool".to_owned(),
60+
U8 => "u8".to_owned(),
61+
U64 => "u64".to_owned(),
62+
U128 => "u128".to_owned(),
63+
Address => "address".to_owned(),
64+
Signer => "signer".to_owned(),
65+
Vector(t) => format!("vector<{}>", t.to_canonical_string()),
66+
Struct(s) => s.to_canonical_string(),
67+
}
68+
}
69+
}
70+
4571
impl FromStr for TypeTag {
4672
type Err = anyhow::Error;
4773

@@ -70,6 +96,33 @@ impl StructTag {
7096
pub fn module_id(&self) -> ModuleId {
7197
ModuleId::new(self.address, self.module.to_owned())
7298
}
99+
100+
/// Return a canonical string representation of the struct.
101+
/// Struct types are represented as fully qualified type names; e.g.
102+
/// `00000000000000000000000000000001::string::String` or
103+
/// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2<u64>>`
104+
/// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform)
105+
/// Note: this function is guaranteed to be stable, and this is suitable for use inside
106+
/// Move native functions or the VM. By contrast, the `Display` implementation is subject
107+
/// to change and should not be used inside stable code.
108+
pub fn to_canonical_string(&self) -> String {
109+
let mut generics = String::new();
110+
if let Some(first_ty) = self.type_params.first() {
111+
generics.push('<');
112+
generics.push_str(&first_ty.to_canonical_string());
113+
for ty in self.type_params.iter().skip(1) {
114+
generics.push_str(&ty.to_canonical_string())
115+
}
116+
generics.push('>');
117+
}
118+
format!(
119+
"{}::{}::{}{}",
120+
self.address.to_canonical_string(),
121+
self.module,
122+
self.name,
123+
generics
124+
)
125+
}
73126
}
74127

75128
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

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 type names; e.g.
44+
<code>00000000000000000000000000000001::string::String</code> or
45+
<code>0000000000000000000000000000000a::module_name1::type_name1&lt;0000000000000000000000000000000a::module_name2::type_name2&lt;u64&gt;&gt;</code>
46+
Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform)
47+
</dd>
48+
</dl>
49+
50+
51+
</details>
52+
53+
<a name="0x1_type_name_get"></a>
54+
55+
## Function `get`
56+
57+
Return a value representation of the type <code>T</code>.
58+
59+
60+
<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>
61+
</code></pre>
62+
63+
64+
65+
<details>
66+
<summary>Implementation</summary>
67+
68+
69+
<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>;
70+
</code></pre>
71+
72+
73+
74+
</details>
75+
76+
<a name="0x1_type_name_borrow_string"></a>
77+
78+
## Function `borrow_string`
79+
80+
Get the String representation of <code>self</code>
81+
82+
83+
<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>
84+
</code></pre>
85+
86+
87+
88+
<details>
89+
<summary>Implementation</summary>
90+
91+
92+
<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 {
93+
&self.name
94+
}
95+
</code></pre>
96+
97+
98+
99+
</details>
100+
101+
<a name="0x1_type_name_into_string"></a>
102+
103+
## Function `into_string`
104+
105+
Convert <code>self</code> into its inner String
106+
107+
108+
<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>
109+
</code></pre>
110+
111+
112+
113+
<details>
114+
<summary>Implementation</summary>
115+
116+
117+
<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 {
118+
self.name
119+
}
120+
</code></pre>
121+
122+
123+
124+
</details>
125+
126+
127+
[//]: # ("File containing references which can be used from documentation")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 type names; e.g.
10+
/// `00000000000000000000000000000001::string::String` or
11+
/// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2<u64>>`
12+
/// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform)
13+
name: String
14+
}
15+
16+
/// Return a value representation of the type `T`.
17+
public native fun get<T>(): TypeName;
18+
19+
/// Get the String representation of `self`
20+
public fun borrow_string(self: &TypeName): &String {
21+
&self.name
22+
}
23+
24+
/// Convert `self` into its inner String
25+
public fun into_string(self: TypeName): String {
26+
self.name
27+
}
28+
}

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,38 @@
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+
// Note: these tests assume a 16 byte address length, and will fail on platforms where addresses are 20 or 32 bytes
23+
#[test]
24+
fun test_structs() {
25+
assert!(into_string(get<TestStruct>()) == string(b"0000000000000000000000000000000a::type_name_tests::TestStruct"), 0);
26+
assert!(into_string(get<std::ascii::String>()) == string(b"00000000000000000000000000000001::ascii::String"), 0);
27+
assert!(into_string(get<std::option::Option<u64>>()) == string(b"00000000000000000000000000000001::option::Option<u64>"), 0);
28+
assert!(into_string(get<std::string::String>()) == string(b"00000000000000000000000000000001::string::String"), 0);
29+
}
30+
31+
// Note: these tests assume a 16 byte address length, and will fail on platforms where addresses are 20 or 32 bytes
32+
#[test]
33+
fun test_generics() {
34+
assert!(into_string(get<TestGenerics<std::string::String>>()) == string(b"0000000000000000000000000000000a::type_name_tests::TestGenerics<00000000000000000000000000000001::string::String>"), 0);
35+
assert!(into_string(get<vector<TestGenerics<u64>>>()) == string(b"vector<0000000000000000000000000000000a::type_name_tests::TestGenerics<u64>>"), 0);
36+
assert!(into_string(get<std::option::Option<TestGenerics<u8>>>()) == string(b"00000000000000000000000000000001::option::Option<0000000000000000000000000000000a::type_name_tests::TestGenerics<u8>>"), 0);
37+
}
38+
}

0 commit comments

Comments
 (0)