Skip to main content

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}