use alloc::string::String;
use alloc::sync::Arc;
use miden_objects::assembly::diagnostics::NamedSource;
use miden_objects::assembly::{
Assembler,
DefaultSourceManager,
Library,
LibraryPath,
SourceManagerSync,
};
use miden_objects::note::NoteScript;
use miden_objects::transaction::TransactionScript;
use crate::errors::ScriptBuilderError;
use crate::transaction::TransactionKernel;
#[derive(Clone)]
pub struct ScriptBuilder {
assembler: Assembler,
source_manager: Arc<dyn SourceManagerSync>,
}
impl ScriptBuilder {
pub fn new(in_debug_mode: bool) -> Self {
let source_manager = Arc::new(DefaultSourceManager::default());
let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone())
.with_debug_mode(in_debug_mode);
Self { assembler, source_manager }
}
pub fn with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Self {
let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone())
.with_debug_mode(true);
Self { assembler, source_manager }
}
pub fn link_module(
&mut self,
module_path: impl AsRef<str>,
module_code: impl AsRef<str>,
) -> Result<(), ScriptBuilderError> {
let lib_path = LibraryPath::new(module_path.as_ref()).map_err(|err| {
ScriptBuilderError::build_error_with_source(
format!("invalid module path: {}", module_path.as_ref()),
err,
)
})?;
let module = NamedSource::new(format!("{lib_path}"), String::from(module_code.as_ref()));
self.assembler.compile_and_statically_link(module).map_err(|err| {
ScriptBuilderError::build_error_with_report("failed to assemble module", err)
})?;
Ok(())
}
pub fn link_static_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> {
self.assembler.link_static_library(library).map_err(|err| {
ScriptBuilderError::build_error_with_report("failed to add static library", err)
})
}
pub fn link_dynamic_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> {
self.assembler.link_dynamic_library(library).map_err(|err| {
ScriptBuilderError::build_error_with_report("failed to add dynamic library", err)
})
}
pub fn with_statically_linked_library(
mut self,
library: &Library,
) -> Result<Self, ScriptBuilderError> {
self.link_static_library(library)?;
Ok(self)
}
pub fn with_dynamically_linked_library(
mut self,
library: &Library,
) -> Result<Self, ScriptBuilderError> {
self.link_dynamic_library(library)?;
Ok(self)
}
pub fn with_linked_module(
mut self,
module_path: impl AsRef<str>,
module_code: impl AsRef<str>,
) -> Result<Self, ScriptBuilderError> {
self.link_module(module_path, module_code)?;
Ok(self)
}
pub fn compile_tx_script(
self,
tx_script: impl AsRef<str>,
) -> Result<TransactionScript, ScriptBuilderError> {
let assembler = self.assembler;
let program = assembler.assemble_program(tx_script.as_ref()).map_err(|err| {
ScriptBuilderError::build_error_with_report("failed to compile transaction script", err)
})?;
Ok(TransactionScript::new(program))
}
pub fn compile_note_script(
self,
program: impl AsRef<str>,
) -> Result<NoteScript, ScriptBuilderError> {
let assembler = self.assembler;
let program = assembler.assemble_program(program.as_ref()).map_err(|err| {
ScriptBuilderError::build_error_with_report("failed to compile note script", err)
})?;
Ok(NoteScript::new(program))
}
pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
self.source_manager.clone()
}
#[cfg(any(feature = "testing", test))]
pub fn with_mock_libraries() -> Result<Self, ScriptBuilderError> {
use miden_objects::account::AccountCode;
use crate::testing::mock_account_code::MockAccountCodeExt;
use crate::testing::mock_util_lib::mock_util_library;
Self::new(true)
.with_dynamically_linked_library(&AccountCode::mock_account_library())?
.with_dynamically_linked_library(&AccountCode::mock_faucet_library())?
.with_statically_linked_library(&mock_util_library())
}
}
impl Default for ScriptBuilder {
fn default() -> Self {
Self::new(true)
}
}
#[cfg(test)]
mod tests {
use anyhow::Context;
use super::*;
#[test]
fn test_script_builder_new() {
let _builder = ScriptBuilder::default();
}
#[test]
fn test_script_builder_basic_script_compilation() -> anyhow::Result<()> {
let builder = ScriptBuilder::default();
builder
.compile_tx_script("begin nop end")
.context("failed to compile basic tx script")?;
Ok(())
}
#[test]
fn test_create_library_and_create_tx_script() -> anyhow::Result<()> {
let script_code = "
use.external_contract::counter_contract
begin
call.counter_contract::increment
end
";
let account_code = "
use.miden::active_account
use.miden::native_account
use.std::sys
export.increment
push.0
exec.active_account::get_item
push.1 add
push.0
exec.native_account::set_item
exec.sys::truncate_stack
end
";
let library_path = "external_contract::counter_contract";
let mut builder_with_lib = ScriptBuilder::default();
builder_with_lib
.link_module(library_path, account_code)
.context("failed to link module")?;
builder_with_lib
.compile_tx_script(script_code)
.context("failed to compile tx script")?;
Ok(())
}
#[test]
fn test_compile_library_and_add_to_builder() -> anyhow::Result<()> {
let script_code = "
use.external_contract::counter_contract
begin
call.counter_contract::increment
end
";
let account_code = "
use.miden::active_account
use.miden::native_account
use.std::sys
export.increment
push.0
exec.active_account::get_item
push.1 add
push.0
exec.native_account::set_item
exec.sys::truncate_stack
end
";
let library_path = "external_contract::counter_contract";
let mut builder_with_lib = ScriptBuilder::default();
builder_with_lib
.link_module(library_path, account_code)
.context("failed to link module")?;
builder_with_lib
.compile_tx_script(script_code)
.context("failed to compile tx script")?;
let mut builder_with_libs = ScriptBuilder::default();
builder_with_libs
.link_module(library_path, account_code)
.context("failed to link first module")?;
builder_with_libs
.link_module("test::lib", "export.test nop end")
.context("failed to link second module")?;
builder_with_libs
.compile_tx_script(script_code)
.context("failed to compile tx script with multiple libraries")?;
Ok(())
}
#[test]
fn test_builder_style_chaining() -> anyhow::Result<()> {
let script_code = "
use.external_contract::counter_contract
begin
call.counter_contract::increment
end
";
let account_code = "
use.miden::active_account
use.miden::native_account
use.std::sys
export.increment
push.0
exec.active_account::get_item
push.1 add
push.0
exec.native_account::set_item
exec.sys::truncate_stack
end
";
let builder = ScriptBuilder::default()
.with_linked_module("external_contract::counter_contract", account_code)
.context("failed to link module")?;
builder.compile_tx_script(script_code).context("failed to compile tx script")?;
Ok(())
}
#[test]
fn test_multiple_chained_modules() -> anyhow::Result<()> {
let script_code =
"use.test::lib1 use.test::lib2 begin exec.lib1::test1 exec.lib2::test2 end";
let builder = ScriptBuilder::default()
.with_linked_module("test::lib1", "export.test1 push.1 add end")
.context("failed to link first module")?
.with_linked_module("test::lib2", "export.test2 push.2 add end")
.context("failed to link second module")?;
builder.compile_tx_script(script_code).context("failed to compile tx script")?;
Ok(())
}
#[test]
fn test_static_and_dynamic_linking() -> anyhow::Result<()> {
let script_code = "
use.external_contract::contract_1
use.external_contract::contract_2
begin
call.contract_1::increment_1
call.contract_2::increment_2
end
";
let account_code_1 = "
use.miden::active_account
use.miden::native_account
use.std::sys
export.increment_1
push.0
exec.active_account::get_item
push.1 add
push.0
exec.native_account::set_item
exec.sys::truncate_stack
end
";
let account_code_2 = "
use.miden::active_account
use.miden::native_account
use.std::sys
export.increment_2
push.0
exec.active_account::get_item
push.2 add
push.0
exec.native_account::set_item
exec.sys::truncate_stack
end
";
let temp_assembler = TransactionKernel::assembler();
let static_lib = temp_assembler
.clone()
.assemble_library([NamedSource::new("external_contract::contract_1", account_code_1)])
.map_err(|e| anyhow::anyhow!("failed to assemble static library: {}", e))?;
let dynamic_lib = temp_assembler
.assemble_library([NamedSource::new("external_contract::contract_2", account_code_2)])
.map_err(|e| anyhow::anyhow!("failed to assemble dynamic library: {}", e))?;
let builder = ScriptBuilder::default()
.with_statically_linked_library(&static_lib)
.context("failed to link static library")?
.with_dynamically_linked_library(&dynamic_lib)
.context("failed to link dynamic library")?;
builder
.compile_tx_script(script_code)
.context("failed to compile tx script with static and dynamic libraries")?;
Ok(())
}
}