-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Core Script Builder Codegen Logic #14168
Conversation
⏱️ 4m total CI duration on this PR
🚨 1 job on the last run was significantly faster/slower than expected
|
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub(crate) struct Script { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hack as aptos_types
cannot be compiled into WASM, so I have to avoid using it as a dependency.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #14168 +/- ##
=========================================
+ Coverage 59.8% 60.0% +0.2%
=========================================
Files 853 857 +4
Lines 207949 210898 +2949
=========================================
+ Hits 124354 126555 +2201
- Misses 83595 84343 +748 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some high level comments
@@ -0,0 +1,489 @@ | |||
// Copyright © Aptos Foundation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if we should move this into move-binary-format
. A builder library would have a lot of use cases besides the scope of the intent engine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think parts of the code can potentially go to move-binary-format? But this would require some refactoring to be able to handle both module and script so I would avoid doing that in this PR.
@@ -0,0 +1,235 @@ | |||
// Copyright © Aptos Foundation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this approach. One question though: when do we decide to run this decompiler? When we see some special flag in the metadata? I'm just thinking more about the unhappy path where you have something that isn't really an intent script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also playing the devil's advocate: how do you envision the long term maintainability of this mini decompiler if we continue to add more functionalities?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very good point. Since the decompiler will be served as part of the full node we need extra eyes on this logic to make sure it won't crash anything.
For longer term maintainability I think we need to maintain the tests to make sure the round trip equality holds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1/n Took a look at codegen.rs
. Guess my main hope is for this to become a more general bytecode manipulation library other clients can also depend on.
if let Some(result) = self.address_pool.get(address) { | ||
return Ok(*result); | ||
} | ||
if self.script.address_identifiers.len() >= TableIndex::MAX as usize { | ||
return Err(PartialVMError::new(StatusCode::INDEX_OUT_OF_BOUNDS)); | ||
} | ||
let return_idx = AddressIdentifierIndex(self.script.address_identifiers.len() as u16); | ||
self.script.address_identifiers.push(*address); | ||
self.address_pool.insert(*address, return_idx); | ||
Ok(return_idx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can use the entry()
API to save a lookup.
It might also be worth coming up with some higher order helpers to perform the common "get or insert" op. For reference, here's some similar stuff I did in my gov proposal simulation PR
fn get_or_add<T: PartialEq>(pool: &mut Vec<T>, val: T) -> usize {
match pool.iter().position(|elem| elem == &val) {
Some(idx) => idx,
None => {
let idx = pool.len();
pool.push(val);
idx
},
}
}
return Ok(*result); | ||
} | ||
if self.script.module_handles.len() >= TableIndex::MAX as usize { | ||
return Err(PartialVMError::new(StatusCode::INDEX_OUT_OF_BOUNDS)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we have a history of abusing vm errors in tools, but wonder if we would benefit from this not repeating the same pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be fine this case since all errors will be turned into anyhow error here? Although I agree that we could remove those deps. WDYT?
use serde::{Deserialize, Serialize}; | ||
use std::collections::BTreeMap; | ||
|
||
#[derive(Default)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to make this a general-use library, then I feel like we'll need to come up with a way to make it work with an existing non-empty CompiledScript/Module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok now I see your point. Basically you want to get rid of the intent codegen logic and move the logic to the builder, so that what's remaining would be purely reusable code for manipulating compiled script.
if let Some(result) = self.address_pool.get(address) { | ||
return Ok(*result); | ||
} | ||
if self.script.address_identifiers.len() >= TableIndex::MAX as usize { | ||
return Err(PartialVMError::new(StatusCode::INDEX_OUT_OF_BOUNDS)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the logic here assumes that if something is not in the hashmap then it also does not exist in the script. Fine if we're starting from an empty script, but personally I'd like to see this becoming a general library we can use to modify CompiledModule
and CompiledScript
Err(PartialVMError::new(StatusCode::LOOKUP_FAILED)) | ||
} | ||
|
||
fn compile_batched_call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if this belongs to builder.rs
} | ||
|
||
#[derive(Serialize, Deserialize)] | ||
pub(crate) struct Script { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, feel like it might be helpful to separate out for CompiledScript
building and (Aptos) transaction script generation. The added modularity will benefit other use cases that requires bytecode manipulation.
function_pool: BTreeMap<(ModuleHandleIndex, IdentifierIndex), FunctionHandleIndex>, | ||
struct_pool: BTreeMap<(ModuleHandleIndex, IdentifierIndex), StructHandleIndex>, | ||
signature_pool: BTreeMap<Signature, SignatureIndex>, | ||
script: CompiledScript, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If timing permits, I'd love to see this evolving into a builder that supports both CompiledScript
and CompiledModule
. This doesn't mean you need to add all the import functions though -- those can be added on-demand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few superficial comments. Still digesting...
Token::Name(s) => ModuleId::new(AccountAddress::from_hex_literal(&addr)?, Identifier::new(s)?), | ||
tok => bail!("unexpected token {:?}, expected string", tok), | ||
} | ||
tok => bail!("unexpected token {:?}, expected string", tok), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should you mention that ::
is also allowed here in your error?
Token::Name(s) => ModuleId::new(AccountAddress::from_hex_literal(&addr)?, Identifier::new(s)?), | ||
Token::ColonColon => match self.next()? { | ||
Token::Name(s) => ModuleId::new(AccountAddress::from_hex_literal(&addr)?, Identifier::new(s)?), | ||
tok => bail!("unexpected token {:?}, expected string", tok), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string --> identifier
below, also.
fn parse_module_id(&mut self) -> Result<ModuleId> { | ||
Ok(match self.next()? { | ||
Token::Address(addr) => match self.next()? { | ||
Token::Name(s) => ModuleId::new(AccountAddress::from_hex_literal(&addr)?, Identifier::new(s)?), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you allow Token::Address
followed by Token::Name
with nothing in between? That seems odd.
let (module, handle) = find_function(&self.modules, module_id, func_name)?; | ||
let mut returns = vec![]; | ||
for (idx, sig) in module.signature_at(handle.return_).0.iter().enumerate() { | ||
let call_idx = (self.calls.len() - 1) as u16; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if self.calls.len()
is 0 this underflows and behavior differs between debug and release mode. Not a great idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return_values will always be called after self.calls
is pushed so we should be fine. With that said tho it's always good to add an extra check here.
aptos-move/aptos-intent/Cargo.toml
Outdated
|
||
[dev-dependencies] | ||
e2e-move-tests = { path = "../e2e-move-tests" } | ||
aptos-types = { workspace = true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume the funky symbol means there's no trailing \n
on this file? I think you need one to be POSIX-compliant.
0968d9b
to
6d820a3
Compare
vm_status::StatusCode, | ||
}; | ||
|
||
fn get_or_add<T: PartialEq>(pool: &mut Vec<T>, val: T) -> PartialVMResult<usize> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit sad to see the caching gone here. Without caching it could be O(n^2)
complexity to build a complex script. If another component on the production path picks this up it could also run into time complexity issues unexpectedly, albeit not at the same severity as the resource viewer.
This issue is stale because it has been open 45 days with no activity. Remove the |
01b0823
to
12d67fd
Compare
ba4e83c
to
03d36cd
Compare
03d36cd
to
f28aaed
Compare
Cargo.toml
Outdated
@@ -31,6 +31,7 @@ members = [ | |||
"aptos-move/aptos-vm-types", | |||
"aptos-move/aptos-workspace-server", | |||
"aptos-move/block-executor", | |||
"aptos-move/dynamic-transaction-composer", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that important, but I think on this level transaction-composer
would be good enough, 'dynamic' is more like an extra marketing attribute. Moving forward, we will just call it on technical level the 'transaction composer' anyway, three words too long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll rename this folder after a stamp. Brian suggested that renaming will dismiss previous commments
@@ -0,0 +1,498 @@ | |||
// Copyright © Aptos Foundation | |||
// SPDX-License-Identifier: Apache-2.0 | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A short module description? We try to do this in the compiler now and it is really helpful. (//! doc
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added description.
} | ||
|
||
#[wasm_bindgen(js_name = BatchArgument)] | ||
/// Arguments for each function. Wasm bindgen only support C-style enum so use option to work around. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite understanding this restriction. At https://rustwasm.github.io/wasm-bindgen/?search=enum%20t I couldn't find anything either. Looks like bindgen supports the full JS value system, is then perhaps a matter to use serde to convert some of our Rust data types to JS before we pass it out? Please elaborate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct Context { | ||
script: CompiledScript, | ||
// Arguments to the compiled script. | ||
args: Vec<Vec<u8>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite understand what this here is. The BCS of a set of concrete arguments? Why would this be on this level?
I would the compiled script expected to be parametric over a set of formal parameters, so it can be stored and executed many times with different parameters.
move-binary-format = { workspace = true } | ||
move-bytecode-verifier = { workspace = true } | ||
move-core-types = { workspace = true } | ||
reqwest = { version = "*", features = ["blocking"] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not workspace here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason why I'm avoiding workspace is this repo will be compiled into WASM and thus might have different dependencies and I don't want to pollute the main workspace because of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I started to think how we deal with THAT. As a matter of fact, you have to pull in significant other parts of the code which already have workspace dependencies. We need a more general solution for this problem. I'm not sure whether there is something like 'dependency override' in cargo. Or perhaps for the WASM build, we need to generate the build config by our own script, that is generate source for a crate then compile it. For now I think this should be workspace too, it doesn't appear to solve the problem this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still needs resolution. Not using workspace doesn't help here. Also "*" is not our practice, should be a concrete version.
ba6f516
to
1cec6a9
Compare
1cec6a9
to
e78b872
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1/3 The third-party/move
changes all look good to me
self.add_signature(sig) | ||
} | ||
|
||
pub fn add_signature(&mut self, signature: Signature) -> PartialVMResult<SignatureIndex> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@runtian-zhou can correct me if I'm wrong, but it seems like import_
is specifically for taking things from another module (see they all take compiled module + index as arguments), while here add_signature
doesn't care about the origin of the signature added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2/3 Builder
The main logic looks good. Some questions on the implementation details
Also wondering if you plan to offer a textual representation for the input. It could be a simple register-based DSL
(a, b) = foo();
bar(a);
baz(&b, &b);
Argument::Raw(bytes) => { | ||
let new_local_idx = self.parameters_ty.len() as u16; | ||
self.parameters_ty.push(ty); | ||
self.parameters.push(bytes); | ||
arguments.push(AllocatedLocal { | ||
op_type: ArgumentOperation::Move, | ||
is_parameter: true, | ||
local_idx: new_local_idx, | ||
}) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to allocate a local for this argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another related question: do we worry about running out of locals? There are only 256 of them IIRC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO there are two ways to do it:
- Import the argument into constant pool
- Assign a parameter location and pass it as argument to the script.
I would personally prefer 2 because it makes script caching a bit easier, as the script body part would remain the same if there's a list of commonly composed script.
On running out of locals, this is indeed a concern. There are only 256 of them and we need it for # of signers + # of parameters + # of returns. I don't want to over optimize for it rightaway but I don't see a clear path getting rid of the constraint rightway either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would personally prefer 2 because it makes script caching a bit easier, as the script body part would remain the same if there's a list of commonly composed script.
So all Argument::Raw
are exposed as script parameters? If so, I find the idea both intriguing and worrying. It's good that we may be able to cache more stuff, but it also feels like a very leaky abstraction -- users may be surprised by the extra parameters the builder inserted.
There are only 256 of them and we need it for # of signers + # of parameters + # of returns.
I was more thinking about returns. Wondering if we could make more use of the stack. Agreed it's not necessarily something we need to optimize at this moment though.
I don't want to over optimize for it rightaway but I don't see a clear path getting rid of the constraint rightway either.
Relaxing it at the file format/vm level is probably not that hard (but still a nasty chore). @wrwg just curious, would the bytecode verifier have trouble with, let's say, 1024 locals?
Argument::Local(AllocatedLocal { | ||
op_type: ArgumentOperation::Move, | ||
is_parameter: false, | ||
local_idx: idx, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually, I wonder if you should return to the caller a "location" without the operation type, and allow them to specify their intent later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nice thing here is that the api would be defaulted to Move behavior. I think this will make the TS sdk interface look very clean as they can pass the returned value directly as arugment to the next call, which actually aligns with the logic here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your reasoning here. Although I wonder if this is better done at a higher level (e.g. the register DSL I mentioned). Anyway, not a blocker for the current implemetation
} | ||
|
||
/// Load up a module from a remote endpoint. Will need to invoke this function prior to the call. | ||
pub async fn load_module( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against the fetching modules from remote, but I wonder if we should provide an option for supplying them (or ABIs) locally, potentially for saving the turnaround time + better service availability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3/3 Decompiler looks good. Although I didn't have the mental capacity to check if the (local_idx -> prev_result
) remapping is done 100% correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please indicate clearer what comments are resolved, most of my previous comments have not been acked or resolved. I'd prefer to not figuring this out by reading everything again.
serde = { workspace = true } | ||
serde_bytes = { workspace = true } | ||
serde_json = { workspace = true } | ||
tsify-next = "0.5.4" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
} | ||
|
||
#[wasm_bindgen] | ||
pub struct BatchedFunctionCallBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address
use wasm_bindgen::prelude::*; | ||
|
||
#[wasm_bindgen] | ||
#[derive(Tsify, Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc comments on all public entities, please.
6ad13e6
to
b1d0201
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Let's get the first version landed
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] | ||
pub struct PreviousResult { | ||
/// Refering to the return value in the `call_idx`th call. | ||
call_idx: u16, | ||
/// Refering to the `return_idx`th return value in that call, since move function call can | ||
/// return multiple values. | ||
return_idx: u16, | ||
/// How this result would be used. | ||
operation_type: ArgumentOperation, | ||
} | ||
|
||
/// Arguments to the `MoveFunctionCall`. | ||
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, Tsify)] | ||
#[tsify(into_wasm_abi, from_wasm_abi)] | ||
pub enum CallArgument { | ||
/// Passing raw bytes to the function. The bytes must follows the existing constraints for | ||
/// transaction arguments. | ||
Raw(Vec<u8>), | ||
/// Refering to signer of the transaction. If it's a single signer transaction you will only | ||
/// be able to access `Signer(0)`. You will be able to access other signers if it's a multi | ||
/// agent transaction. | ||
Signer(u16), | ||
/// The arugment came from the returned value of a previous `MoveFunctionCall`. | ||
PreviousResult(PreviousResult), | ||
} | ||
|
||
/// How to consume the returned value coming from a previous `MoveFunctionCall`. | ||
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] | ||
pub enum ArgumentOperation { | ||
/// Move the returned value to the next caller. This can be used for values that don't have | ||
/// `copy` ability. | ||
Move, | ||
/// Copy the returned value and pass it to the next caller. | ||
Copy, | ||
/// Borrow an immutable reference from a returned value and pass it to the next caller. | ||
Borrow, | ||
/// Borrow a mutable reference from a returned value and pass it to the next caller. | ||
BorrowMut, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the changes here.
061598e
to
43be219
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
✅ Forge suite
|
✅ Forge suite
|
✅ Forge suite
|
Description
Implemented a library to
Type of Change
Which Components or Systems Does This Change Impact?
How Has This Been Tested?
Published round trip tests to make sure they can be compiled and decompiled correctly.
Key Areas to Review
For the decompilation logic, we will be hosting this logic as part of the api service so it's important to make sure it will not crash.
Checklist