Public structs and enums
AIP-142: Public Structs and Enums
Section titled “AIP-142: Public Structs and Enums”Summary
Section titled “Summary”Currently, Move enforces strict encapsulation of struct and enum types, allowing their construction, deconstruction, and field access/modification only within the defining module. Although this model works well for structs as resources, it limits expressiveness and modularity of Move programs—particularly when building reusable libraries or composing cross-module abstractions. To address this limitation, we propose extending Move with explicit visibility modifiers for structs and enums, such as public, friend, and package. A public struct or enum would be fully accessible across modules, enabling external code to construct, destruct, pattern-match, inspect, or modify values as if the type were locally defined. Similarly, a package declaration would grant this level of access to all modules declared as friends of this module, while still restricting access from other modules. These visibility controls enhance the expressiveness of Move programs. For instance, an enum Result<T,E> can be defined in the standard library for returning and propagating errors.
module stdlib::result { public enum Result<T, E> { Ok(T), Err(E) }}With Result declared as public, other modules—whether in the same package or in external packages—could return Result values, match on Ok or Err variants, and propagate errors in a clean, idiomatic style, as shown below.
module app::logic { use stdlib::Result; fun run(..) { // Can match type from other module match (prepare(..)) { Result::Ok(x) => execute(x), Result::Err(e) => handle_error(e) } }}With public structs/enums with the copy ability and without the key ability, struct/enum values can be passed into transactions:
module app::entries { public struct Data(String, u64) has copy, drop;
// Can pass struct as transaction argument entry fun run(s: &signer, data: Data) { .. }}Out of scope
Section titled “Out of scope”- Compiler changes (front-end parsing, AST/model support for visibility modifiers, and code generation of struct API wrapper functions) are not covered in this AIP and are documented separately.
- CLI and TypeScript SDK support for public struct/enum transaction arguments are not included in this change and are tracked separately (see developer platform support below).
High-level Overview
Section titled “High-level Overview”- VM changes (guarded by bytecode VERSION_10): new
FunctionAttributevariants to annotate compiler-generated wrappers in the binary format; a newstruct_api_checkerbytecode verifier pass that enforces API integrity at publish and load time; and reference-safety special-casing for mutable borrow wrappers. - Transaction argument support (guarded by
PUBLIC_STRUCT_ENUM_ARGSfeature flag): validation that transaction argument types are public structs/enums withcopyand withoutkeyability; VM-level value construction by invoking the generated pack APIs; and JSON/API support for enum argument types.
Impact
Section titled “Impact”This feature has high impact for the language itself. Without support for public structs and enums, developers are forced to place all code that needs to construct, destruct, or pattern-match over a type into the same module that defines it.
Moreover, ecosystem projects will greatly benefit from public structs/enums as transaction arguments. However, to actually use this feature, SDK, API, and CLI support are required.
Alternative Solutions
Section titled “Alternative Solutions”There are no known viable alternatives for enabling structs and enums to be used directly as transaction arguments. Currently, users must pass individual fields as separate primitive or vector arguments and reconstruct the desired structure within the transaction logic. This approach is cumbersome and results in a poor developer and user experience.
An alternative approach would be to allow direct cross-module access to public structs and enums without generating intermediary APIs. Compared to APIs, direct access will be more efficient with respect to gas usage. However, this would require significant changes to the VM, potentially breaking implicit invariants and introducing serious security risks. The compiler-generated public API approach offers the safest and most controlled path.
Specification and Implementation Details
Section titled “Specification and Implementation Details”Source language extension
Section titled “Source language extension”Extend the language with visibility modifiers public, package, and friend on struct and enum declarations, gated by language version 2.4:
public struct S<T> { ... }
package struct S<T> { ... }
friend struct S<T> { ... }
public enum E<T> { ... }
package enum E<T> { ... }
friend enum E<T> { ... }Semantics
Section titled “Semantics”public: the type can be fully used outside the defining module—constructed, destructed, matched, and field-accessed.package: same access, but restricted to modules within the same package.friend: same access, but restricted to modules declared as friends.- No modifier: remains module-private, as in current Move.
Upgradability
Section titled “Upgradability”The compiler generates APIs for structs/enums with a visibility modifier, so upgradability rules map directly onto the generated function set:
privatestructs/enums can be upgraded topackage,friend, orpublic.friendorpackagestructs/enums can be upgraded topublic.friendorpackagestructs/enums can be downgraded toprivate.- Adding a new variant to a public enum is a compatible upgrade. However, callers using exhaustive match on the old variants will abort with
INCOMPLETE_MATCH_ABORT_CODEat runtime when they encounter the new variant.
- For consistency with function visibility modifier,
public(package)andpublic(friend)are also supported, but these are not preferred. - Structs and enums with the
keyability cannot havepublic,package, orfriendvisibility. Resource operations (move_to,move_from,borrow_global, etc.) are only permitted within the defining module, so exposing akeytype publicly would allow other modules to construct or destruct values of that type while bypassing the intended resource ownership model.
VM changes
Section titled “VM changes”The compiler generates appropriate (public, package, or friend) wrapper functions for each non-private struct or enum, and translates cross-module operations on those types into calls through the wrappers. For a struct S the wrappers are named pack$S, unpack$S, borrow$S$<offset>, borrow_mut$S$<offset>; for an enum E they are pack$E$Variant, unpack$E$Variant, test_variant$E$Variant, and borrow$E$<offset>$<ty_order>. The $ character is reserved in identifiers for these generated names. The VM changes below operate on these wrappers.
FunctionAttribute variants and bytecode format
Section titled “FunctionAttribute variants and bytecode format”To enable the VM and verifier to identify and validate compiler-generated wrapper functions, new FunctionAttribute variants are introduced and encoded in Move bytecode VERSION_10:
| Attribute | Annotates | Description |
|---|---|---|
Pack | pack$S functions | Pack constructor for a non-private struct |
PackVariant(variant_idx) | pack$E$Variant functions | Pack constructor for a non-private enum variant |
Unpack | unpack$S functions | Unpack destructor for a non-private struct |
UnpackVariant(variant_idx) | unpack$E$Variant functions | Unpack destructor for a non-private enum variant |
TestVariant(variant_idx) | test_variant$E$Variant functions | Variant discriminator for non-private enums |
BorrowFieldImmutable(offset) | borrow$... functions | Immutable field borrow; offset is the field’s member index |
BorrowFieldMutable(offset) | borrow_mut$... functions | Mutable field borrow; offset is the field’s member index |
These attributes are serialized/deserialized as part of the Move binary format and are gated to bytecode VERSION_10.
Reference safety and dynamic reference checker special-casing
Section titled “Reference safety and dynamic reference checker special-casing”Normally, simultaneous mutable borrows of two fields through function-call boundaries would be rejected by Move’s static reference-safety checker and flagged at runtime by the dynamic reference checker, because both conservatively assume that two outputs derived from the same mutable input may alias. The borrow_mut$... wrappers would trigger these false-positive violations even though each wrapper borrows a distinct, disjoint field.
To address this, both the static reference-safety checker and the dynamic reference checker are updated to special-case calls to functions carrying a BorrowFieldMutable struct API attribute, treating them as transparent field borrows rather than opaque function calls. This makes the behavior of cross-module field access consistent with module-local field access.
The safety of this special-casing is guaranteed by the struct_api_checker bytecode verifier pass (described below), which strictly validates that every function bearing a BorrowFieldMutable(offset) attribute genuinely performs a single-field mutable borrow at the declared offset and nothing else. Without this integrity guarantee, the special-casing could be exploited to bypass Move’s reference safety guarantees.
Bytecode verification pass: struct_api_checker
Section titled “Bytecode verification pass: struct_api_checker”A new struct_api_checker verifier pass runs at module publish and load time to enforce API integrity. It validates that compiler-generated wrapper functions correctly implement the contracts declared by their FunctionAttribute. Specifically, it checks:
- Name ↔ attribute correspondence (bidirectional): a struct API name (e.g.
pack$S,borrow$S$0) requires the matchingFunctionAttribute, and vice versa. The attribute kind must match the name prefix (pack$↔Pack,borrow$↔BorrowFieldImmutable, etc.). At most one struct API attribute is allowed per function. - Signature: parameters and return types must match the struct/variant field types in order; borrow functions must return a reference with matching mutability.
- Index/offset consistency: the offset or variant index in the function name must equal the attribute value, which must equal the index in the bytecode instruction.
- Bytecode pattern: the function body must be exactly
MoveLoc*+ the corresponding instruction (Pack,Unpack,ImmBorrowField, etc.) +Ret. - BorrowVariantField completeness: a
BorrowVariantFieldinstruction must enumerate all variants sharing the same(offset, type), in ascending order.
Verifier enforcement of upgrade correctness
Section titled “Verifier enforcement of upgrade correctness”The struct_api_checker verifier pass enforces structural consistency of the generated APIs at publish time, catching any attempt to publish an inconsistent upgrade. Because the verifier runs as part of every module publish (and re-publish), these rules apply equally to initial publication and upgrades. A module upgrade that silently breaks struct API integrity cannot be committed to the chain.
Support for public structs/enums as transaction arguments
Section titled “Support for public structs/enums as transaction arguments”Public structs/enums with copy and without key ability can be used as entry function and view function arguments, gated by the PUBLIC_STRUCT_ENUM_ARGS on-chain feature flag.
Eligibility rules
Section titled “Eligibility rules”The Aptos verifier enforces the following rules during transaction argument validation:
- The type must be a
publicstruct or enum, identified by the presence of a publicpack$...function carrying aPackorPackVariantstruct API attribute in the defining module. - The type must have the
copyability and must not havekeyability. - All field types must be valid transaction argument types.
- Types that are never allowed as transaction arguments (e.g., function types) must not appear anywhere in the type’s fields unless being used as phantom types.
Value construction
Section titled “Value construction”When the VM processes entry function arguments that include a public struct/enum value:
- The VM locates the appropriate
pack$...function using thePackorPackVariantstruct API attribute. - It calls the pack function with the deserialized field values to materialize the struct/enum value.
- A pack-function cache is maintained to avoid repeated lazy-load gas charges across argument construction calls.
- DoS-bounding: the maximum number of pack function invocations is currently capped to 32 (subject to change) to prevent abuse when
PUBLIC_STRUCT_ENUM_ARGSis enabled.
Reference Implementation
Section titled “Reference Implementation”Feature flag: PUBLIC_STRUCT_ENUM_ARGS gates transaction argument support for public structs/enums.
- PR #18117 – VM: FunctionAttribute variants, bytecode VERSION_10
- PR #18165 – VM:
struct_api_checkerverifier pass - PR #18489 – Transaction argument support
Testing
Section titled “Testing”- Transactional tests in move-vm covering all
struct_api_checkervalidation rules: name–attribute correspondence, bytecode pattern matching, variant completeness, field offset completeness, and mutability consistency. - E2E tests in
public_structs_enums_upgrade.rscovering:- Cross-module struct operations: pack/unpack, borrow/borrow_mut, nested structs, generics, vectors, phantom type parameters.
- Cross-module enum operations: all variants, pattern matching, variant testing.
- Enum upgrade compatibility: adding a new variant and observing
INCOMPLETE_MATCH_ABORT_CODEfor unhandled exhaustive matches.
- API tests covering JSON-to-BCS enum conversion, generic entry function argument handling and rejection paths (non-public, non-copy types).
Risks and Drawbacks
Section titled “Risks and Drawbacks”This feature introduces conceptual risks that require careful consideration. In particular, exposing public structs and enums across module boundaries expands the surface area for misuse and could unintentionally violate internal invariants if not used carefully. While the design avoids deep VM changes by relying on compiler-generated APIs, it still touches sensitive components such as the verifier, binary format (VERSION_10), and transaction argument deserialization, all of which must be audited thoroughly. Additionally, there is a risk of overuse: developers may default to marking types as public for convenience, leading to tightly coupled modules, reduced encapsulation and module size increase. Leveraging linter might be able to mitigate this concern.
Security Considerations
Section titled “Security Considerations”Bytecode verification and reference safety. The struct_api_checker verifier pass is the critical security boundary for this feature. By strictly validating at publish time that every function bearing a struct API attribute implements exactly the declared operation—and nothing else—it prevents malicious modules from abusing the reference-safety and dynamic reference checker special-casing introduced for borrow_mut$... wrappers. Without this pass, an attacker could publish a function carrying a BorrowFieldMutable attribute that performs arbitrary operations, and the VM’s reference checkers would treat it as a trusted transparent borrow, potentially allowing unsafe aliasing or bypassing Move’s resource safety guarantees. The verifier pass must be thoroughly audited to ensure its validation rules are both sound and complete.
Transaction argument safety. Only public structs and enums with the copy ability and without the key ability are permitted as transaction arguments. The copy requirement is the key safety constraint: it ensures that no resource type (which by definition lacks copy) can ever be constructed from transaction arguments. This prevents attackers from conjuring resource values—such as coins or capability tokens—out of thin air by passing crafted arguments to an entry function. Field types are recursively validated with the same rules, so a resource type cannot be smuggled in through a nested field either. The overall effect is that the transaction argument path can only produce values that are freely duplicable and disposable, preserving Move’s resource safety invariants.
Timeline
Section titled “Timeline”Suggested implementation timeline
Section titled “Suggested implementation timeline”TBD
Suggested developer platform support
Section titled “Suggested developer platform support”- Aptos CLI (PR #18591): adds support for passing public struct/enum values as transaction arguments via
-json-fileinaptos move runandaptos move view. Includes an async struct/enum argument parser with on-chain ABI fetching and caching, special handling for common framework types (String,Object<T>,FixedPoint*,Option<T>), and CLI e2e tests. - TypeScript SDK (aptos-ts-sdk PR #824): adds
MoveStructArgumentandMoveEnumArgumentBCS-serializable types and aStructEnumArgumentParserthat fetches module ABIs from the REST API to encode struct/enum arguments. Supports nested structs/enums (up to 7 levels), generic type parameter substitution, all Move primitive types, and framework wrapper types. Argument conversion functions are now async to support ABI fetching.