Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@ The format is based on [Keep a Changelog], and this project adheres to
- ergonomic transaction macro (`sui_move_runtime::tx!`).
- Add `sui-move-codegen`: fetch + normalize package metadata (`NormalizedPackage`) and render typed Rust bindings (Move-shaped types + `CallSpec` builders).

### Changed

- Generated package scopes now resolve type identity by module and datatype while calls continue to
use the current package address.

[Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ serde_json = "1.0"
thiserror = "1.0"
bcs = "0.1.6"
ethnum = "1.5.3"
sui-sdk-types = { version = "0.3.1", features = ["serde"] }
sui-rpc = { version = "0.3.1" }
sui-crypto = { version = "0.3.0" }
sui-sdk-types = { version = "0.3.2", features = ["serde"] }
sui-rpc = { version = "0.3.2" }
sui-crypto = { version = "0.3.1" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
Expand Down
91 changes: 90 additions & 1 deletion sui-move-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ documentation and converts reserved Rust identifiers safely in generated APIs.
Given a `NormalizedPackage` (either fetched from gRPC or loaded from JSON), this crate can render:

- `CALL_PACKAGE` / `TYPE_PACKAGE` constants for generated calls and type identity
- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package-id overrides
- `call_package()` / `type_package()` / `with_packages(...)` helpers for scoped package address overrides
- `type_package_for(...)` / `with_package_context(...)` helpers for exact datatype origins
- One Rust module per Move module (or a flat layout via `RenderOptions::flatten`)
- Move datatypes as Rust types (structs use `#[sui_move::move_struct]` via `sui-move`’s `derive`
feature)
Expand Down Expand Up @@ -102,6 +103,94 @@ bindings::with_packages(localnet_package, localnet_package, || {
});
```

An upgraded package can contain datatypes that originate in different package versions. Calls must
target the current package, while each type tag must retain the package where that datatype was
first defined. Generated call builders resolve their target through `call_package()`, and each
generated datatype resolves its type identity through `type_package_for(module, datatype)`. Use
`with_package_context` to scope both lookups to one release:

```rust
# use std::collections::BTreeMap;
# use sui_sdk_types::Address;
# mod bindings {
# use std::collections::BTreeMap;
# use sui_sdk_types::Address;
# pub type TypeOrigins = BTreeMap<String, BTreeMap<String, Address>>;
# #[derive(Clone)]
# struct PackageContext {
# call_package: Address,
# fallback_type_package: Address,
# origins: TypeOrigins,
# }
# std::thread_local! {
# static CONTEXT: std::cell::RefCell<Option<PackageContext>> =
# const { std::cell::RefCell::new(None) };
# }
# pub fn with_package_context<R>(
# call_package: Address,
# fallback_type_package: Address,
# origins: &TypeOrigins,
# f: impl FnOnce() -> R,
# ) -> R {
# struct Reset(Option<PackageContext>);
# impl Drop for Reset {
# fn drop(&mut self) {
# CONTEXT.with(|slot| {
# slot.replace(self.0.take());
# });
# }
# }
# let previous = CONTEXT.with(|slot| {
# slot.replace(Some(PackageContext {
# call_package,
# fallback_type_package,
# origins: origins.clone(),
# }))
# });
# let _reset = Reset(previous);
# f()
# }
# pub fn call_package() -> Address {
# CONTEXT.with(|slot| slot.borrow().as_ref().unwrap().call_package)
# }
# pub fn type_package_for(module: &str, datatype: &str) -> Address {
# CONTEXT.with(|slot| {
# let context = slot.borrow();
# let context = context.as_ref().unwrap();
# context
# .origins
# .get(module)
# .and_then(|datatypes| datatypes.get(datatype))
# .copied()
# .unwrap_or(context.fallback_type_package)
# })
# }
# }
let initial_package = Address::from_static("0xa1");
let upgraded_package = Address::from_static("0xa2");
let current_package = Address::from_static("0xa3");
let origins = BTreeMap::from([(
"agent".to_owned(),
BTreeMap::from([
("Agent".to_owned(), initial_package),
("AgentStateV2".to_owned(), upgraded_package),
]),
)]);

let (call_package, agent_type_package, state_v2_type_package) =
bindings::with_package_context(current_package, initial_package, &origins, || {
(
bindings::call_package(),
bindings::type_package_for("agent", "Agent"),
bindings::type_package_for("agent", "AgentStateV2"),
)
});

assert_eq!(call_package, current_package);
assert_eq!(agent_type_package, initial_package);
assert_eq!(state_v2_type_package, upgraded_package);
```

## Example: render from an in-memory IR

```rust
Expand Down
17 changes: 14 additions & 3 deletions sui-move-codegen/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ mod tests {
assert!(!code.contains("pub fn package()"));
assert!(!code.contains("pub fn with_package<R>"));
assert!(code.contains("CallTarget::new(call_package(), \"m\", \"mutate\")"));
assert!(code.contains("address_fn = \"super::type_package\""));
assert!(code.contains("address_fn = \"__type_package_for_Obj\""));
}

#[test]
Expand All @@ -746,7 +746,18 @@ mod tests {
assert!(code.contains("pub fn type_package() -> sui_move::prelude::Address"));
assert!(code.contains("pub fn with_packages<R>"));
assert!(code.contains("CallTarget::new(call_package(), \"m\", \"mutate\")"));
assert!(code.contains("address_fn = \"super::type_package\""));
assert!(code.contains("address_fn = \"__type_package_for_Obj\""));
}

#[test]
fn generated_bindings_resolve_each_datatype_origin() {
let code = render_package(&demo_pkg(), &RenderOptions::default());

assert!(code.contains("pub type TypeOrigins"));
assert!(code.contains("pub fn type_package_for("));
assert!(code.contains("pub fn with_package_context<R>"));
assert!(code.contains("type_package_for(\"m\", \"Obj\")"));
assert!(code.contains("type_package_for(\"m\", \"Pair\")"));
}

#[test]
Expand Down Expand Up @@ -973,7 +984,7 @@ mod tests {

let module = parts.modules.get("m").expect("module body");
assert!(module.contains("pub mod m"));
assert!(module.contains("use super::{call_package, type_package};"));
assert!(module.contains("use super::{call_package, type_package_for};"));
assert!(module.contains("pub struct Obj"));
assert!(module.contains("pub fn mutate"));
}
Expand Down
32 changes: 26 additions & 6 deletions sui-move-codegen/src/render/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,37 @@ pub(crate) fn render_datatype(
pkg: &NormalizedPackage,
opts: &RenderOptions,
) -> TokenStream {
let address_function = render_datatype_address_function(dt);
let datatype = match &dt.kind {
DatatypeKind::Struct { fields } => render_struct(dt, fields, pkg, opts),
DatatypeKind::Enum { variants } => render_enum(dt, variants, pkg, opts),
};
let helpers = render_canonical_helpers(dt, pkg, opts);
quote! {
#address_function
#datatype
#helpers
}
}

fn render_datatype_address_function(dt: &Datatype) -> TokenStream {
let function = datatype_address_function_ident(dt);
let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site());
let datatype = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site());

quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
fn #function() -> sui_move::prelude::Address {
type_package_for(#module, #datatype)
}
}
}

fn datatype_address_function_ident(dt: &Datatype) -> syn::Ident {
format_ident!("__type_package_for_{}", dt.name)
}

pub(crate) fn render_type_ref_in_module(
ty: &TypeRef,
current_module: &str,
Expand Down Expand Up @@ -89,11 +109,10 @@ fn render_struct(
let abilities_arg = abilities_lit.map(|lit| quote! { abilities = #lit, });
let phantoms_arg = phantoms_lit.map(|lit| quote! { phantoms = #lit, });
let type_abilities_arg = type_abilities_lit.map(|lit| quote! { type_abilities = #lit, });
let address_fn = if opts.flatten {
syn::LitStr::new("type_package", proc_macro2::Span::call_site())
} else {
syn::LitStr::new("super::type_package", proc_macro2::Span::call_site())
};
let address_fn = syn::LitStr::new(
&datatype_address_function_ident(dt).to_string(),
proc_macro2::Span::call_site(),
);

let fields_tokens = fields.iter().map(|f| {
let ident = idents::ident(&f.name);
Expand Down Expand Up @@ -973,6 +992,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream {

let module = syn::LitStr::new(&dt.type_name.module, proc_macro2::Span::call_site());
let name = syn::LitStr::new(&dt.type_name.name, proc_macro2::Span::call_site());
let address_function = datatype_address_function_ident(dt);

let type_params = type_params_idents(dt.type_parameters.len());
let ty_params_for_tag = type_params
Expand All @@ -981,7 +1001,7 @@ fn struct_tag_builder_tokens(dt: &Datatype, use_aliases: bool) -> TokenStream {

quote! {
#sm::__private::sui_sdk_types::StructTag::new(
type_package(),
#address_function(),
#sm::parse_identifier(#module).expect("invalid module"),
#sm::parse_identifier(#name).expect("invalid struct name"),
vec![#(#ty_params_for_tag),*],
Expand Down
89 changes: 78 additions & 11 deletions sui-move-codegen/src/render/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub(crate) fn render_module_file(

quote! {
#aliases
use super::{call_package, type_package};
use super::{call_package, type_package_for};
#(#items)*
}
}
Expand Down Expand Up @@ -136,7 +136,7 @@ pub(crate) fn render_module(
quote! {
pub mod #module_ident {
#aliases
use super::{call_package, type_package};
use super::{call_package, type_package_for};
#(#items)*
}
}
Expand Down Expand Up @@ -164,11 +164,26 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream {
let _ = opts;
let address_ty = quote! { sui_move::prelude::Address };
quote! {
/// Exact datatype origins grouped by module and datatype name.
pub type TypeOrigins = std::collections::BTreeMap<
String,
std::collections::BTreeMap<String, #address_ty>,
>;

/// Scoped datatype identity for one generated package.
///
/// A single package address cannot represent datatypes introduced by
/// different versions of the same Sui package.
struct TypeScope {
fallback: #address_ty,
origins: TypeOrigins,
}

std::thread_local! {
static CALL_PACKAGE_OVERRIDE: std::cell::Cell<Option<#address_ty>> =
std::cell::Cell::new(None);
static TYPE_PACKAGE_OVERRIDE: std::cell::Cell<Option<#address_ty>> =
std::cell::Cell::new(None);
static TYPE_SCOPE_OVERRIDE: std::cell::RefCell<Option<TypeScope>> =
const { std::cell::RefCell::new(None) };
}

/// Current call package address for this generated binding.
Expand All @@ -184,7 +199,30 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream {
/// Returns the scoped override set by [`with_type_package`] or [`with_packages`], or
/// [`TYPE_PACKAGE`] when no override is active.
pub fn type_package() -> #address_ty {
TYPE_PACKAGE_OVERRIDE.with(|slot| slot.get().unwrap_or(TYPE_PACKAGE))
TYPE_SCOPE_OVERRIDE.with(|slot| {
slot.borrow()
.as_ref()
.map(|scope| scope.fallback)
.unwrap_or(TYPE_PACKAGE)
})
}

/// Resolve the defining package for one Move datatype.
///
/// The exact origin set by [`with_type_origins`] takes precedence over
/// the current fallback returned by [`type_package`].
pub fn type_package_for(module: &str, datatype: &str) -> #address_ty {
TYPE_SCOPE_OVERRIDE.with(|slot| {
let scope = slot.borrow();
let Some(scope) = scope.as_ref() else {
return TYPE_PACKAGE;
};
scope.origins
.get(module)
.and_then(|datatypes| datatypes.get(datatype))
.copied()
.unwrap_or(scope.fallback)
})
}

/// Run a closure with this generated binding scoped to `package` for Move calls.
Expand Down Expand Up @@ -212,18 +250,32 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream {
///
/// The previous type package override is restored when the closure returns or unwinds.
pub fn with_type_package<R>(package: #address_ty, f: impl FnOnce() -> R) -> R {
struct Reset(Option<#address_ty>);
with_type_origins(package, &TypeOrigins::new(), f)
}

/// Run a closure with a fallback package and exact datatype origins.
///
/// The previous type scope is restored when the closure returns or unwinds.
pub fn with_type_origins<R>(
fallback: #address_ty,
origins: &TypeOrigins,
f: impl FnOnce() -> R,
) -> R {
struct Reset(Option<TypeScope>);

impl Drop for Reset {
fn drop(&mut self) {
TYPE_PACKAGE_OVERRIDE.with(|slot| slot.set(self.0));
TYPE_SCOPE_OVERRIDE.with(|slot| {
slot.replace(self.0.take());
});
}
}

let previous = TYPE_PACKAGE_OVERRIDE.with(|slot| {
let previous = slot.get();
slot.set(Some(package));
previous
let previous = TYPE_SCOPE_OVERRIDE.with(|slot| {
slot.replace(Some(TypeScope {
fallback,
origins: origins.clone(),
}))
});
let _reset = Reset(previous);
f()
Expand All @@ -240,6 +292,21 @@ fn package_scope_tokens(opts: &RenderOptions) -> TokenStream {
) -> R {
with_call_package(call_package, || with_type_package(type_package, f))
}

/// Run a closure with the current call package and exact datatype origins.
///
/// Use this when one package contains datatypes introduced by several
/// package versions.
pub fn with_package_context<R>(
call_package: #address_ty,
fallback_type_package: #address_ty,
origins: &TypeOrigins,
f: impl FnOnce() -> R,
) -> R {
with_call_package(call_package, || {
with_type_origins(fallback_type_package, origins, f)
})
}
}
}

Expand Down
Loading