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}