Skip to main content

miden_standards/code_builder/
mod.rs

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