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}