use alloc::sync::Arc;
use alloc::vec::Vec;
use miden_protocol::account::AccountComponentCode;
use miden_protocol::assembly::{
Assembler,
DefaultSourceManager,
Library,
Parse,
ParseOptions,
Path,
SourceManagerSync,
};
use miden_protocol::note::NoteScript;
use miden_protocol::transaction::{TransactionKernel, TransactionScript};
use miden_protocol::vm::AdviceMap;
use miden_protocol::{Felt, Word};
use crate::errors::CodeBuilderError;
use crate::standards_lib::StandardsLib;
#[derive(Clone)]
pub struct CodeBuilder {
assembler: Assembler,
source_manager: Arc<dyn SourceManagerSync>,
advice_map: AdviceMap,
}
impl CodeBuilder {
pub fn new() -> Self {
Self::with_source_manager(Arc::new(DefaultSourceManager::default()))
}
pub fn with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Self {
let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone())
.with_dynamic_library(StandardsLib::default())
.expect("linking std lib should work");
Self {
assembler,
source_manager,
advice_map: AdviceMap::default(),
}
}
pub fn with_warnings_as_errors(mut self, yes: bool) -> Self {
self.assembler = self.assembler.with_warnings_as_errors(yes);
self
}
pub fn link_module(
&mut self,
module_path: impl AsRef<str>,
module_code: impl Parse,
) -> Result<(), CodeBuilderError> {
let mut parse_options = ParseOptions::for_library();
parse_options.path = Some(Path::new(module_path.as_ref()).into());
let module = module_code.parse_with_options(self.source_manager(), parse_options).map_err(
|err| CodeBuilderError::build_error_with_report("failed to parse module code", err),
)?;
self.assembler.compile_and_statically_link(module).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to assemble module", err)
})?;
Ok(())
}
pub fn link_static_library(&mut self, library: &Library) -> Result<(), CodeBuilderError> {
self.assembler.link_static_library(library).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to add static library", err)
})
}
pub fn link_dynamic_library(&mut self, library: &Library) -> Result<(), CodeBuilderError> {
self.assembler.link_dynamic_library(library).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to add dynamic library", err)
})
}
pub fn with_statically_linked_library(
mut self,
library: &Library,
) -> Result<Self, CodeBuilderError> {
self.link_static_library(library)?;
Ok(self)
}
pub fn with_dynamically_linked_library(
mut self,
library: impl AsRef<Library>,
) -> Result<Self, CodeBuilderError> {
self.link_dynamic_library(library.as_ref())?;
Ok(self)
}
pub fn with_linked_module(
mut self,
module_path: impl AsRef<str>,
module_code: impl Parse,
) -> Result<Self, CodeBuilderError> {
self.link_module(module_path, module_code)?;
Ok(self)
}
pub fn add_advice_map_entry(&mut self, key: Word, value: impl Into<Vec<Felt>>) {
self.advice_map.insert(key, value.into());
}
pub fn with_advice_map_entry(mut self, key: Word, value: impl Into<Vec<Felt>>) -> Self {
self.add_advice_map_entry(key, value);
self
}
pub fn extend_advice_map(&mut self, advice_map: AdviceMap) {
self.advice_map.extend(advice_map);
}
pub fn with_extended_advice_map(mut self, advice_map: AdviceMap) -> Self {
self.extend_advice_map(advice_map);
self
}
fn apply_advice_map(
advice_map: AdviceMap,
program: miden_protocol::vm::Program,
) -> miden_protocol::vm::Program {
if advice_map.is_empty() {
program
} else {
program.with_advice_map(advice_map)
}
}
fn apply_advice_map_to_library(advice_map: AdviceMap, library: Library) -> Library {
if advice_map.is_empty() {
library
} else {
library.with_advice_map(advice_map)
}
}
pub fn compile_component_code(
self,
component_path: impl AsRef<str>,
component_code: impl Parse,
) -> Result<AccountComponentCode, CodeBuilderError> {
let CodeBuilder { assembler, source_manager, advice_map } = self;
let mut parse_options = ParseOptions::for_library();
parse_options.path = Some(Path::new(component_path.as_ref()).into());
let module =
component_code
.parse_with_options(source_manager, parse_options)
.map_err(|err| {
CodeBuilderError::build_error_with_report("failed to parse component code", err)
})?;
let library = assembler.assemble_library([module]).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to parse component code", err)
})?;
Ok(AccountComponentCode::from(Self::apply_advice_map_to_library(
advice_map,
Arc::unwrap_or_clone(library),
)))
}
pub fn compile_tx_script(
self,
tx_script: impl Parse,
) -> Result<TransactionScript, CodeBuilderError> {
let CodeBuilder { assembler, advice_map, .. } = self;
let program = assembler.assemble_program(tx_script).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to parse transaction script", err)
})?;
Ok(TransactionScript::new(Self::apply_advice_map(advice_map, program)))
}
pub fn compile_note_script(self, source: impl Parse) -> Result<NoteScript, CodeBuilderError> {
let CodeBuilder { assembler, advice_map, .. } = self;
let note_script_lib = assembler.assemble_library([source]).map_err(|err| {
CodeBuilderError::build_error_with_report("failed to parse note script library", err)
})?;
NoteScript::from_library(&Self::apply_advice_map_to_library(
advice_map,
Arc::unwrap_or_clone(note_script_lib),
))
.map_err(|err| {
CodeBuilderError::build_error_with_source(
"failed to create note script from library",
err,
)
})
}
pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
self.source_manager.clone()
}
#[cfg(any(feature = "testing", test))]
pub fn with_kernel_library(source_manager: Arc<dyn SourceManagerSync>) -> Self {
let mut builder = Self::with_source_manager(source_manager);
builder
.link_dynamic_library(&TransactionKernel::library())
.expect("failed to link kernel library");
builder
}
#[cfg(any(feature = "testing", test))]
pub fn with_mock_libraries() -> Self {
Self::with_mock_libraries_with_source_manager(Arc::new(DefaultSourceManager::default()))
}
#[cfg(any(feature = "testing", test))]
pub fn mock_libraries() -> impl Iterator<Item = Library> {
use miden_protocol::account::AccountCode;
use crate::testing::mock_account_code::MockAccountCodeExt;
vec![AccountCode::mock_account_library(), AccountCode::mock_faucet_library()].into_iter()
}
#[cfg(any(feature = "testing", test))]
pub fn with_mock_libraries_with_source_manager(
source_manager: Arc<dyn SourceManagerSync>,
) -> Self {
use crate::testing::mock_util_lib::mock_util_library;
let mut builder = Self::with_source_manager(source_manager);
builder
.link_dynamic_library(&TransactionKernel::library())
.expect("failed to link kernel library");
for library in Self::mock_libraries() {
builder
.link_dynamic_library(&library)
.expect("failed to link mock account libraries");
}
builder
.link_static_library(&mock_util_library())
.expect("failed to link mock util library");
builder
}
}
impl Default for CodeBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<CodeBuilder> for Assembler {
fn from(builder: CodeBuilder) -> Self {
builder.assembler
}
}
#[cfg(test)]
mod tests {
use anyhow::Context;
use miden_protocol::assembly::diagnostics::NamedSource;
use miden_protocol::testing::note::DEFAULT_NOTE_SCRIPT;
use super::*;
#[test]
fn test_code_builder_new() {
let _builder = CodeBuilder::default();
}
#[test]
fn test_code_builder_basic_script_compiling() -> anyhow::Result<()> {
let builder = CodeBuilder::default();
builder
.compile_tx_script("begin nop end")
.context("failed to parse 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::protocol::active_account
use miden::protocol::native_account
use miden::core::sys
pub proc 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 = CodeBuilder::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 parse tx script")?;
Ok(())
}
#[test]
fn test_parse_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::protocol::active_account
use miden::protocol::native_account
use miden::core::sys
pub proc 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 = CodeBuilder::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 parse tx script")?;
let mut builder_with_libs = CodeBuilder::default();
builder_with_libs
.link_module(library_path, account_code)
.context("failed to link first module")?;
builder_with_libs
.link_module("test::lib", "pub proc test nop end")
.context("failed to link second module")?;
builder_with_libs
.compile_tx_script(script_code)
.context("failed to parse 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::protocol::active_account
use miden::protocol::native_account
use miden::core::sys
pub proc 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 = CodeBuilder::default()
.with_linked_module("external_contract::counter_contract", account_code)
.context("failed to link module")?;
builder.compile_tx_script(script_code).context("failed to parse 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 = CodeBuilder::default()
.with_linked_module("test::lib1", "pub proc test1 push.1 add end")
.context("failed to link first module")?
.with_linked_module("test::lib2", "pub proc test2 push.2 add end")
.context("failed to link second module")?;
builder.compile_tx_script(script_code).context("failed to parse tx script")?;
Ok(())
}
#[test]
fn test_static_and_dynamic_linking() -> anyhow::Result<()> {
let script_code = "
use contracts::static_contract
begin
call.static_contract::increment_1
end
";
let account_code_1 = "
pub proc increment_1
push.0 drop
end
";
let account_code_2 = "
pub proc increment_2
push.0 drop
end
";
let temp_assembler = TransactionKernel::assembler();
let static_lib = temp_assembler
.clone()
.assemble_library([NamedSource::new("contracts::static_contract", account_code_1)])
.map_err(|e| anyhow::anyhow!("failed to assemble static library: {}", e))?;
let dynamic_lib = temp_assembler
.assemble_library([NamedSource::new("contracts::dynamic_contract", account_code_2)])
.map_err(|e| anyhow::anyhow!("failed to assemble dynamic library: {}", e))?;
let builder = CodeBuilder::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 parse tx script with static and dynamic libraries")?;
Ok(())
}
#[test]
fn test_code_builder_warnings_as_errors() {
let assembler: Assembler = CodeBuilder::default().with_warnings_as_errors(true).into();
assert!(assembler.warnings_as_errors());
}
#[test]
fn test_code_builder_with_advice_map_entry() -> anyhow::Result<()> {
let key = Word::from([1u32, 2, 3, 4]);
let value = vec![Felt::new(42), Felt::new(43)];
let script = CodeBuilder::default()
.with_advice_map_entry(key, value.clone())
.compile_tx_script("begin nop end")
.context("failed to compile tx script with advice map")?;
let mast = script.mast();
let stored_value = mast.advice_map().get(&key).expect("advice map entry should be present");
assert_eq!(stored_value.as_ref(), value.as_slice());
Ok(())
}
#[test]
fn test_code_builder_extend_advice_map() -> anyhow::Result<()> {
let key1 = Word::from([1u32, 0, 0, 0]);
let key2 = Word::from([2u32, 0, 0, 0]);
let mut advice_map = AdviceMap::default();
advice_map.insert(key1, vec![Felt::new(1)]);
advice_map.insert(key2, vec![Felt::new(2)]);
let script = CodeBuilder::default()
.with_extended_advice_map(advice_map)
.compile_tx_script("begin nop end")
.context("failed to compile tx script")?;
let mast = script.mast();
assert!(mast.advice_map().get(&key1).is_some(), "key1 should be present");
assert!(mast.advice_map().get(&key2).is_some(), "key2 should be present");
Ok(())
}
#[test]
fn test_code_builder_advice_map_in_note_script() -> anyhow::Result<()> {
let key = Word::from([5u32, 6, 7, 8]);
let value = vec![Felt::new(100)];
let script = CodeBuilder::default()
.with_advice_map_entry(key, value.clone())
.compile_note_script(DEFAULT_NOTE_SCRIPT)
.context("failed to compile note script with advice map")?;
let mast = script.mast();
let stored_value = mast
.advice_map()
.get(&key)
.expect("advice map entry should be present in note script");
assert_eq!(stored_value.as_ref(), value.as_slice());
Ok(())
}
#[test]
fn test_code_builder_advice_map_in_component_code() -> anyhow::Result<()> {
let key = Word::from([11u32, 22, 33, 44]);
let value = vec![Felt::new(500)];
let component_code = CodeBuilder::default()
.with_advice_map_entry(key, value.clone())
.compile_component_code("test::component", "pub proc test nop end")
.context("failed to compile component code with advice map")?;
let mast = component_code.mast_forest();
let stored_value = mast
.advice_map()
.get(&key)
.expect("advice map entry should be present in component code");
assert_eq!(stored_value.as_ref(), value.as_slice());
Ok(())
}
}