midenc_codegen_masm/artifact.rs
1use alloc::sync::Arc;
2use core::{fmt, ops::ControlFlow};
3
4use miden_assembly::{Path, ProjectSourceInputs, ast::InvocationTarget};
5use miden_core::Word;
6use midenc_hir::{constants::ConstantData, dialects::builtin, interner::Symbol};
7use midenc_session::{
8 Emit, OutputMode, OutputType, Session, Writer,
9 diagnostics::{IntoDiagnostic, Report, SourceSpan, Span, WrapErr},
10};
11
12use crate::{Event, lower::NativePtr, masm};
13
14pub struct MasmComponent {
15 pub id: Option<builtin::ComponentId>,
16 /// True if [`Self::id`] belongs to a component the compiler invented to wrap a bare core
17 /// module, rather than one an author wrote — see
18 /// [`builtin::Component::SYNTHETIC_WRAPPER_ATTR`], which is where this comes from.
19 pub synthetic_wrapper: bool,
20 /// The path of the root module for this component
21 ///
22 /// All components must have a canonical root module, even if empty
23 pub root: Arc<Path>,
24 /// The symbol name of the component initializer function
25 ///
26 /// This function is responsible for initializing global variables and writing data segments
27 /// into memory at program startup, and at cross-context call boundaries (in callee prologue).
28 pub init: Option<masm::InvocationTarget>,
29 /// The symbol name of the program entrypoint, if this component is executable.
30 ///
31 /// If unset, it indicates that the component is a library, even if it could be made executable.
32 pub entrypoint: Option<masm::InvocationTarget>,
33 /// A private copy of the selected canonical-ABI entrypoint without its component `init`
34 /// prologue.
35 ///
36 /// This is present only when a component with a core Wasm start is compiled as an executable
37 /// through a canonical-ABI wrapper. Generated `main` invokes `init` itself, then any test
38 /// harness initialization, then this copy. The public wrapper retains its normal `init`
39 /// prologue for fresh-context calls.
40 pub executable_entrypoint_without_init: Option<masm::Procedure>,
41 /// The rodata segments of this component keyed by the offset of the segment
42 pub rodata: Vec<Rodata>,
43 /// The address of the start of the global heap
44 pub heap_base: u32,
45 /// The address of the `__stack_pointer` global, if such a global has been defined
46 pub stack_pointer: Option<u32>,
47 /// The set of modules in this component
48 pub modules: Vec<Arc<masm::Module>>,
49}
50
51impl Emit for MasmComponent {
52 fn name(&self) -> Option<Symbol> {
53 None
54 }
55
56 fn output_type(&self, _mode: OutputMode) -> OutputType {
57 OutputType::Masm
58 }
59
60 fn write_to<W: Writer>(
61 &self,
62 mut writer: W,
63 mode: OutputMode,
64 _session: &Session,
65 ) -> anyhow::Result<()> {
66 if mode != OutputMode::Text {
67 anyhow::bail!("masm emission does not support binary mode");
68 }
69 writer.write_fmt(core::format_args!("{self}"))?;
70 Ok(())
71 }
72}
73
74/// Represents a read-only data segment, combined with its content digest
75#[derive(Clone, PartialEq, Eq)]
76pub struct Rodata {
77 /// The component to which this read-only data segment belongs
78 pub component: builtin::ComponentId,
79 /// The content digest computed for `data`
80 pub digest: Word,
81 /// The address at which the data for this segment begins
82 pub start: NativePtr,
83 /// The raw binary data for this segment
84 pub data: Arc<ConstantData>,
85}
86impl fmt::Debug for Rodata {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.debug_struct("Rodata")
89 .field("digest", &format_args!("{}", self.digest))
90 .field("start", &self.start)
91 .field_with("data", |f| {
92 f.debug_struct("ConstantData")
93 .field("len", &self.data.len())
94 .finish_non_exhaustive()
95 })
96 .finish()
97 }
98}
99impl Rodata {
100 pub fn size_in_bytes(&self) -> usize {
101 self.data.len()
102 }
103
104 pub fn size_in_felts(&self) -> usize {
105 self.data.len().div_ceil(4)
106 }
107
108 pub fn size_in_words(&self) -> usize {
109 self.size_in_felts().div_ceil(4)
110 }
111
112 /// Attempt to convert this rodata object to its equivalent representation in felts
113 ///
114 /// See [Self::bytes_to_elements] for more details.
115 pub fn to_elements(&self) -> Vec<miden_processor::Felt> {
116 Self::bytes_to_elements(self.data.as_slice())
117 }
118
119 /// Attempt to convert the given bytes to their equivalent representation in felts
120 ///
121 /// The resulting felts will be in padded out to the nearest number of words, i.e. if the data
122 /// only takes up 3 felts worth of bytes, then the resulting `Vec` will contain 4 felts, so that
123 /// the total size is a valid number of words.
124 pub fn bytes_to_elements(bytes: &[u8]) -> Vec<miden_processor::Felt> {
125 use miden_processor::Felt;
126
127 let mut felts = Vec::with_capacity(bytes.len() / 4);
128 let mut iter = bytes.iter().copied().array_chunks::<4>();
129 felts.extend(
130 iter.by_ref().map(|chunk| Felt::new_unchecked(u32::from_le_bytes(chunk) as u64)),
131 );
132 let remainder = iter.into_remainder();
133 if remainder.len() > 0 {
134 let mut chunk = [0u8; 4];
135 for (i, byte) in remainder.enumerate() {
136 chunk[i] = byte;
137 }
138 felts.push(Felt::new_unchecked(u32::from_le_bytes(chunk) as u64));
139 }
140
141 let size_in_felts = bytes.len().div_ceil(4);
142 let size_in_words = size_in_felts.div_ceil(4);
143 let padding = (size_in_words * 4).abs_diff(felts.len());
144 felts.resize(felts.len() + padding, Felt::ZERO);
145 debug_assert_eq!(felts.len() % 4, 0, "expected to be a valid number of words");
146 felts
147 }
148}
149
150inventory::submit! {
151 midenc_session::CompileFlag::new("test_harness")
152 .long("test-harness")
153 .action(midenc_session::FlagAction::SetTrue)
154 .help("If present, causes the code generator to emit extra code for the VM test harness")
155 .help_heading("Testing")
156}
157
158impl fmt::Display for MasmComponent {
159 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
160 for module in self.modules.iter() {
161 writeln!(f, "{module}")?;
162 }
163 Ok(())
164 }
165}
166
167impl MasmComponent {
168 pub fn source_inputs(
169 &self,
170 target: &midenc_session::miden_project::Target,
171 session: &Session,
172 ) -> Result<ProjectSourceInputs, Report> {
173 let is_executable_target = target.is_executable();
174 let emit_test_harness = session.get_flag("test_harness");
175 let mut support = Vec::with_capacity(self.modules.len());
176 let mut root = None;
177 for module in self.modules.iter() {
178 if module.path() == self.root.as_ref() {
179 root = Some(Box::new(Arc::unwrap_or_clone(module.clone())));
180 continue;
181 }
182
183 support.push(Box::new(Arc::unwrap_or_clone(module.clone())));
184 }
185
186 if is_executable_target && let Some(entrypoint) = self.entrypoint.as_ref() {
187 // Our generated main module takes precedence here, so move the root module into support
188 support.extend(root);
189 let root =
190 self.generate_main(entrypoint, emit_test_harness, session.source_manager.clone())?;
191 return Ok(ProjectSourceInputs { root, support });
192 }
193
194 let mut root = root.expect("components must always have a root module");
195
196 // A library-like target's root module must sit exactly at the target's namespace, or
197 // the assembler rejects the whole target (`load_target_sources`). Two of the shapes that
198 // reach here can never satisfy that on their own, and both for the same reason: the root
199 // code generation gave them is not a name any source declares, so no namespace derived
200 // from the source can equal it. See [`Self::has_no_authored_identity`].
201 //
202 // Re-rooting is correct rather than merely expedient, for that same reason. The target's
203 // namespace is the name the author *did* choose, and is where they expect this library's
204 // procedures to be addressable from; the root being replaced is one the compiler picked
205 // on their behalf and never told them about. A component whose id its author wrote is
206 // left exactly where it is: that id is part of the code's own identity, and moving it
207 // would silently rename the procedures every dependent addresses.
208 //
209 // Nothing is done here for an executable, which is handled above: its root is discarded
210 // in favor of the generated `$exec` module, so its namespace already agrees.
211 //
212 // The equality check is what keeps a target that *already* agrees from being rewritten to
213 // itself. That is the ordinary case for a component-less world with one top-level module,
214 // whose root is that module's name and whose synthesized namespace is read from the very
215 // same declaration; it is also the case for a manifest that declares
216 // `namespace = "root_ns:root@1.0.0"`, which is how projects worked around the wrapper
217 // before it was fixed.
218 //
219 // Only the modules handed to the assembler move. `MasmComponent`'s own `root`, `init` and
220 // `entrypoint` still name the old root afterwards, which is why a library's `--emit=masm`
221 // document (written from the component, not from these inputs) shows it while the
222 // assembled package uses the target's namespace. Neither field has a consumer on this
223 // path — `init` is invoked symbolically from within the component, and `entrypoint` is
224 // only read by `generate_main` on the executable branch above — so they are left as they
225 // are rather than rewritten to no effect.
226 let namespace = target.namespace.inner();
227 if self.has_no_authored_identity() && self.root.as_ref() != namespace.as_ref() {
228 let mut rebase = Rebase {
229 from: &self.root,
230 to: namespace,
231 };
232 rebase.apply(&mut root);
233 for module in support.iter_mut() {
234 rebase.apply(module);
235 }
236 }
237
238 Ok(ProjectSourceInputs { root, support })
239 }
240
241 /// Returns true if this component declares no identity its author chose, and so belongs
242 /// wherever its target says rather than where code generation put it.
243 ///
244 /// Two shapes qualify:
245 ///
246 /// - **The synthetic wrapper** the Wasm frontend builds around every *core* Wasm module
247 /// (`frontend/wasm`'s `build_ir_component`). Its identity is the same for every such build
248 /// and carries no information about this one, and `ComponentId::to_library_path` renders it
249 /// as the single quoted component `"root_ns:root@1.0.0"` — a spelling no target is named.
250 /// - **A world declaring no component**, which has no id at all. Its modules "belong to one
251 /// logical component, which has no identity beyond the namespace those modules sit in"
252 /// (`world_body_to_masm_component`), so lowering has to invent a root: the placeholder
253 /// constant `::init` for zero or several top-level modules, and that module's own name for
254 /// exactly one.
255 ///
256 /// A component whose id its author wrote is the complement, and is left where it is: that id
257 /// is part of the code's identity, and moving it would rename the procedures every dependent
258 /// addresses.
259 ///
260 /// # Why one top-level module is not carved out
261 ///
262 /// `::{module}` *is* a name the file says, so it is the one root here that could arguably be
263 /// preserved. It is not, for three reasons.
264 ///
265 /// First, it costs the ordinary case nothing: preparation synthesizes that target's namespace
266 /// by reading the very same declaration (`hir_declared_namespace` in `midenc-compile`), so
267 /// the two agree and the equality guard in [`Self::source_inputs`] makes the rewrite a no-op.
268 /// Second, `MasmComponent` has no way to tell the two roots apart — nothing records how many
269 /// top-level modules the world had — so carving it out would mean either comparing `root`
270 /// against the literal `"::init"`, which is exactly the one-value-in-two-places duplication
271 /// preparation refused, or threading a flag down from lowering for a case that is a no-op.
272 /// Third, a world is not a component: a module's name says where its procedures sit *within*
273 /// a namespace, not what that namespace is, so a target that names a different one is not
274 /// contradicting the file the way a component id would be.
275 fn has_no_authored_identity(&self) -> bool {
276 self.id.is_none() || self.synthetic_wrapper
277 }
278
279 /// Generate an executable module which when run expects the raw data segment data to be
280 /// provided on the advice stack in the same order as initialization, and the operands of
281 /// the entrypoint function on the operand stack.
282 fn generate_main(
283 &self,
284 entrypoint: &InvocationTarget,
285 emit_test_harness: bool,
286 source_manager: Arc<dyn midenc_session::SourceManager>,
287 ) -> Result<Box<masm::Module>, Report> {
288 use masm::{Instruction as Inst, Op};
289
290 let mut exe = Box::new(masm::Module::new_executable());
291 let span = SourceSpan::default();
292 let mut invoked = Vec::new();
293 let entrypoint = if let Some(procedure) = self.executable_entrypoint_without_init.as_ref() {
294 let target = InvocationTarget::Symbol(procedure.name().as_ident());
295 exe.define_procedure(procedure.clone(), source_manager.clone())
296 .into_diagnostic()
297 .wrap_err("failed to define executable entrypoint without init")?;
298 target
299 } else {
300 entrypoint.clone()
301 };
302 let body = {
303 let mut block = masm::Block::new(span, Vec::with_capacity(64));
304 // Invoke component initializer, if present
305 if let Some(init) = self.init.as_ref() {
306 invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, init.clone()));
307 block.push(Op::Inst(Span::new(span, Inst::Exec(init.clone()))));
308 }
309
310 // Initialize test harness, if requested
311 if emit_test_harness {
312 self.emit_test_harness(&mut block);
313 }
314
315 // Invoke the program entrypoint
316 block.push(Op::Inst(Span::new(
317 span,
318 Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
319 )));
320 invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, entrypoint.clone()));
321 block.push(Op::Inst(Span::new(span, Inst::Exec(entrypoint))));
322 block.push(Op::Inst(Span::new(
323 span,
324 Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
325 )));
326
327 // Truncate the stack to 16 elements on exit
328 let truncate_stack = {
329 let name = masm::ProcedureName::new("truncate_stack").unwrap();
330 let module = masm::LibraryPath::new("::miden::core::sys").unwrap();
331 let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
332 InvocationTarget::Path(Span::new(span, qualified.into_inner()))
333 };
334 invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
335 block.push(Op::Inst(Span::new(span, Inst::Exec(truncate_stack))));
336 block
337 };
338 let mut start = masm::Procedure::new(
339 span,
340 masm::Visibility::Public,
341 masm::ProcedureName::main(),
342 0,
343 body,
344 );
345 start.extend_invoked(invoked);
346 exe.define_procedure(start, source_manager)
347 .into_diagnostic()
348 .wrap_err("failed to define executable `main` procedure")?;
349 Ok(exe)
350 }
351
352 fn emit_test_harness(&self, block: &mut masm::Block) {
353 use masm::{Instruction as Inst, IntValue, Op, PushValue};
354 use miden_core::Felt;
355
356 let span = SourceSpan::default();
357
358 let pipe_words_to_memory = {
359 let name = masm::ProcedureName::new("pipe_words_to_memory").unwrap();
360 let module = masm::LibraryPath::new("::miden::core::mem").unwrap();
361 let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
362 InvocationTarget::Path(Span::new(span, qualified.into_inner()))
363 };
364
365 // Step 1: Get the number of initializers to run
366 // => [inits] on operand stack
367 block.push(Op::Inst(Span::new(span, Inst::AdvPush)));
368
369 // Step 2: Evaluate the initial state of the loop condition `inits > 0`
370 // => [inits, inits]
371 block.push(Op::Inst(Span::new(span, Inst::Dup0)));
372 // => [inits > 0, inits]
373 block.push(Op::Inst(Span::new(span, Inst::Push(PushValue::Int(IntValue::U8(0)).into()))));
374 block.push(Op::Inst(Span::new(span, Inst::Gt)));
375
376 // Step 3: Loop until `inits == 0`
377 let mut loop_body = Vec::with_capacity(16);
378
379 // State of operand stack on entry to `loop_body`: [inits]
380 // State of advice stack on entry to `loop_body`: [dest_ptr, num_words, ...]
381 //
382 // Step 3a: Compute next value of `inits`, i.e. `inits'`
383 // => [inits - 1]
384 loop_body.push(Op::Inst(Span::new(span, Inst::SubImm(Felt::ONE.into()))));
385
386 // Step 3b: Copy initializer data to memory
387 // => [num_words, dest_ptr, inits']
388 loop_body.push(Op::Inst(Span::new(span, Inst::AdvPush)));
389 loop_body.push(Op::Inst(Span::new(span, Inst::AdvPush)));
390 // => [C, B, A, dest_ptr, inits'] on operand stack
391 loop_body.push(Op::Inst(Span::new(
392 span,
393 Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
394 )));
395 loop_body.push(Op::Inst(Span::new(span, Inst::Exec(pipe_words_to_memory))));
396 loop_body.push(Op::Inst(Span::new(
397 span,
398 Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
399 )));
400 // Drop C, B, A
401 loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
402 loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
403 loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
404 // => [inits']
405 loop_body.push(Op::Inst(Span::new(span, Inst::Drop)));
406
407 // Step 3c: Evaluate loop condition `inits' > 0`
408 // => [inits', inits']
409 loop_body.push(Op::Inst(Span::new(span, Inst::Dup0)));
410 // => [inits' > 0, inits']
411 loop_body
412 .push(Op::Inst(Span::new(span, Inst::Push(PushValue::Int(IntValue::U8(0)).into()))));
413 loop_body.push(Op::Inst(Span::new(span, Inst::Gt)));
414
415 // Step 4: Enter (or skip) loop
416 block.push(Op::While {
417 span,
418 body: masm::Block::new(span, loop_body),
419 });
420
421 // Step 5: Drop `inits` after loop is evaluated
422 block.push(Op::Inst(Span::new(span, Inst::Drop)));
423 }
424}
425
426/// Moves a component's modules from one root path to another, in place.
427///
428/// A component's modules are *nested under* its root — code generation defines them relative to
429/// it (`MasmComponentBuilder::define_module`) — and the calls between them are emitted as
430/// absolute paths carrying that same root. So moving the root is not a matter of renaming one
431/// module: every module path and every intra-component invocation target has to move with it, or
432/// the root ends up declaring submodules that do not exist and the procedures end up calling
433/// modules that are no longer there.
434///
435/// Paths that are not under `from` — the intrinsics and the core library, notably — are left
436/// alone, which is what confines this to the component's own modules.
437struct Rebase<'a> {
438 from: &'a Path,
439 to: &'a Path,
440}
441
442impl Rebase<'_> {
443 /// Move `module`, and everything it refers to within the component, under [`Self::to`].
444 fn apply(&mut self, module: &mut masm::Module) {
445 use masm::visit::VisitMut;
446
447 if let Some(path) = self.rebase(module.path()) {
448 module.set_path(&path);
449 }
450 // The rewrite below never breaks out of the walk, so there is no outcome to inspect.
451 let _ = self.visit_mut_module(module);
452 }
453
454 /// The path `path` becomes under [`Self::to`], or `None` if it is not under [`Self::from`].
455 fn rebase(&self, path: &Path) -> Option<masm::LibraryPath> {
456 path.strip_prefix(self.from).map(|rest| self.to.join(rest))
457 }
458
459 /// Move `target` under [`Self::to`] if it names something in the component, reporting whether
460 /// it did.
461 fn rebase_target(&self, target: &mut InvocationTarget) -> bool {
462 let InvocationTarget::Path(path) = target else {
463 return false;
464 };
465 let Some(rebased) = self.rebase(path.inner()) else {
466 return false;
467 };
468 *path = Span::new(path.span(), Arc::from(rebased.into_boxed_path()));
469 true
470 }
471
472 /// Replace `procedure` with an equivalent one whose recorded callees are `invoked`.
473 ///
474 /// A procedure carries a set of the callees code generation emitted for it, and the linker
475 /// resolves every entry in that set to build the call graph — so an entry naming a module
476 /// that has moved fails the link with "undefined item", even though the body it was derived
477 /// from now says otherwise. The set can be added to (`extend_invoked`) but not pruned from
478 /// outside the syntax crate, hence rebuilding rather than editing in place.
479 ///
480 /// WARNING: rebuilding means enumerating everything a `Procedure` carries, so **a field added
481 /// to `miden_assembly_syntax::ast::Procedure` upstream is silently dropped here** — for the
482 /// procedures whose callees moved, on the live path, with no compile error and nothing
483 /// mechanical to catch it (`Procedure`'s hand-written `PartialEq` already omits `span` and
484 /// `invoked`, so even a round-trip equality check would not reliably notice). The field list
485 /// below was audited against **miden-assembly-syntax 0.25.8** and is complete for that
486 /// version; re-audit it when that dependency is bumped. The real fix is a `clear_invoked` on
487 /// `Procedure` upstream, which would make this whole function unnecessary.
488 fn replace_invoked(procedure: &mut masm::Procedure, invoked: Vec<masm::Invoke>) {
489 use masm::Spanned;
490
491 let span = procedure.span();
492 let body = core::mem::replace(procedure.body_mut(), masm::Block::new(span, Vec::new()));
493 let mut rebuilt = masm::Procedure::new(
494 span,
495 procedure.visibility(),
496 procedure.name().clone(),
497 procedure.num_locals(),
498 body,
499 )
500 .with_docs(procedure.docs().map(|docs| docs.map(alloc::string::String::from)))
501 .with_attributes(procedure.attributes().iter().cloned());
502 rebuilt.set_syscall(procedure.is_syscall());
503 if let Some(signature) = procedure.signature() {
504 rebuilt.set_signature(signature.clone());
505 }
506 rebuilt.extend_invoked(invoked);
507 *procedure = rebuilt;
508 }
509}
510
511impl masm::visit::VisitMut for Rebase<'_> {
512 /// Every call-like instruction reaches this, as `exec`, `call`, `syscall` and `procref` all
513 /// funnel through it.
514 fn visit_mut_invoke_target(&mut self, target: &mut InvocationTarget) -> ControlFlow<()> {
515 self.rebase_target(target);
516 ControlFlow::Continue(())
517 }
518
519 /// The body is rewritten by the default walk; a procedure's *recorded* callees are not
520 /// reachable from it, so they are rebased here as well. See [`Rebase::replace_invoked`].
521 fn visit_mut_procedure(&mut self, procedure: &mut masm::Procedure) -> ControlFlow<()> {
522 masm::visit::visit_mut_procedure(self, procedure)?;
523
524 let mut moved = false;
525 let invoked = procedure
526 .invoked()
527 .cloned()
528 .map(|mut invoke| {
529 moved |= self.rebase_target(&mut invoke.target);
530 invoke
531 })
532 .collect::<Vec<_>>();
533 if moved {
534 Self::replace_invoked(procedure, invoked);
535 }
536 ControlFlow::Continue(())
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use proptest::prelude::*;
543
544 use super::*;
545
546 fn validate_bytes_to_elements(bytes: &[u8]) {
547 let result = Rodata::bytes_to_elements(bytes);
548
549 // Each felt represents 4 bytes
550 let expected_felts = bytes.len().div_ceil(4);
551 // Felts should be padded to a multiple of 4 (1 word = 4 felts)
552 let expected_total_felts = expected_felts.div_ceil(4) * 4;
553
554 assert_eq!(
555 result.len(),
556 expected_total_felts,
557 "For {} bytes, expected {} felts (padded from {} felts), but got {}",
558 bytes.len(),
559 expected_total_felts,
560 expected_felts,
561 result.len()
562 );
563
564 // Verify padding is zeros
565 for (i, felt) in result.iter().enumerate().skip(expected_felts) {
566 assert_eq!(*felt, miden_processor::Felt::ZERO, "Padding at index {i} should be zero");
567 }
568 }
569
570 #[test]
571 fn test_bytes_to_elements_edge_cases() {
572 validate_bytes_to_elements(&[]);
573 validate_bytes_to_elements(&[1]);
574 validate_bytes_to_elements(&[0u8; 4]);
575 validate_bytes_to_elements(&[0u8; 15]);
576 validate_bytes_to_elements(&[0u8; 16]);
577 validate_bytes_to_elements(&[0u8; 17]);
578 validate_bytes_to_elements(&[0u8; 31]);
579 validate_bytes_to_elements(&[0u8; 32]);
580 validate_bytes_to_elements(&[0u8; 33]);
581 validate_bytes_to_elements(&[0u8; 64]);
582 }
583
584 proptest! {
585 #![proptest_config(ProptestConfig::with_cases(1000))]
586 #[test]
587 fn proptest_bytes_to_elements(bytes in prop::collection::vec(any::<u8>(), 0..=1000)) {
588 validate_bytes_to_elements(&bytes);
589 }
590
591 #[test]
592 fn proptest_bytes_to_elements_word_boundaries(size_factor in 0u32..=100) {
593 // Test specifically around word boundaries
594 // Test sizes around multiples of 16 (since 1 word = 4 felts = 16 bytes)
595 let base_size = size_factor * 16;
596 for offset in -2i32..=2 {
597 let size = (base_size as i32 + offset).max(0) as usize;
598 let bytes = vec![0u8; size];
599 validate_bytes_to_elements(&bytes);
600 }
601 }
602 }
603
604 // -------------------------------------------------------------------------------------------
605 // Where a component's Miden Assembly is rooted.
606 //
607 // `load_target_sources` rejects a root module whose path is not exactly the target's
608 // namespace, so this is what decides whether a target assembles at all.
609 // -------------------------------------------------------------------------------------------
610
611 mod rooting {
612 use alloc::rc::Rc;
613
614 use midenc_hir::{Context, version::Version};
615 use midenc_session::miden_project::{Target, Uri};
616
617 use super::*;
618
619 /// The identity a real Wasm *component* carries, which its author chose.
620 fn authored_id() -> builtin::ComponentId {
621 builtin::ComponentId {
622 namespace: Symbol::intern("miden:example"),
623 name: Symbol::intern("example"),
624 version: Version::new(1, 0, 0),
625 }
626 }
627
628 /// The component `frontend/wasm` wraps around a core Wasm module: the identity it gives
629 /// that wrapper, plus the marker saying the compiler invented it — which is what
630 /// [`MasmComponent::has_no_authored_identity`] reads. The id alone is a name an author
631 /// may write, and says nothing on its own.
632 fn wrapper_component() -> MasmComponent {
633 let id = builtin::ComponentId {
634 namespace: Symbol::intern("root_ns"),
635 name: Symbol::intern("root"),
636 version: Version::new(1, 0, 0),
637 };
638 let mut component = component(id);
639 component.synthetic_wrapper = true;
640 component
641 }
642
643 /// A component of `id` whose author wrote that id, rooted at the path it renders to.
644 fn component(id: builtin::ComponentId) -> MasmComponent {
645 let root_path: Arc<Path> = Arc::from(
646 id.to_library_path()
647 .to_absolute()
648 .expect("absolute")
649 .into_owned()
650 .into_boxed_path(),
651 );
652 rooted_component(Some(id), root_path)
653 }
654
655 /// What a world declaring **no** component lowers to: no id at all, and a root
656 /// `world_body_to_masm_component` chose rather than one any source declares — either the
657 /// world's single top-level module (`::{module}`) or, for zero or several of them, the
658 /// placeholder constant `::init`.
659 fn component_less(root: &str) -> MasmComponent {
660 let root_path: Arc<Path> = Arc::from(
661 masm::LibraryPath::new(root)
662 .unwrap()
663 .to_absolute()
664 .expect("absolute")
665 .into_owned()
666 .into_boxed_path(),
667 );
668 rooted_component(None, root_path)
669 }
670
671 /// A component rooted at `root_path` holding a root module and one submodule, in the
672 /// shape code generation produces: the submodule is nested under the component's path,
673 /// the root declares it, and the submodule's exported procedure calls one of its own by
674 /// absolute path as well as an intrinsic that lives outside the component.
675 fn rooted_component(
676 id: Option<builtin::ComponentId>,
677 root_path: Arc<Path>,
678 ) -> MasmComponent {
679 let child_path = root_path.join(masm::Path::new("child"));
680
681 let mut root = masm::Module::new(masm::ModuleKind::Library, &root_path);
682 root.declare_submodule(masm::Ident::new("child").unwrap(), masm::Visibility::Public)
683 .expect("should declare submodule");
684
685 let mut child = masm::Module::new(masm::ModuleKind::Library, &child_path);
686 child
687 .define_procedure(
688 procedure("callee", []),
689 Arc::new(midenc_session::diagnostics::DefaultSourceManager::default()),
690 )
691 .expect("should define callee");
692 child
693 .define_procedure(
694 procedure("caller", [child_path.join(masm::Path::new("callee")), intrinsic()]),
695 Arc::new(midenc_session::diagnostics::DefaultSourceManager::default()),
696 )
697 .expect("should define caller");
698
699 MasmComponent {
700 id,
701 synthetic_wrapper: false,
702 root: root_path,
703 init: None,
704 entrypoint: Some(exec_target(&child_path.join(masm::Path::new("caller")))),
705 executable_entrypoint_without_init: None,
706 rodata: Vec::new(),
707 heap_base: 0,
708 stack_pointer: None,
709 modules: vec![Arc::new(root), Arc::new(child)],
710 }
711 }
712
713 /// A procedure named `name` that `exec`s each of `callees`, recording them the way code
714 /// generation does — in the body *and* in the procedure's set of invoked callees.
715 ///
716 /// It carries a signature and a marker attribute because re-rooting rebuilds procedures
717 /// whose callees moved, and everything code generation attached has to survive that: the
718 /// signature is what the assembler type-checks exported procedures against, and the
719 /// markers are the ones `MasmFunctionBuilder::build` copies onto a procedure
720 /// (`lower/component.rs:903`) from the attributes the frontend sets on lifted exports
721 /// (`frontend/wasm`'s `lift_exports.rs:442`), which classify an account component's
722 /// procedures.
723 fn procedure<I>(name: &str, callees: I) -> masm::Procedure
724 where
725 I: IntoIterator<Item = masm::LibraryPath>,
726 {
727 let span = SourceSpan::default();
728 let mut ops = Vec::new();
729 let mut invoked = Vec::new();
730 for callee in callees {
731 let target = exec_target(&callee);
732 invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
733 ops.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
734 }
735 ops.push(masm::Op::Inst(Span::new(span, masm::Instruction::Nop)));
736 let mut procedure = masm::Procedure::new(
737 span,
738 masm::Visibility::Public,
739 masm::ProcedureName::new(name).unwrap(),
740 3,
741 masm::Block::new(span, ops),
742 )
743 .with_signature(masm::FunctionType::new(
744 midenc_hir::CallConv::Fast,
745 vec![masm::TypeExpr::from(midenc_hir::Type::U32)],
746 vec![],
747 ))
748 .with_attributes([masm::Attribute::Marker(
749 masm::Ident::new("account_procedure").unwrap(),
750 )]);
751 procedure.extend_invoked(invoked);
752 procedure
753 }
754
755 /// Everything code generation attached to `name` beyond its body, rendered for comparison.
756 fn decorations(module: &masm::Module, name: &str) -> alloc::string::String {
757 use alloc::string::ToString;
758
759 let procedure = module
760 .items()
761 .iter()
762 .find_map(|item| match item {
763 masm::Item::Procedure(procedure) if procedure.name().as_str() == name => {
764 Some(procedure)
765 }
766 _ => None,
767 })
768 .unwrap_or_else(|| panic!("no procedure named '{name}' in '{}'", module.path()));
769 format!(
770 "{:?} locals={} syscall={} signature={:?} attributes={:?}",
771 procedure.visibility(),
772 procedure.num_locals(),
773 procedure.is_syscall(),
774 procedure.signature().map(|signature| format!("{signature:?}")),
775 procedure.attributes().iter().map(|attr| attr.to_string()).collect::<Vec<_>>(),
776 )
777 }
778
779 /// A call target outside any component, which must survive re-rooting untouched.
780 fn intrinsic() -> masm::LibraryPath {
781 masm::LibraryPath::new("::intrinsics::mem::heap_init").unwrap()
782 }
783
784 fn exec_target(path: &masm::LibraryPath) -> InvocationTarget {
785 InvocationTarget::Path(Span::new(
786 SourceSpan::default(),
787 Arc::from(path.clone().into_boxed_path()),
788 ))
789 }
790
791 fn library_target(namespace: &str) -> Target {
792 Target::library(
793 Arc::<Path>::from(
794 masm::LibraryPath::new(namespace)
795 .unwrap()
796 .to_absolute()
797 .unwrap()
798 .into_owned()
799 .into_boxed_path(),
800 ),
801 Uri::new("lib.wasm"),
802 )
803 }
804
805 /// A default compiler context, which is where `source_inputs` gets its session.
806 fn context() -> Rc<Context> {
807 Rc::new(Context::default())
808 }
809
810 /// Every path in `module`, i.e. its own and each call target in each of its procedures,
811 /// including the targets recorded on the procedure rather than written in its body.
812 fn paths(module: &masm::Module) -> Vec<alloc::string::String> {
813 use alloc::string::ToString;
814
815 let mut paths = vec![module.path().to_string()];
816 for item in module.items() {
817 let masm::Item::Procedure(procedure) = item else {
818 continue;
819 };
820 for op in procedure.iter() {
821 if let masm::Op::Inst(inst) = op
822 && let masm::Instruction::Exec(InvocationTarget::Path(path)) = &**inst
823 {
824 paths.push(path.to_string());
825 }
826 }
827 for invoke in procedure.invoked() {
828 paths.push(invoke.target.to_string());
829 }
830 }
831 paths
832 }
833
834 /// The *allocation* behind each call target in `module`, cloned so that it can be
835 /// compared later by identity rather than by value.
836 ///
837 /// This is what tells a component that was never rewritten from one rewritten to the very
838 /// same paths, and nothing comparing values can: rebuilding a procedure is lossless by
839 /// design — that is the entire point of [`Rebase::replace_invoked`] — so every path, every
840 /// decoration and every recorded callee comes back *equal* either way.
841 ///
842 /// Identity survives the copy that [`MasmComponent::source_inputs`] makes, because an
843 /// `InvocationTarget::Path` holds an `Arc<Path>` and cloning a module shares it. A rewrite
844 /// cannot preserve it: `Rebase::rebase_target` builds its replacement with `Arc::from`,
845 /// which allocates unconditionally, whether or not the path it produces differs.
846 fn target_allocations(module: &masm::Module) -> Vec<Arc<Path>> {
847 let mut targets = Vec::new();
848 for item in module.items() {
849 let masm::Item::Procedure(procedure) = item else {
850 continue;
851 };
852 for op in procedure.iter() {
853 if let masm::Op::Inst(inst) = op
854 && let masm::Instruction::Exec(InvocationTarget::Path(path)) = &**inst
855 {
856 targets.push(path.inner().clone());
857 }
858 }
859 }
860 targets
861 }
862
863 /// Whether every call target in `module` is still the allocation it was in `before`.
864 ///
865 /// See [`target_allocations`]. A `false` here means the rewrite ran, regardless of what it
866 /// produced.
867 fn targets_are_untouched(before: &[Arc<Path>], module: &masm::Module) -> bool {
868 let after = target_allocations(module);
869 before.len() == after.len()
870 && before
871 .iter()
872 .zip(after.iter())
873 .all(|(before, after)| Arc::ptr_eq(before, after))
874 }
875
876 /// A synthetic wrapper compiled for a library target is rooted at the target's namespace,
877 /// and its whole module tree moves with it.
878 ///
879 /// The wrapper's id renders as the single quoted component `::"root_ns:root@1.0.0"`, so
880 /// it can never equal a target namespace; re-rooting is the only way such a target
881 /// satisfies the assembler. Everything that named the old root has to move too — module
882 /// paths, call targets, and the callee set each procedure carries — or the root declares
883 /// submodules that are not there and the linker fails to resolve the calls.
884 #[test]
885 fn a_synthetic_wrappers_library_is_rooted_at_the_target_namespace() {
886 let context = context();
887 let target = library_target("::example");
888 let component = wrapper_component();
889 let decorated = decorations(&component.modules[1], "caller");
890
891 let sources = component.source_inputs(&target, context.session()).unwrap();
892
893 assert_eq!(sources.root.path(), target.namespace.inner().as_ref());
894 assert_eq!(sources.support.len(), 1, "the component's one submodule");
895 assert_eq!(
896 paths(&sources.support[0]),
897 vec![
898 "::example::child",
899 "::example::child::callee",
900 // The intrinsic is outside the component, so it stays where it is; the two
901 // call targets appear twice because each is both written in the body and
902 // recorded on the procedure, and the linker resolves both.
903 "::intrinsics::mem::heap_init",
904 "::example::child::callee",
905 "::intrinsics::mem::heap_init",
906 ],
907 "nothing may be left addressing the wrapper's id"
908 );
909 assert_eq!(
910 decorations(&sources.support[0], "caller"),
911 decorated,
912 "a procedure whose callees moved is rebuilt, and must come back whole"
913 );
914 }
915
916 /// A library target already named after the wrapper comes back untouched.
917 ///
918 /// A manifest may declare `namespace = "root_ns:root@1.0.0"`, which is how projects worked
919 /// around this defect before it was fixed, and which parses to exactly the path
920 /// `ComponentId::to_library_path` produces. Such a target needs no re-rooting, and the
921 /// equality guard in [`MasmComponent::source_inputs`] is what keeps it from being rewritten
922 /// to itself — which is what lets those existing projects be said to be unaffected by this
923 /// change.
924 ///
925 /// The last assertion is what pins the *guard* rather than merely the outcome. Deleting
926 /// the guard degenerates the rewrite into an identity mapping, which every value-based
927 /// assertion above survives — rebuilding a procedure is lossless by design, so equal paths
928 /// and equal decorations come back either way. [`target_allocations`] compares identity
929 /// instead, which a rewrite cannot preserve however little it changes.
930 #[test]
931 fn a_library_target_named_after_the_wrapper_is_left_alone() {
932 let context = context();
933 let component = wrapper_component();
934 let expected = paths(&component.modules[1]);
935 let decorated = decorations(&component.modules[1], "caller");
936 let allocations = target_allocations(&component.modules[1]);
937 let target = library_target("root_ns:root@1.0.0");
938 assert_eq!(
939 target.namespace.inner().as_ref(),
940 component.root.as_ref(),
941 "the manifest namespace and the wrapper's id must really be the same path, or \
942 this test is about some other case"
943 );
944
945 let sources = component.source_inputs(&target, context.session()).unwrap();
946
947 assert_eq!(sources.root.path(), component.root.as_ref());
948 assert_eq!(paths(&sources.support[0]), expected);
949 assert_eq!(decorations(&sources.support[0], "caller"), decorated);
950 assert!(
951 targets_are_untouched(&allocations, &sources.support[0]),
952 "the rewrite must not have run at all, not merely have produced the same paths"
953 );
954 }
955
956 /// A component whose id its author chose is left exactly where it is.
957 ///
958 /// Re-rooting is justified only by the wrapper being invisible to whoever wrote the code.
959 /// An authored component id is part of the code's own identity, and moving it would
960 /// silently rename the procedures every dependent addresses.
961 #[test]
962 fn an_authored_components_library_keeps_its_own_path() {
963 let context = context();
964 let target = library_target("::example");
965 let component = component(authored_id());
966 let expected = paths(&component.modules[1]);
967
968 let sources = component.source_inputs(&target, context.session()).unwrap();
969
970 assert_eq!(
971 sources.root.path(),
972 component.root.as_ref(),
973 "an authored component's root is its own library path, whatever the target is \
974 called"
975 );
976 assert_eq!(paths(&sources.support[0]), expected);
977 }
978
979 /// A component-less world's library is rooted at the target's namespace too.
980 ///
981 /// The second half of the same rule the wrapper is the first half of: a world declaring
982 /// no component has no identity of its own, so lowering has to invent a root. With
983 /// several top-level modules — or none — that root is the constant `::init`, which no
984 /// source declares and which therefore no synthesized namespace can equal, so such a
985 /// target could never satisfy `load_target_sources` at all. The whole module tree moves
986 /// with the root here for the same reason it does for the wrapper.
987 #[test]
988 fn a_component_less_worlds_library_is_rooted_at_the_target_namespace() {
989 let context = context();
990 let target = library_target("::example");
991 let component = component_less("::init");
992 let decorated = decorations(&component.modules[1], "caller");
993
994 let sources = component.source_inputs(&target, context.session()).unwrap();
995
996 assert_eq!(sources.root.path(), target.namespace.inner().as_ref());
997 assert_eq!(sources.support.len(), 1, "the component's one submodule");
998 assert_eq!(
999 paths(&sources.support[0]),
1000 vec![
1001 "::example::child",
1002 "::example::child::callee",
1003 "::intrinsics::mem::heap_init",
1004 "::example::child::callee",
1005 "::intrinsics::mem::heap_init",
1006 ],
1007 "nothing may be left addressing the placeholder root"
1008 );
1009 assert_eq!(
1010 decorations(&sources.support[0], "caller"),
1011 decorated,
1012 "a procedure whose callees moved is rebuilt, and must come back whole"
1013 );
1014 }
1015
1016 /// A component-less world already sitting at its target's namespace comes back untouched.
1017 ///
1018 /// This is the *single*-module shape, and it is why subsuming it into the same rule costs
1019 /// nothing: lowering roots it at `::{module}` and preparation's `.hir` scan reads that
1020 /// same module's name, so the two agree and the equality guard makes the rewrite a no-op.
1021 /// Treating one module and several by one rule is what keeps codegen from having two
1022 /// answers preparation would have to mirror separately.
1023 ///
1024 /// "Costs nothing" is a claim about the guard, so the last assertion is about the guard:
1025 /// see [`target_allocations`] for why comparing values cannot distinguish a rewrite that
1026 /// never ran from one that reproduced its input exactly.
1027 #[test]
1028 fn a_component_less_world_already_at_the_target_namespace_is_left_alone() {
1029 let context = context();
1030 let component = component_less("::lib");
1031 let expected = paths(&component.modules[1]);
1032 let decorated = decorations(&component.modules[1], "caller");
1033 let allocations = target_allocations(&component.modules[1]);
1034 let target = library_target("::lib");
1035
1036 let sources = component.source_inputs(&target, context.session()).unwrap();
1037
1038 assert_eq!(sources.root.path(), component.root.as_ref());
1039 assert_eq!(paths(&sources.support[0]), expected);
1040 assert_eq!(decorations(&sources.support[0], "caller"), decorated);
1041 assert!(
1042 targets_are_untouched(&allocations, &sources.support[0]),
1043 "the rewrite must not have run at all, not merely have produced the same paths"
1044 );
1045 }
1046
1047 /// An executable target is untouched: its root is the generated `$exec` module, and the
1048 /// component's own modules keep the paths that module calls them by.
1049 ///
1050 /// Both shapes that re-rooting applies to are checked, because the early return for an
1051 /// executable is what keeps either from reaching it: the synthetic wrapper, and a
1052 /// component-less world, which is the shape a bare-module `.hir` or a disassembled
1053 /// `.masm` program takes.
1054 #[test]
1055 fn an_executable_target_still_gets_the_generated_main_module() {
1056 for component in [wrapper_component(), component_less("::init")] {
1057 let context = context();
1058 let expected = paths(&component.modules[1]);
1059 let target = Target::executable("main", Uri::new("main.wasm"));
1060
1061 let sources = component.source_inputs(&target, context.session()).unwrap();
1062
1063 assert_eq!(sources.root.path(), target.namespace.inner().as_ref());
1064 assert!(sources.root.kind().is_executable());
1065 let child = sources
1066 .support
1067 .iter()
1068 .find(|module| module.path().last() == Some("child"))
1069 .expect("the component's submodule is carried over as a support module");
1070 assert_eq!(
1071 paths(child),
1072 expected,
1073 "the generated `$exec` module calls the component by its own path, so \
1074 re-rooting here would break the very case that already works"
1075 );
1076 }
1077 }
1078 }
1079}