miden_lib/utils/
script_builder.rs

1use alloc::string::String;
2use alloc::sync::Arc;
3
4use miden_objects::assembly::diagnostics::NamedSource;
5use miden_objects::assembly::{
6    Assembler,
7    DefaultSourceManager,
8    Library,
9    LibraryPath,
10    SourceManagerSync,
11};
12use miden_objects::note::NoteScript;
13use miden_objects::transaction::TransactionScript;
14
15use crate::errors::ScriptBuilderError;
16use crate::transaction::TransactionKernel;
17
18// SCRIPT BUILDER
19// ================================================================================================
20
21/// A builder for compiling note scripts and transaction scripts with optional library dependencies.
22///
23/// The ScriptBuilder simplifies the process of creating transaction scripts by providing:
24/// - A clean API for adding multiple libraries with static or dynamic linking
25/// - Automatic assembler configuration with all added libraries
26/// - Debug mode support
27/// - Builder pattern support for method chaining
28///
29/// ## Static vs Dynamic Linking
30///
31/// **Static Linking** (`link_static_library()` / `with_statically_linked_library()`):
32/// - Use when you control and know the library code
33/// - The library code is copied into the script code
34/// - Best for most user-written libraries and dependencies
35/// - Results in larger script size but ensures the code is always available
36///
37/// **Dynamic Linking** (`link_dynamic_library()` / `with_dynamically_linked_library()`):
38/// - Use when making Foreign Procedure Invocation (FPI) calls
39/// - The library code is available on-chain and referenced, not copied
40/// - Results in smaller script size but requires the code to be available on-chain
41///
42/// ## Typical Workflow
43///
44/// 1. Create a new ScriptBuilder with debug mode preference
45/// 2. Add any required modules using `link_module()` or `with_linked_module()`
46/// 3. Add libraries using `link_static_library()` / `link_dynamic_library()` as appropriate
47/// 4. Compile your script with `compile_note_script()` or `compile_tx_script()`
48///
49/// Note that the compilation methods consume the ScriptBuilder, so if you need to compile
50/// multiple scripts with the same configuration, you should clone the builder first.
51///
52/// ## Builder Pattern Example
53///
54/// ```no_run
55/// # use anyhow::Context;
56/// # use miden_lib::utils::ScriptBuilder;
57/// # use miden_objects::assembly::Library;
58/// # use miden_stdlib::StdLibrary;
59/// # fn example() -> anyhow::Result<()> {
60/// # let module_code = "export.test push.1 add end";
61/// # let script_code = "begin nop end";
62/// # // Create sample libraries for the example
63/// # let my_lib = StdLibrary::default().into(); // Convert StdLibrary to Library
64/// # let fpi_lib = StdLibrary::default().into();
65/// let script = ScriptBuilder::default()
66///     .with_linked_module("my::module", module_code).context("failed to link module")?
67///     .with_statically_linked_library(&my_lib).context("failed to link static library")?
68///     .with_dynamically_linked_library(&fpi_lib).context("failed to link dynamic library")?  // For FPI calls
69///     .compile_tx_script(script_code).context("failed to compile tx script")?;
70/// # Ok(())
71/// # }
72/// ```
73///
74/// # Note
75/// The ScriptBuilder automatically includes the `miden` and `std` libraries, which
76/// provide access to transaction kernel procedures. Due to being available on-chain
77/// these libraries are linked dynamically and do not add to the size of built script.
78#[derive(Clone)]
79pub struct ScriptBuilder {
80    assembler: Assembler,
81    source_manager: Arc<dyn SourceManagerSync>,
82}
83
84impl ScriptBuilder {
85    // CONSTRUCTORS
86    // --------------------------------------------------------------------------------------------
87
88    /// Creates a new ScriptBuilder with the specified debug mode.
89    ///
90    /// # Arguments
91    /// * `in_debug_mode` - Whether to enable debug mode in the assembler
92    pub fn new(in_debug_mode: bool) -> Self {
93        let source_manager = Arc::new(DefaultSourceManager::default());
94        let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone())
95            .with_debug_mode(in_debug_mode);
96        Self { assembler, source_manager }
97    }
98
99    /// Creates a new ScriptBuilder with the specified source manager.
100    ///
101    /// The returned builder is instantiated with debug mode enabled.
102    ///
103    /// # Arguments
104    /// * `source_manager` - The source manager to use with the internal `Assembler`
105    pub fn with_source_manager(source_manager: Arc<dyn SourceManagerSync>) -> Self {
106        let assembler = TransactionKernel::assembler_with_source_manager(source_manager.clone())
107            .with_debug_mode(true);
108        Self { assembler, source_manager }
109    }
110
111    // LIBRARY MANAGEMENT
112    // --------------------------------------------------------------------------------------------
113
114    /// Compiles and links a module to the script builder.
115    ///
116    /// This method compiles the provided module code and adds it directly to the assembler
117    /// for use in script compilation.
118    ///
119    /// # Arguments
120    /// * `module_path` - The path identifier for the module (e.g., "my_lib::my_module")
121    /// * `module_code` - The source code of the module to compile and link
122    ///
123    /// # Errors
124    /// Returns an error if:
125    /// - The module path is invalid
126    /// - The module code cannot be parsed
127    /// - The module cannot be assembled
128    pub fn link_module(
129        &mut self,
130        module_path: impl AsRef<str>,
131        module_code: impl AsRef<str>,
132    ) -> Result<(), ScriptBuilderError> {
133        // Parse the library path
134        let lib_path = LibraryPath::new(module_path.as_ref()).map_err(|err| {
135            ScriptBuilderError::build_error_with_source(
136                format!("invalid module path: {}", module_path.as_ref()),
137                err,
138            )
139        })?;
140
141        let module = NamedSource::new(format!("{lib_path}"), String::from(module_code.as_ref()));
142
143        self.assembler.compile_and_statically_link(module).map_err(|err| {
144            ScriptBuilderError::build_error_with_report("failed to assemble module", err)
145        })?;
146
147        Ok(())
148    }
149
150    /// Statically links the given library.
151    ///
152    /// Static linking means the library code is copied into the script code.
153    /// Use this for most libraries that are not available on-chain.
154    ///
155    /// # Arguments
156    /// * `library` - The compiled library to statically link
157    ///
158    /// # Errors
159    /// Returns an error if:
160    /// - adding the library to the assembler failed
161    pub fn link_static_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> {
162        self.assembler.link_static_library(library).map_err(|err| {
163            ScriptBuilderError::build_error_with_report("failed to add static library", err)
164        })
165    }
166
167    /// Dynamically links a library.
168    ///
169    /// This is useful to dynamically link the [`Library`] of a foreign account
170    /// that is invoked using foreign procedure invocation (FPI). Its code is available
171    /// on-chain and so it does not have to be copied into the script code.
172    ///
173    /// For all other use cases not involving FPI, link the library statically.
174    ///
175    /// # Arguments
176    /// * `library` - The compiled library to dynamically link
177    ///
178    /// # Errors
179    /// Returns an error if the library cannot be added to the assembler
180    pub fn link_dynamic_library(&mut self, library: &Library) -> Result<(), ScriptBuilderError> {
181        self.assembler.link_dynamic_library(library).map_err(|err| {
182            ScriptBuilderError::build_error_with_report("failed to add dynamic library", err)
183        })
184    }
185
186    /// Builder-style method to statically link a library and return the modified builder.
187    ///
188    /// This enables method chaining for convenient builder patterns.
189    ///
190    /// # Arguments
191    /// * `library` - The compiled library to statically link
192    ///
193    /// # Errors
194    /// Returns an error if the library cannot be added to the assembler
195    pub fn with_statically_linked_library(
196        mut self,
197        library: &Library,
198    ) -> Result<Self, ScriptBuilderError> {
199        self.link_static_library(library)?;
200        Ok(self)
201    }
202
203    /// Builder-style method to dynamically link a library and return the modified builder.
204    ///
205    /// This enables method chaining for convenient builder patterns.
206    ///
207    /// # Arguments
208    /// * `library` - The compiled library to dynamically link
209    ///
210    /// # Errors
211    /// Returns an error if the library cannot be added to the assembler
212    pub fn with_dynamically_linked_library(
213        mut self,
214        library: &Library,
215    ) -> Result<Self, ScriptBuilderError> {
216        self.link_dynamic_library(library)?;
217        Ok(self)
218    }
219
220    /// Builder-style method to link a module and return the modified builder.
221    ///
222    /// This enables method chaining for convenient builder patterns.
223    ///
224    /// # Arguments
225    /// * `module_path` - The path identifier for the module (e.g., "my_lib::my_module")
226    /// * `module_code` - The source code of the module to compile and link
227    ///
228    /// # Errors
229    /// Returns an error if the module cannot be compiled or added to the assembler
230    pub fn with_linked_module(
231        mut self,
232        module_path: impl AsRef<str>,
233        module_code: impl AsRef<str>,
234    ) -> Result<Self, ScriptBuilderError> {
235        self.link_module(module_path, module_code)?;
236        Ok(self)
237    }
238
239    // SCRIPT COMPILATION
240    // --------------------------------------------------------------------------------------------
241
242    /// Compiles a transaction script with the provided program code.
243    ///
244    /// The compiled script will have access to all modules that have been added to this builder.
245    ///
246    /// # Arguments
247    /// * `program` - The transaction script source code
248    ///
249    /// # Errors
250    /// Returns an error if:
251    /// - The transaction script compilation fails
252    pub fn compile_tx_script(
253        self,
254        tx_script: impl AsRef<str>,
255    ) -> Result<TransactionScript, ScriptBuilderError> {
256        let assembler = self.assembler;
257
258        let program = assembler.assemble_program(tx_script.as_ref()).map_err(|err| {
259            ScriptBuilderError::build_error_with_report("failed to compile transaction script", err)
260        })?;
261        Ok(TransactionScript::new(program))
262    }
263
264    /// Compiles a note script with the provided program code.
265    ///
266    /// The compiled script will have access to all modules that have been added to this builder.
267    ///
268    /// # Arguments
269    /// * `program` - The note script source code
270    ///
271    /// # Errors
272    /// Returns an error if:
273    /// - The note script compilation fails
274    pub fn compile_note_script(
275        self,
276        program: impl AsRef<str>,
277    ) -> Result<NoteScript, ScriptBuilderError> {
278        let assembler = self.assembler;
279
280        let program = assembler.assemble_program(program.as_ref()).map_err(|err| {
281            ScriptBuilderError::build_error_with_report("failed to compile note script", err)
282        })?;
283        Ok(NoteScript::new(program))
284    }
285
286    // ACCESSORS
287    // --------------------------------------------------------------------------------------------
288
289    /// Access the [`Assembler`]'s [`SourceManagerSync`].
290    pub fn source_manager(&self) -> Arc<dyn SourceManagerSync> {
291        self.source_manager.clone()
292    }
293
294    // TESTING CONVENIENCE FUNCTIONS
295    // --------------------------------------------------------------------------------------------
296
297    /// Returns a [`ScriptBuilder`] with the `mock::{account, faucet, util}` libraries.
298    ///
299    /// This script builder includes:
300    /// - [`MockAccountCodeExt::mock_account_library`][account_lib],
301    /// - [`MockAccountCodeExt::mock_faucet_library`][faucet_lib],
302    /// - [`mock_util_library`][util_lib]
303    ///
304    /// [account_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_account_library
305    /// [faucet_lib]: crate::testing::mock_account_code::MockAccountCodeExt::mock_faucet_library
306    /// [util_lib]: crate::testing::mock_util_lib::mock_util_library
307    #[cfg(any(feature = "testing", test))]
308    pub fn with_mock_libraries() -> Result<Self, ScriptBuilderError> {
309        use miden_objects::account::AccountCode;
310
311        use crate::testing::mock_account_code::MockAccountCodeExt;
312        use crate::testing::mock_util_lib::mock_util_library;
313
314        Self::new(true)
315            .with_dynamically_linked_library(&AccountCode::mock_account_library())?
316            .with_dynamically_linked_library(&AccountCode::mock_faucet_library())?
317            .with_statically_linked_library(&mock_util_library())
318    }
319}
320
321impl Default for ScriptBuilder {
322    fn default() -> Self {
323        Self::new(true)
324    }
325}
326
327// TESTS
328// ================================================================================================
329
330#[cfg(test)]
331mod tests {
332    use anyhow::Context;
333
334    use super::*;
335
336    #[test]
337    fn test_script_builder_new() {
338        let _builder = ScriptBuilder::default();
339        // Test that the builder can be created successfully
340    }
341
342    #[test]
343    fn test_script_builder_basic_script_compilation() -> anyhow::Result<()> {
344        let builder = ScriptBuilder::default();
345        builder
346            .compile_tx_script("begin nop end")
347            .context("failed to compile basic tx script")?;
348        Ok(())
349    }
350
351    #[test]
352    fn test_create_library_and_create_tx_script() -> anyhow::Result<()> {
353        let script_code = "
354            use.external_contract::counter_contract
355
356            begin
357                call.counter_contract::increment
358            end
359        ";
360
361        let account_code = "
362            use.miden::active_account
363            use.miden::native_account
364            use.std::sys
365
366            export.increment
367                push.0
368                exec.active_account::get_item
369                push.1 add
370                push.0
371                exec.native_account::set_item
372                exec.sys::truncate_stack
373            end
374        ";
375
376        let library_path = "external_contract::counter_contract";
377
378        let mut builder_with_lib = ScriptBuilder::default();
379        builder_with_lib
380            .link_module(library_path, account_code)
381            .context("failed to link module")?;
382        builder_with_lib
383            .compile_tx_script(script_code)
384            .context("failed to compile tx script")?;
385
386        Ok(())
387    }
388
389    #[test]
390    fn test_compile_library_and_add_to_builder() -> anyhow::Result<()> {
391        let script_code = "
392            use.external_contract::counter_contract
393
394            begin
395                call.counter_contract::increment
396            end
397        ";
398
399        let account_code = "
400            use.miden::active_account
401            use.miden::native_account
402            use.std::sys
403
404            export.increment
405                push.0
406                exec.active_account::get_item
407                push.1 add
408                push.0
409                exec.native_account::set_item
410                exec.sys::truncate_stack
411            end
412        ";
413
414        let library_path = "external_contract::counter_contract";
415
416        // Test single library
417        let mut builder_with_lib = ScriptBuilder::default();
418        builder_with_lib
419            .link_module(library_path, account_code)
420            .context("failed to link module")?;
421        builder_with_lib
422            .compile_tx_script(script_code)
423            .context("failed to compile tx script")?;
424
425        // Test multiple libraries
426        let mut builder_with_libs = ScriptBuilder::default();
427        builder_with_libs
428            .link_module(library_path, account_code)
429            .context("failed to link first module")?;
430        builder_with_libs
431            .link_module("test::lib", "export.test nop end")
432            .context("failed to link second module")?;
433        builder_with_libs
434            .compile_tx_script(script_code)
435            .context("failed to compile tx script with multiple libraries")?;
436
437        Ok(())
438    }
439
440    #[test]
441    fn test_builder_style_chaining() -> anyhow::Result<()> {
442        let script_code = "
443            use.external_contract::counter_contract
444
445            begin
446                call.counter_contract::increment
447            end
448        ";
449
450        let account_code = "
451            use.miden::active_account
452            use.miden::native_account
453            use.std::sys
454
455            export.increment
456                push.0
457                exec.active_account::get_item
458                push.1 add
459                push.0
460                exec.native_account::set_item
461                exec.sys::truncate_stack
462            end
463        ";
464
465        // Test builder-style chaining with modules
466        let builder = ScriptBuilder::default()
467            .with_linked_module("external_contract::counter_contract", account_code)
468            .context("failed to link module")?;
469
470        builder.compile_tx_script(script_code).context("failed to compile tx script")?;
471
472        Ok(())
473    }
474
475    #[test]
476    fn test_multiple_chained_modules() -> anyhow::Result<()> {
477        let script_code =
478            "use.test::lib1 use.test::lib2 begin exec.lib1::test1 exec.lib2::test2 end";
479
480        // Test chaining multiple modules
481        let builder = ScriptBuilder::default()
482            .with_linked_module("test::lib1", "export.test1 push.1 add end")
483            .context("failed to link first module")?
484            .with_linked_module("test::lib2", "export.test2 push.2 add end")
485            .context("failed to link second module")?;
486
487        builder.compile_tx_script(script_code).context("failed to compile tx script")?;
488
489        Ok(())
490    }
491
492    #[test]
493    fn test_static_and_dynamic_linking() -> anyhow::Result<()> {
494        let script_code = "
495            use.external_contract::contract_1
496            use.external_contract::contract_2
497
498            begin
499                call.contract_1::increment_1
500                call.contract_2::increment_2
501            end
502        ";
503
504        let account_code_1 = "
505            use.miden::active_account
506            use.miden::native_account
507            use.std::sys
508
509            export.increment_1
510                push.0
511                exec.active_account::get_item
512                push.1 add
513                push.0
514                exec.native_account::set_item
515                exec.sys::truncate_stack
516            end
517        ";
518
519        let account_code_2 = "
520            use.miden::active_account
521            use.miden::native_account
522            use.std::sys
523
524            export.increment_2
525                push.0
526                exec.active_account::get_item
527                push.2 add
528                push.0
529                exec.native_account::set_item
530                exec.sys::truncate_stack
531            end
532        ";
533
534        // Create libraries using the assembler
535        let temp_assembler = TransactionKernel::assembler();
536
537        let static_lib = temp_assembler
538            .clone()
539            .assemble_library([NamedSource::new("external_contract::contract_1", account_code_1)])
540            .map_err(|e| anyhow::anyhow!("failed to assemble static library: {}", e))?;
541
542        let dynamic_lib = temp_assembler
543            .assemble_library([NamedSource::new("external_contract::contract_2", account_code_2)])
544            .map_err(|e| anyhow::anyhow!("failed to assemble dynamic library: {}", e))?;
545
546        // Test linking both static and dynamic libraries
547        let builder = ScriptBuilder::default()
548            .with_statically_linked_library(&static_lib)
549            .context("failed to link static library")?
550            .with_dynamically_linked_library(&dynamic_lib)
551            .context("failed to link dynamic library")?;
552
553        builder
554            .compile_tx_script(script_code)
555            .context("failed to compile tx script with static and dynamic libraries")?;
556
557        Ok(())
558    }
559}