miden_lib/utils/
script_builder.rs

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