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}