Skip to main content

midenc_codegen_masm/lower/
component.rs

1#[cfg(test)]
2mod tests;
3
4use alloc::{
5    collections::{BTreeMap, BTreeSet},
6    sync::Arc,
7    vec::Vec,
8};
9
10use miden_assembly::{PathBuf as LibraryPath, ast::InvocationTarget};
11use miden_assembly_syntax::{
12    ast::{Attribute, DebugVarLocation},
13    parser::WordValue,
14};
15use miden_core::serde::Deserializable;
16use midenc_hir::{
17    FunctionIdent, Op, OpExt, SourceSpan, Span, Symbol, TraceTarget, Type, ValueRef,
18    diagnostics::IntoDiagnostic,
19    dialects::{
20        builtin,
21        debuginfo::attributes::{Expression, ExpressionOp, FrameBase, SubprogramAttr},
22    },
23    interner,
24    pass::AnalysisManager,
25};
26use midenc_hir_analysis::analyses::LivenessAnalysis;
27use midenc_session::diagnostics::{Report, Spanned, WrapErr};
28use smallvec::SmallVec;
29
30use crate::{
31    Event, OperandStack,
32    artifact::MasmComponent,
33    emitter::BlockEmitter,
34    linker::{FunctionTableLayout, LinkInfo, Linker},
35    masm,
36};
37
38/// The generated procedure each module uses to fill the function-table slots whose callees it
39/// defines.
40///
41/// One procedure per module, rather than one for the component, because `procref` on a private
42/// procedure is only legal within its defining module — and a callee's visibility is its
43/// author's decision, not something initialization gets to widen. A module's procedure also
44/// invokes the procedures of the modules nested within it, so the component's `init` only has
45/// to reach the top-level ones; that is the shape a single component-global table would want.
46const FUNCTION_TABLE_INIT_PROC: &str = "__init_function_table";
47
48/// The private canonical-ABI entry body generated only for executable dispatch after `main` has
49/// already initialized the component.
50const EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC: &str = "__midenc_entrypoint_without_init";
51
52/// This trait represents a conversion pass from some HIR entity to a Miden Assembly component.
53pub trait ToMasmComponent {
54    fn to_masm_component(&self, analysis_manager: AnalysisManager)
55    -> Result<MasmComponent, Report>;
56}
57
58/// Derivation of a MASM component from an HIR world
59///
60/// A world is not a component, and the difference is what this impl exists to handle: a
61/// component's body holds modules, interfaces and functions, while a world's body holds
62/// *components* as well. Handing a world's own operation to `MasmComponentBuilder`, which walks a
63/// component body, therefore panics the moment it meets the first `builtin.component`.
64///
65/// So the shape of the world decides how it is lowered:
66///
67/// - A world holding **no** component is treated as one logical component whose body is the
68///   world's, which is what it has always meant here. This is the shape `frontend/masm`'s
69///   disassembler produces — it defines modules directly on the world — so it is a live path.
70/// - A world holding **one** component is lowered by lowering that component, because a
71///   component is what a Miden package is rooted at and carries the identity it is rooted at.
72///   Delegating rather than reimplementing is deliberate: the result is then the same
73///   [`MasmComponent`] the equivalent standalone `builtin.component` produces, by construction
74///   rather than by two implementations agreeing.
75/// - A world holding **more than one** component is reported, and that limitation is external to
76///   this crate — see `too_many_components`.
77///
78/// # Top-level items beside the component are normal, and are not an error
79///
80/// A world is not "a component, optionally". It may hold a component — the current codegen unit —
81/// **plus any number of sibling interfaces and modules**, which are either
82///
83/// - *external dependencies represented in the IR*, which hold declarations only and contribute
84///   nothing to the generated Miden Assembly, or
85/// - *supporting modules*, which are translated 1:1 to Miden Assembly modules and linked into the
86///   final assembly as ad-hoc modules.
87///
88/// A world holding a single component is only the *happy path*, and only for the Rust frontend,
89/// which compiles to one Wasm component and translates it to one HIR component. Other frontends,
90/// the MASM one included, legitimately produce several top-level items. **Neither kind of sibling
91/// may fail a build.**
92///
93/// The first kind is recognized by `is_declaration_only` and ignored, silently, because that is
94/// exactly what it is worth. The second is translated beside the component, by handing it to the
95/// same `MasmComponentBuilder` the component is lowered by — which is what makes it share the
96/// component's `LinkInfo` rather than lay a layout of its own over it.
97///
98/// The one shape left out is a top-level module that **owns memory**, i.e. declares a global
99/// variable or a data segment; see `report_siblings_that_own_memory` for the rule and why it is
100/// where the line falls.
101///
102/// Every producer hands this impl a *top-level* world: the world a whole-`builtin.world` `.hir`
103/// file parses to, the world `midenc_hir::parse` anchors any other top-level operation at, the
104/// world the Wasm frontend builds, and the one `frontend/masm`'s disassembler builds.
105impl ToMasmComponent for builtin::World {
106    fn to_masm_component(
107        &self,
108        analysis_manager: AnalysisManager,
109    ) -> Result<MasmComponent, Report> {
110        let mut components = Vec::new();
111        let mut siblings = Vec::new();
112        for op in self.body().entry().body().iter() {
113            match op.as_operation_ref().try_downcast_op::<builtin::Component>() {
114                Ok(component) => components.push(component),
115                Err(op) => siblings.push(op),
116            }
117        }
118
119        match components.len() {
120            0 => world_body_to_masm_component(self, analysis_manager),
121            1 => {
122                let (supporting, owns_memory) = classify_siblings(&siblings);
123                // The analysis manager is rooted at the world, and `AnalysisManager::nest`
124                // accepts any proper descendant, so the component impl can nest at its own
125                // modules — and at these siblings, which are children of the world — exactly as
126                // it does when codegen anchors it at the component itself.
127                let lowered = component_to_masm_component(
128                    &components[0].borrow(),
129                    analysis_manager,
130                    &supporting,
131                )?;
132                // Reported after lowering succeeded, so that a build which failed for an
133                // unrelated reason is not also told about a limitation it never reached.
134                report_siblings_that_own_memory(self, &owns_memory);
135                Ok(lowered)
136            }
137            _ => Err(too_many_components(self, &components)),
138        }
139    }
140}
141
142/// Whether `op`, a top-level item of a world, contributes nothing to the generated Miden Assembly.
143///
144/// This is how an *external dependency represented in the IR* is told apart from a *supporting
145/// module*: the former holds declarations only. There is no flag for it — `Symbol::is_declaration`
146/// is defined on functions and global variables but not on the modules and interfaces that hold
147/// them, so the question has to be asked of the contents.
148///
149/// Deliberately conservative: anything unrecognized counts as carrying definitions. Guessing wrong
150/// in that direction produces a warning about something that did not need one, while guessing
151/// wrong in the other direction silently omits code.
152fn is_declaration_only(op: &midenc_hir::OperationRef) -> bool {
153    /// A body defines nothing if every item in it is itself only a declaration.
154    ///
155    /// An empty body is vacuously declaration-only, which is the answer we want: an empty module
156    /// would lower to an empty Miden Assembly module.
157    fn body_is_all_declarations(region: &midenc_hir::Region) -> bool {
158        region.entry().body().iter().all(|item| {
159            if let Some(function) = item.downcast_ref::<builtin::Function>() {
160                function.is_declaration()
161            } else if let Some(gv) = item.downcast_ref::<builtin::GlobalVariable>() {
162                gv.is_declaration()
163            } else {
164                // A `builtin::Segment` initializes memory, and so does anything unrecognized as
165                // far as this predicate is willing to assume.
166                false
167            }
168        })
169    }
170
171    if let Ok(module) = op.try_downcast_op::<builtin::Module>() {
172        let module = module.borrow();
173        body_is_all_declarations(&module.body())
174    } else if let Ok(interface) = op.try_downcast_op::<builtin::Interface>() {
175        let interface = interface.borrow();
176        body_is_all_declarations(&interface.body())
177    } else if let Ok(function) = op.try_downcast_op::<builtin::Function>() {
178        let function = function.borrow();
179        function.is_declaration()
180    } else {
181        false
182    }
183}
184
185/// Whether `module` **owns memory**, i.e. declares a `builtin::GlobalVariable` or a
186/// `builtin::Segment`.
187///
188/// Those two are exactly the items [`Linker::link`] scans a component's modules for in order to
189/// compute its data layout, and that correspondence is the whole reason a module which owns none
190/// of them is safe to translate beside a component: it contributes nothing to the layout, so there
191/// is nothing of the component's for it to overlay. A module which *does* own one is not safe,
192/// because `Linker::link` walks only the direct module children of the component it is given and
193/// so cannot see a sibling of the *world* — see `report_siblings_that_own_memory`.
194///
195/// Deliberately conservative in two places, both so that the failure mode stays *diagnosed* rather
196/// than *silently mistranslated*:
197///
198/// - **Anything unrecognized counts as owning memory.** A module body is only ever legal here if
199///   it holds functions, global variables, segments, function tables and nested modules —
200///   `MasmModuleBuilder::build` panics on anything else — so an item this does not recognize is
201///   one that cannot be translated anyway. Treating it as owning memory is therefore also what
202///   makes the answer right *at any depth*: the only container that could hold a global or a
203///   segment deeper down is a nested `builtin::Module`, which is not recognized here and so does
204///   not get past this. That is deliberately stricter than lowering now requires — a nested module
205///   can be lowered, but a top-level sibling holding one still has no parent component to own
206///   whatever memory that nesting hides, and this predicate cannot see that far.
207/// - **A declared global counts, not just a defined one.** [`Linker::link`] skips declarations
208///   when building the layout, so this is strictly stricter than the overlay hazard requires. It
209///   is the right side to err on: a declaration whose definition is elsewhere is absent from the
210///   component's layout, so lowering a use of it would panic in
211///   `GlobalVariableLayout::get_computed_addr` — and it is a global variable declared by a module
212///   with no parent component either way, which is the rule being enforced.
213fn module_owns_memory(module: &builtin::Module) -> bool {
214    module.body().entry().body().iter().any(|item| {
215        // The two items a component owns, which are the two `Linker::link` scans for.
216        if item.is::<builtin::GlobalVariable>() || item.is::<builtin::Segment>() {
217            return true;
218        }
219        // Plus anything this cannot place, which includes a nested `builtin::Module` — the only
220        // item that could hide a global or a segment deeper down — so this arm is the conservative
221        // one, not a third case.
222        !item.is::<builtin::Function>()
223    })
224}
225
226/// Split a world's top-level items, beside its component, into the ones to translate and the ones
227/// to report.
228///
229/// Three outcomes, and everything a world can hold falls into one of them:
230///
231/// - an item holding only declarations is an *external dependency represented in the IR*, and is
232///   dropped here — silently, because that is exactly what it is worth. This is asked *first*, so
233///   a stub module whose declarations happen to include a global variable is ignored rather than
234///   reported: it contributes nothing at all, which is a stronger statement than owning no memory;
235/// - a `builtin::Module` owning no memory is a *supporting module*, and is returned to be
236///   translated 1:1 beside the component;
237/// - everything else is returned to be reported. That includes a module which owns memory, and
238///   also every non-module item, which is not an oversight: `MasmComponentBuilder::define_interface`
239///   places an interface *under the component's path* when the component has an id, so a world
240///   sibling translated through it would be silently relocated into a namespace it does not belong
241///   to. Reporting is the conservative answer until that seam is taught the difference.
242fn classify_siblings(
243    siblings: &[midenc_hir::OperationRef],
244) -> (SmallVec<[builtin::ModuleRef; 4]>, SmallVec<[midenc_hir::OperationRef; 4]>) {
245    let mut supporting = SmallVec::<[builtin::ModuleRef; 4]>::new();
246    let mut owns_memory = SmallVec::<[midenc_hir::OperationRef; 4]>::new();
247    for op in siblings.iter().copied() {
248        if is_declaration_only(&op) {
249            continue;
250        }
251        match op.try_downcast_op::<builtin::Module>() {
252            Ok(module) if !module_owns_memory(&module.borrow()) => supporting.push(module),
253            _ => owns_memory.push(op),
254        }
255    }
256    (supporting, owns_memory)
257}
258
259/// Warn about top-level items beside a component that own memory — plus, per `classify_siblings`,
260/// the ones this crate treats as if they did — and are therefore left out.
261///
262/// The rule, which is what the message and help exist to teach:
263///
264/// > Global variables and data segments are owned by a *component*. It is the component that lays
265/// > out memory for them, assigns each one its address, and emits the `init` that writes them
266/// > there. A module declared at the top level of a world has no parent component, so there is
267/// > nothing to own its memory — which makes declaring either of them there meaningless rather
268/// > than merely unsupported.
269///
270/// The mechanism agrees with the rule, which is why the line falls here rather than anywhere else.
271/// [`Linker::link`] walks only the direct `builtin::Module` children of the operation it is handed,
272/// so the [`LinkInfo`] computed for the component — `link(Some(id), <the component>)` — cannot see
273/// a sibling of the *world*. `LinkInfo` is what assigns every global its address and every segment
274/// its offset, so a sibling that owned memory and was lowered anyway would have to be given a
275/// layout of its own, laid straight over the component's. A sibling that owns none is invisible to
276/// that computation in the only sense that matters: it contributes nothing to it, so sharing the
277/// component's `LinkInfo` is not an approximation but the exact answer.
278///
279/// Declaration-only siblings are *not* reported. They are ignored by design, and warning about
280/// them would make the normal case noisy.
281fn report_siblings_that_own_memory(world: &builtin::World, siblings: &[midenc_hir::OperationRef]) {
282    if siblings.is_empty() {
283        return;
284    }
285
286    let mut diagnostic = world
287        .as_operation()
288        .context()
289        .diagnostics()
290        .diagnostic(miden_assembly::diagnostics::Severity::Warning)
291        .with_message(
292            "a top-level module beside a component cannot own global variables or data segments",
293        );
294    // The first label has to be the primary one; the builder asserts on that ordering.
295    for (index, op) in siblings.iter().enumerate() {
296        let op = op.borrow();
297        let label = format!("this '{}' is omitted from the generated package", op.name());
298        diagnostic = if index == 0 {
299            diagnostic.with_primary_label(op.span(), label)
300        } else {
301            diagnostic.with_secondary_label(op.span(), label)
302        };
303    }
304    diagnostic
305        .with_help(
306            "global variables and data segments belong to a component: the component is what lays \
307             out memory for them and emits the code that initializes it. A module declared at the \
308             top level of a world has no parent component to own them, so this build omits the \
309             module, and code that calls into it will fail to resolve. A top-level module that \
310             declares neither is a supporting module, and is translated 1:1 to a Miden Assembly \
311             module and linked into the final assembly as an ad-hoc module. Top-level items that \
312             only declare symbols — external dependencies represented in the IR — contribute no \
313             Miden Assembly and are ignored by design; they are not reported here. Any other \
314             top-level item is reported here as well, rather than translated on a guess.",
315        )
316        .emit();
317}
318
319/// The report for a world declaring more than one component.
320///
321/// The **one** shape this impl rejects, and the blocker is external to this crate rather than a
322/// gap in it: a Miden package's metadata can currently describe a single component, so a build
323/// emits one component per package. Two components in a world would have to become two packages.
324/// Work on multi-component packages is happening elsewhere; until it lands there is nothing this
325/// crate could do with the second component but invent merge semantics, which would be worse than
326/// saying so.
327///
328/// Note what this is *not*: a claim that worlds are single-component by nature. They are not, and
329/// sibling interfaces and modules are ordinary — see the docs on `ToMasmComponent for
330/// builtin::World`. Only a second *component* stops a build.
331///
332/// The wording matters as much as the rejection, so the message says what is unimplemented and the
333/// help says who is unblocking it, rather than implying the input is wrong.
334fn too_many_components(world: &builtin::World, components: &[builtin::ComponentRef]) -> Report {
335    // The limitation belongs in the *message*, not only in the help: a `Report` built from a
336    // diagnostic renders its message alone under `Display`, which is all a caller that only
337    // formats the error ever sees.
338    let mut diagnostic = world
339        .as_operation()
340        .context()
341        .diagnostics()
342        .diagnostic(miden_assembly::diagnostics::Severity::Error)
343        .with_message(format!(
344            "lowering a world containing {} components is not yet implemented",
345            components.len()
346        ))
347        .with_primary_label(world.span(), "in this world");
348    for component in components {
349        let component = component.borrow();
350        diagnostic = diagnostic.with_secondary_label(component.span(), "this component");
351    }
352    diagnostic
353        .with_help(
354            "this is a known limitation of the compiler rather than a problem with this input: a \
355             Miden package's metadata can currently describe only one component, so a build emits \
356             one component per package. Support for multiple components in a package is being \
357             worked on; until it lands, compile each component separately.",
358        )
359        .into_report()
360}
361
362/// Report a function that reached codegen with no body.
363///
364/// Unlike [`too_many_components`], this says the **input is invalid**, not that the compiler is
365/// incomplete — and it is worth being precise about why, because the three facts below are what a
366/// future reader needs and none of them is obvious from the code.
367///
368/// **Nothing can ever provide the definition.** A body-less function is a declaration: it names a
369/// procedure whose implementation is expected to come from somewhere else. Miden Assembly has no
370/// such somewhere else at this point — there is no later link step that could supply it — so a
371/// declaration surviving into codegen names a procedure that will never exist.
372///
373/// **Why a surviving declaration is assumed to be referenced.** Dead symbol elimination would
374/// strip a declaration nothing refers to. There is no such pass today, but when there is, an
375/// unreferenced declaration will not reach here — so a declaration that *does* reach here is one
376/// something referred to, which is exactly the case that cannot be satisfied. That is why this is
377/// an error rather than an item to skip: skipping it would emit a module whose callers reference a
378/// procedure it does not define, and the failure would surface at link time with nothing to point
379/// at.
380///
381/// **Why this is not checked before codegen.** It is an invariant of Miden Assembly, not of the
382/// IR. A body-less `builtin::Function` is a perfectly well-formed operation — verification cannot
383/// reject it without rejecting every legitimate declaration — so the check can only live where the
384/// IR is being turned into something that has to be complete. Moving it into verification will not
385/// work; this is the note that saves the next person the attempt.
386fn function_without_a_body(function: &builtin::Function) -> Report {
387    // The reason belongs in the *message*, not only the help: a `Report` built from a diagnostic
388    // renders its message alone under `Display`, which is all a caller that merely formats the
389    // error ever sees. See the same note on `too_many_components`.
390    function
391        .as_operation()
392        .context()
393        .diagnostics()
394        .diagnostic(miden_assembly::diagnostics::Severity::Error)
395        .with_message(
396            "cannot emit masm for a function with no body: nothing can provide its definition",
397        )
398        .with_primary_label(function.span(), "this function is declared but never defined")
399        .with_help(
400            "a declaration names a procedure whose implementation comes from elsewhere, and Miden \
401             Assembly has no later step that could supply one. Either give this function a body, \
402             or remove it along with whatever refers to it.",
403        )
404        .into_report()
405}
406
407/// Derive a MASM component by treating `world`'s body as a component body.
408///
409/// The meaning a world has always had here, and correct only when the world declares no
410/// component of its own: every definition-carrying module in it belongs to one logical
411/// component, which has no identity beyond the namespace those modules sit in.
412fn world_body_to_masm_component(
413    world: &builtin::World,
414    analysis_manager: AnalysisManager,
415) -> Result<MasmComponent, Report> {
416    // Get the current compiler context
417    let context = world.as_operation().context_rc();
418
419    // Run the linker for this component in order to compute its data layout
420    let link_info = Linker::default().link(None, world.as_operation()).map_err(Report::msg)?;
421
422    // Get the entrypoint, if specified
423    let entrypoint = match context.session().options.entrypoint.as_deref() {
424        Some(entry) => {
425            let entry_id = entry
426                .parse::<FunctionIdent>()
427                .map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
428            let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
429                entry_id.function.span,
430                entry_id.function.as_str().into(),
431            )));
432
433            let path = LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?;
434            let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
435            Some(masm::InvocationTarget::Path(Span::new(
436                entry_id.function.span,
437                qualified.into_inner(),
438            )))
439        }
440        None => None,
441    };
442    let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
443        world.as_operation_ref(),
444        &[],
445        &link_info,
446        entrypoint.as_ref(),
447    )?;
448    let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
449        executable_entrypoint,
450        &analysis_manager,
451        &link_info,
452    )?;
453
454    // If we have global variables, data segments, function tables, or a core Wasm start, we will
455    // require a component initializer function, as well as a module to hold component-level
456    // functions such as init
457    let requires_init = link_info.requires_init();
458    let toplevel_namespaces = world
459        .body()
460        .entry()
461        .body()
462        .iter()
463        // Only modules: this function is reached only for a world that declares no component,
464        // so a `builtin::Component` arm here would be unreachable.
465        .filter_map(|op| {
466            if op.is::<builtin::Module>() {
467                Some(op.as_operation_ref())
468            } else {
469                None
470            }
471        })
472        .collect::<Vec<_>>();
473    let init = if requires_init {
474        let name = masm::ProcedureName::new("init").unwrap();
475        let qualified = match toplevel_namespaces.len() {
476            1 => {
477                let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
478                masm::QualifiedProcedureName::new(format!("::{namespace}").as_str(), name)
479            }
480            _ => masm::QualifiedProcedureName::new("::init", name),
481        };
482        Some(masm::InvocationTarget::Path(Span::new(
483            SourceSpan::default(),
484            qualified.into_inner(),
485        )))
486    } else {
487        None
488    };
489
490    // Define the initial component modules set
491    //
492    // The top-level component module is always defined, but may be empty
493    let root = match toplevel_namespaces.len() {
494        1 => {
495            let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
496            Arc::from(
497                masm::PathBuf::new(&format!("::{namespace}"))
498                    .expect("invalid namespace")
499                    .into_boxed_path(),
500            )
501        }
502        _ => Arc::<masm::Path>::from(masm::Path::new("::init")),
503    };
504    let init_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
505    let modules = vec![init_module];
506
507    let rodata = data_segments_to_rodata(&link_info)?;
508
509    let heap_base = link_info.heap_base();
510    let stack_pointer = link_info.globals_layout().stack_pointer_offset();
511    let mut masm_component = MasmComponent {
512        id: None,
513        // A world declaring no component is not the compiler's wrapper around one: it has no
514        // component boundary at all.
515        synthetic_wrapper: false,
516        root,
517        init,
518        entrypoint,
519        executable_entrypoint_without_init,
520        rodata,
521        heap_base,
522        stack_pointer,
523        modules,
524    };
525    let builder = MasmComponentBuilder {
526        analysis_manager,
527        component: &mut masm_component,
528        link_info: &link_info,
529        source_manager: context.session().source_manager.clone(),
530        init_body: Default::default(),
531        invoked_from_init: Default::default(),
532    };
533
534    // A world declaring no component has no siblings *beside* one: every top-level module in it
535    // is part of the one logical component this treats its body as.
536    builder.build(world.as_operation(), &[])?;
537
538    Ok(masm_component)
539}
540
541/// 1:1 conversion from HIR component to MASM component
542impl ToMasmComponent for builtin::Component {
543    fn to_masm_component(
544        &self,
545        analysis_manager: AnalysisManager,
546    ) -> Result<MasmComponent, Report> {
547        component_to_masm_component(self, analysis_manager, &[])
548    }
549}
550
551/// Derive a MASM component from `component`, and from `supporting` beside it.
552///
553/// `supporting` is empty for a standalone `builtin.component`, which has no siblings; it is
554/// non-empty only when the component was reached through the world that declares it, and holds
555/// that world's supporting modules — see `ToMasmComponent for builtin::World`.
556///
557/// They are lowered *here*, rather than by a second pass of their own, for one reason: the
558/// [`LinkInfo`] computed below is the component's data layout, and a supporting module has to be
559/// lowered against it. `classify_siblings` is what makes that exact rather than approximate — a
560/// module that owns no memory contributes nothing to the layout, so the component's `LinkInfo` is
561/// the same one a link over the whole world would have produced.
562fn component_to_masm_component(
563    component: &builtin::Component,
564    analysis_manager: AnalysisManager,
565    supporting: &[builtin::ModuleRef],
566) -> Result<MasmComponent, Report> {
567    // Get the current compiler context
568    let context = component.as_operation().context_rc();
569
570    // Whether this component is one the compiler invented to wrap a bare core module, which it
571    // says by carrying a marker the frontend set rather than by its id — see
572    // `builtin::Component::SYNTHETIC_WRAPPER_ATTR`.
573    let synthetic_wrapper = component.is_synthetic_wrapper();
574
575    // Run the linker for this component in order to compute its data layout
576    let id = component.id();
577    let link_info = Linker::default()
578        .link(Some(id.clone()), component.as_operation())
579        .map_err(Report::msg)?;
580
581    // Get the library path of the component
582    let component_path = id
583        .to_library_path()
584        .to_absolute()
585        .map_err(|err| {
586            Report::msg(format!("unable to canonicalize '{}': {err}", id.to_library_path()))
587        })?
588        .into_owned();
589
590    // Get the entrypoint, if specified
591    let entrypoint = match context.session().options.entrypoint.as_deref() {
592        Some(entry) => {
593            let entry_id = entry
594                .parse::<FunctionIdent>()
595                .map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
596            let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
597                entry_id.function.span,
598                entry_id.function.as_str().into(),
599            )));
600
601            // Check if we're inside the synthetic "wrapper" component used for pure Rust
602            // compilation. Since the user does not know about it, their entrypoint does not
603            // include the synthetic component path. We append the user-provided path to the
604            // root component path here if needed.
605            //
606            // TODO(pauls): Narrow this to only be true if the target env is not 'rollup', we
607            // cannot currently do so because we do not have sufficient Cargo metadata yet in
608            // 'cargo miden build' to detect the target env, and we default it to 'rollup'
609            let path = if synthetic_wrapper {
610                component_path.join(entry_id.module.as_str())
611            } else {
612                // We're compiling a Wasm component and the component id is included
613                // in the entrypoint.
614                LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?
615            };
616            let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
617            Some(masm::InvocationTarget::Path(Span::new(
618                entry_id.function.span,
619                qualified.into_inner(),
620            )))
621        }
622        None => None,
623    };
624    let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
625        component.as_operation_ref(),
626        supporting,
627        &link_info,
628        entrypoint.as_ref(),
629    )?;
630    let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
631        executable_entrypoint,
632        &analysis_manager,
633        &link_info,
634    )?;
635
636    // If we have global variables, data segments, function tables, or a core Wasm start, we will
637    // require a component initializer function, as well as a module to hold component-level
638    // functions such as init
639    let requires_init = link_info.requires_init();
640    let init = if requires_init {
641        let name = masm::ProcedureName::new("init").unwrap();
642        let qualified = masm::QualifiedProcedureName::new(&component_path, name);
643        Some(masm::InvocationTarget::Path(Span::new(
644            SourceSpan::default(),
645            qualified.into_inner(),
646        )))
647    } else {
648        None
649    };
650
651    // Define the initial component modules set
652    //
653    // The top-level component module is always defined, but may be empty
654    let root: Arc<miden_assembly_syntax::Path> = component_path.into_boxed_path().into();
655    let root_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
656    let modules = vec![root_module];
657
658    let rodata = data_segments_to_rodata(&link_info)?;
659
660    let heap_base = link_info.heap_base();
661    let stack_pointer = link_info.globals_layout().stack_pointer_offset();
662    let mut masm_component = MasmComponent {
663        id: Some(id),
664        synthetic_wrapper,
665        root,
666        init,
667        entrypoint,
668        executable_entrypoint_without_init,
669        rodata,
670        heap_base,
671        stack_pointer,
672        modules,
673    };
674    let builder = MasmComponentBuilder {
675        analysis_manager,
676        component: &mut masm_component,
677        link_info: &link_info,
678        source_manager: context.session().source_manager.clone(),
679        init_body: Default::default(),
680        invoked_from_init: Default::default(),
681    };
682
683    builder.build(component.as_operation(), supporting)?;
684
685    Ok(masm_component)
686}
687
688fn data_segments_to_rodata(link_info: &LinkInfo) -> Result<Vec<crate::Rodata>, Report> {
689    use midenc_hir::constants::ConstantData;
690
691    use crate::data_segments::{ResolvedDataSegment, merge_data_segments};
692    let mut resolved = SmallVec::<[ResolvedDataSegment; 2]>::new();
693    for sref in link_info.segment_layout().iter() {
694        let s = sref.borrow();
695        resolved.push(ResolvedDataSegment {
696            offset: *s.get_offset(),
697            data: s.initializer().as_slice().to_vec(),
698            readonly: *s.get_readonly(),
699        });
700    }
701    Ok(match merge_data_segments(resolved).map_err(Report::msg)? {
702        None => alloc::vec::Vec::new(),
703        Some(merged) => {
704            let data = alloc::sync::Arc::new(ConstantData::from(merged.data));
705            let felts = crate::Rodata::bytes_to_elements(data.as_slice());
706            let digest = miden_core::crypto::hash::Poseidon2::hash_elements(&felts);
707            alloc::vec![crate::Rodata {
708                component: link_info.component().cloned().unwrap_or(builtin::ComponentId {
709                    namespace: interner::Symbol::intern("root_ns"),
710                    name: interner::Symbol::intern("root"),
711                    version: midenc_hir::version::Version::new(1, 0, 0)
712                }),
713                digest,
714                start: super::NativePtr::from_ptr(merged.offset),
715                data,
716            }]
717        }
718    })
719}
720
721/// Identify the selected canonical-ABI wrapper that needs a private no-init executable copy.
722///
723/// Generated executable `main` must remain the initialization owner so that component startup runs
724/// before test-harness memory initialization. The public canonical wrapper must also retain its
725/// prologue so that a direct `call` initializes its fresh MASM context. For the exact combination
726/// of a component start and a selected same-component canonical wrapper, codegen therefore emits a
727/// private no-init copy for `main` alone. Calling convention by itself is deliberately insufficient:
728/// every unmarked input retains the existing path, regardless of its Wasm target.
729fn classify_marked_canonical_abi_entrypoint(
730    component: midenc_hir::OperationRef,
731    supporting: &[builtin::ModuleRef],
732    link_info: &LinkInfo,
733    entrypoint: Option<&masm::InvocationTarget>,
734) -> Result<Option<builtin::FunctionRef>, Report> {
735    let (Some(_), Some(entrypoint)) = (link_info.component_start(), entrypoint) else {
736        return Ok(None);
737    };
738    let entrypoint_path = entrypoint
739        .unwrap_path()
740        .to_absolute()
741        .map_err(|err| Report::msg(format!("invalid executable entrypoint path: {err}")))?;
742
743    let find_canonical_entrypoint = |root: midenc_hir::OperationRef| {
744        let mut canonical_entrypoint = None;
745        root.borrow().prewalk_all(|op| {
746            let Some(function) = op.downcast_ref::<builtin::Function>() else {
747                return;
748            };
749            if !function.signature().cc.is_wasm_canonical_abi() {
750                return;
751            }
752
753            let function_target = super::lowering::invocation_target_from_symbol_path(
754                &function.path(),
755                function.span(),
756            );
757            if function_target.unwrap_path() == entrypoint_path.as_ref() {
758                canonical_entrypoint = Some(function.as_function_ref());
759            }
760        });
761        canonical_entrypoint
762    };
763
764    if let Some(function) = find_canonical_entrypoint(component) {
765        if function.borrow().as_operation().parent_op() != Some(component) {
766            let path = function.borrow().path();
767            return Err(Report::msg(format!(
768                "unsupported executable entrypoint '{path}': a canonical-ABI entrypoint for a \
769                 component with a core Wasm start function must be defined directly in the \
770                 selected component"
771            )));
772        }
773        return Ok(Some(function));
774    }
775
776    let supporting_entrypoint = supporting
777        .iter()
778        .find_map(|module| find_canonical_entrypoint(module.borrow().as_operation_ref()));
779    if let Some(function) = supporting_entrypoint {
780        let path = function.borrow().path();
781        return Err(Report::msg(format!(
782            "unsupported executable entrypoint '{path}': a canonical-ABI entrypoint cannot be \
783             selected for a component with a core Wasm start function because it would execute \
784             component initialization twice in the same context"
785        )));
786    }
787
788    Ok(None)
789}
790
791/// Lower the executable-only copy selected by [`classify_marked_canonical_abi_entrypoint`].
792fn lower_executable_entrypoint_without_init(
793    function: Option<builtin::FunctionRef>,
794    analysis_manager: &AnalysisManager,
795    link_info: &LinkInfo,
796) -> Result<Option<masm::Procedure>, Report> {
797    let Some(function) = function else {
798        return Ok(None);
799    };
800    let function = function.borrow();
801    let mut builder = MasmFunctionBuilder::new(&function)?;
802    builder.name = masm::ProcedureName::new(EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC).unwrap();
803    builder.visibility = masm::Visibility::Private;
804    builder
805        .build(
806            &function,
807            analysis_manager.nest(function.as_operation_ref()),
808            link_info,
809            FunctionLoweringMode::ExecutableEntrypointWithoutInit,
810        )
811        .map(Some)
812}
813
814struct MasmComponentBuilder<'a> {
815    component: &'a mut MasmComponent,
816    analysis_manager: AnalysisManager,
817    link_info: &'a LinkInfo,
818    source_manager: Arc<dyn midenc_session::SourceManager>,
819    init_body: Vec<masm::Op>,
820    invoked_from_init: BTreeSet<masm::Invoke>,
821}
822
823impl MasmComponentBuilder<'_> {
824    /// Convert the component body to Miden Assembly, along with any `supporting` modules that sit
825    /// beside the component in the world that declares it.
826    pub fn build(
827        mut self,
828        component: &midenc_hir::Operation,
829        supporting: &[builtin::ModuleRef],
830    ) -> Result<(), Report> {
831        use masm::{Instruction as Inst, InvocationTarget, Op};
832
833        // Validate exactly the operations this builder will emit. In particular, a world may
834        // contain declaration-only or memory-owning siblings which codegen deliberately omits;
835        // invalid roots in those items must not mask the established omission diagnostic.
836        crate::legalization::validate_procedure_roots(component)?;
837        for module in supporting {
838            crate::legalization::validate_procedure_roots(module.borrow().as_operation())?;
839        }
840
841        // If a component-level init is required, emit code to initialize the heap before any other
842        // initialization code.
843        if self.component.init.is_some() {
844            let span = component.span();
845
846            // Heap metadata initialization
847            let heap_base = self.component.heap_base;
848            self.init_body.push(masm::Op::Inst(Span::new(
849                span,
850                Inst::Push(masm::Immediate::Value(Span::unknown(heap_base.into()))),
851            )));
852            let heap_init = {
853                let name = masm::ProcedureName::new("heap_init").unwrap();
854                let module = masm::LibraryPath::new("::intrinsics::mem").unwrap();
855                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
856                InvocationTarget::Path(Span::new(span, qualified.into_inner()))
857            };
858            self.init_body.push(Op::Inst(Span::new(
859                span,
860                Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
861            )));
862            self.init_body.push(Op::Inst(Span::new(span, Inst::Exec(heap_init))));
863            self.init_body.push(Op::Inst(Span::new(
864                span,
865                Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
866            )));
867
868            // Data segment initialization
869            //
870            // Function table initialization is *not* emitted here: it is attached to the modules
871            // defining the callees, which do not exist yet. See below.
872            self.emit_data_segment_initialization();
873        }
874
875        // Translate component body
876        let region = component.region(0);
877        let block = region.entry();
878        for op in block.body() {
879            if let Some(module) = op.downcast_ref::<builtin::Module>() {
880                self.define_module(module)?;
881            } else if let Some(interface) = op.downcast_ref::<builtin::Interface>() {
882                self.define_interface(interface)?;
883            } else if let Some(function) = op.downcast_ref::<builtin::Function>() {
884                self.define_function(function)?;
885            } else {
886                panic!(
887                    "invalid component-level operation: '{}' is not supported in a component body",
888                    op.name()
889                )
890            }
891        }
892
893        // Translate the supporting modules beside the component, into the same set of modules.
894        //
895        // `define_module` roots a module whose path does not begin with the component's at the top
896        // level, which is exactly where these belong, so they end up as siblings of the component
897        // root rather than children of it — and therefore in `support` when
898        // `MasmComponent::source_inputs` splits the set. None of them can contribute to `init`:
899        // `MasmModuleBuilder` only ever appends to it for a global variable, and a module that
900        // declared one would not be here.
901        for module in supporting {
902            self.define_module(&module.borrow())?;
903        }
904
905        // Finalize the component-level init, if required
906        if self.component.init.is_some() {
907            // Function tables are initialized from the modules that define their callees, so this
908            // has to run after those modules exist — which is why it is here rather than beside
909            // the heap and data segment initialization above. Each fragment invokes the fragments
910            // of the modules nested within it; `init` only reaches the outermost ones.
911            let fragments = self.build_function_table_fragments()?;
912            let owners = fragments.keys().cloned().collect::<Vec<_>>();
913            let mut child_calls: BTreeMap<masm::PathBuf, Vec<masm::PathBuf>> = BTreeMap::new();
914            let mut roots: Vec<masm::PathBuf> = Vec::new();
915            for owner in owners.iter() {
916                // The nearest fragment-bearing ancestor, if any: a module's fragment is reached
917                // from its closest enclosing one, and only the modules with no such enclosing
918                // fragment are reached from `init`. `Path::starts_with_exactly` matches
919                // component-wise, so `::a::bc` is not an ancestor's descendant merely because
920                // the text of `::a::b` is a prefix of it.
921                //
922                // A root is only reachable if `init`, which lives in the component root, may
923                // `exec` into it — so a *private* nested module whose parent defines no table
924                // callee is a root `init` cannot reach: MASM visibility lets a module reach its
925                // own children and its siblings, not a private grandchild. Nothing produces that
926                // shape today (the Wasm frontend puts every core callee in one module, and a
927                // private nested module reached from the component root would be unreachable for
928                // ordinary calls too), and it fails loudly at assembly time rather than silently
929                // leaving the slots zeroed, so it is recorded here rather than worked around. The
930                // fix, if it ever arises, is to give the parent an empty fragment to relay
931                // through rather than to promote anyone's visibility.
932                match owners
933                    .iter()
934                    .filter(|candidate| {
935                        *candidate != owner
936                            && owner.as_path().starts_with_exactly(candidate.as_path())
937                    })
938                    .max_by_key(|candidate| candidate.as_path().components().count())
939                {
940                    Some(parent) => {
941                        child_calls.entry(parent.clone()).or_default().push(owner.clone())
942                    }
943                    None => roots.push(owner.clone()),
944                }
945            }
946
947            let span = SourceSpan::default();
948            let proc_name = masm::ProcedureName::new(FUNCTION_TABLE_INIT_PROC).unwrap();
949            for (
950                owner,
951                FunctionTableFragment {
952                    mut body,
953                    mut invoked,
954                    a_callee,
955                },
956            ) in fragments
957            {
958                for child in child_calls.remove(&owner).unwrap_or_default() {
959                    let qualified =
960                        masm::QualifiedProcedureName::new(child.as_path(), proc_name.clone());
961                    let target =
962                        masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
963                    invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
964                    body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
965                }
966
967                // A module holding nothing but declarations is skipped by `classify_siblings` and
968                // never lowered, but a table entry may still name a function in it: the entry
969                // resolves and the function has a signature, so both the `hir.exec_indirect`
970                // verifier and legalization accept the IR. There is no module here to attach the
971                // slot-filling code to, and no MAST root to fill the slot with, so it is invalid
972                // input rather than a compiler bug — and reported like the rest of the invalid
973                // input this function rejects.
974                let Some(index) = self
975                    .component
976                    .modules
977                    .iter()
978                    .position(|module| module.path() == owner.as_path())
979                else {
980                    return Err(Report::msg(format!(
981                        "invalid function table entry: callee '{a_callee}' is defined in module \
982                         '{owner}', which was not lowered because it holds only declarations — a \
983                         function table cannot name a callee that has no definition to take the \
984                         address of"
985                    )));
986                };
987                let module = Arc::get_mut(&mut self.component.modules[index])
988                    .expect("expected unique reference");
989                let mut procedure = masm::Procedure::new(
990                    span,
991                    // Public so the parent module's fragment (or `init`) can reach it; this is
992                    // the only symbol table initialization contributes to a module's surface,
993                    // and it is the compiler's own, never the author's
994                    masm::Visibility::Public,
995                    proc_name.clone(),
996                    0,
997                    masm::Block::new(span, body),
998                )
999                .with_signature(masm::FunctionType::new(
1000                    midenc_hir::CallConv::Fast,
1001                    vec![],
1002                    vec![],
1003                ));
1004                procedure.extend_invoked(invoked);
1005                module
1006                    .define_procedure(procedure, self.source_manager.clone())
1007                    .into_diagnostic()
1008                    .wrap_err("failed to define a function table initializer")?;
1009            }
1010
1011            for root in roots {
1012                let qualified =
1013                    masm::QualifiedProcedureName::new(root.as_path(), proc_name.clone());
1014                let target = masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
1015                self.invoked_from_init
1016                    .insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
1017                self.init_body
1018                    .push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
1019            }
1020
1021            // The core Wasm start function is the final phase of initialization. It must observe
1022            // the initialized heap, data, globals, and function tables, and `exec` keeps it in the
1023            // same MASM context that `init` is preparing.
1024            if let Some(start) = self.link_info.component_start() {
1025                let start = start.borrow();
1026                let target = super::lowering::invocation_target_from_symbol_path(
1027                    &start.path(),
1028                    start.span(),
1029                );
1030                self.invoked_from_init
1031                    .insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
1032                self.init_body
1033                    .push(masm::Op::Inst(Span::new(start.span(), masm::Instruction::Exec(target))));
1034            }
1035
1036            let module =
1037                Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
1038
1039            let init_name = masm::ProcedureName::new("init").unwrap();
1040            let init_body = core::mem::take(&mut self.init_body);
1041            let mut init = masm::Procedure::new(
1042                Default::default(),
1043                masm::Visibility::Public,
1044                init_name,
1045                0,
1046                masm::Block::new(component.span(), init_body),
1047            )
1048            .with_signature(masm::FunctionType::new(
1049                midenc_hir::CallConv::Fast,
1050                vec![],
1051                vec![],
1052            ));
1053            // What `init` invokes is what the assembler's linker builds its call graph from, and
1054            // until now nothing attached this set to anything — every invocation recorded while
1055            // building `init`'s body, by the fragment roots just above and by global variable
1056            // initializers before them, was written and then dropped. The linker resolves an
1057            // `exec` from the instruction as well, which is why the omission never showed; an
1058            // accurate call graph is still what the set is for, and `init` was the one procedure
1059            // in the component reporting none of its callees.
1060            init.extend_invoked(core::mem::take(&mut self.invoked_from_init));
1061
1062            module
1063                .define_procedure(init, self.source_manager.clone())
1064                .into_diagnostic()
1065                .wrap_err("failed to define component `init` procedure")?;
1066        } else {
1067            assert!(
1068                self.init_body.is_empty(),
1069                "the need for an 'init' function was not expected, but code was generated for one"
1070            );
1071        }
1072
1073        Ok(())
1074    }
1075
1076    fn define_interface(&mut self, interface: &builtin::Interface) -> Result<(), Report> {
1077        let interface_path = if let Some(id) = self.component.id.as_ref() {
1078            let mut path = id.to_library_path();
1079            path.push(interface.name().as_str());
1080            path
1081        } else {
1082            interface.path().to_library_path()
1083        };
1084        let mut masm_module =
1085            Box::new(masm::Module::new(masm::ModuleKind::Library, interface_path));
1086        let builder = MasmModuleBuilder {
1087            module: &mut masm_module,
1088            analysis_manager: self
1089                .analysis_manager
1090                .nest(interface.as_operation().as_operation_ref()),
1091            link_info: self.link_info,
1092            source_manager: self.source_manager.clone(),
1093            init_body: &mut self.init_body,
1094            invoked_from_init: &mut self.invoked_from_init,
1095        };
1096        builder.build_from_interface(interface)?;
1097
1098        self.component.modules.push(Arc::from(masm_module));
1099
1100        Ok(())
1101    }
1102
1103    fn define_module(&mut self, module: &builtin::Module) -> Result<(), Report> {
1104        let module_path = module.path().to_library_path();
1105        let module_path = module_path.to_absolute().unwrap();
1106        let trace_target = TraceTarget::category("codegen");
1107        log::debug!(target: &trace_target, "defining module '{module_path}'");
1108        // The submodule declaration's visibility decides whether the module's public procedures
1109        // belong to the public surface of the assembled package: the assembler derives that
1110        // surface from the modules reachable from the root through *public* submodule
1111        // declarations. Core modules are private in HIR, so their public procedures stay
1112        // resolvable package-internally (a private submodule is visible to its parent and siblings)
1113        // without becoming part of the package's interface.
1114        //
1115        // Two of the shapes reaching here have no component boundary to speak of, and in both
1116        // the modules *are* the artifact's interface, so they keep public submodules. A world
1117        // lowered without a component id is one. The other is the wrapper the compiler invents
1118        // around a bare core module, which is not a real boundary either: the wrapped module is
1119        // the artifact's own interface (the entrypoint of an executable, or the exports of a
1120        // bare library), and the generated executable `main` module lives outside the wrapper's
1121        // module tree. That the wrapper is the compiler's is something it *says* — the frontend
1122        // marks it (`builtin::Component::SYNTHETIC_WRAPPER_ATTR`) — rather than something read
1123        // off its id, which is a name an author may write. An authored component is the
1124        // complement, and keeps the visibility its author declared.
1125        let is_artifact_interface = self.component.id.is_none() || self.component.synthetic_wrapper;
1126        let visibility = if is_artifact_interface {
1127            masm::Visibility::Public
1128        } else {
1129            match *module.get_visibility() {
1130                midenc_hir::Visibility::Public => masm::Visibility::Public,
1131                midenc_hir::Visibility::Internal | midenc_hir::Visibility::Private => {
1132                    masm::Visibility::Private
1133                }
1134            }
1135        };
1136        let module_index = if let Some(rest) = module_path.strip_prefix(&self.component.root) {
1137            self.define_module_tree(rest, Some(0), visibility)?
1138        } else {
1139            self.define_module_tree(&module_path, None, visibility)?
1140        };
1141
1142        let masm_module = Arc::get_mut(&mut self.component.modules[module_index])
1143            .expect("expected unique reference");
1144        let builder = MasmModuleBuilder {
1145            module: masm_module,
1146            analysis_manager: self.analysis_manager.nest(module.as_operation_ref()),
1147            link_info: self.link_info,
1148            source_manager: self.source_manager.clone(),
1149            init_body: &mut self.init_body,
1150            invoked_from_init: &mut self.invoked_from_init,
1151        };
1152        let nested = builder.build(module)?;
1153        for nested_module in nested {
1154            self.define_module(&nested_module.borrow())?;
1155        }
1156
1157        Ok(())
1158    }
1159
1160    fn define_module_tree(
1161        &mut self,
1162        module_path: &masm::Path,
1163        mut parent: Option<usize>,
1164        visibility: masm::Visibility,
1165    ) -> Result<usize, Report> {
1166        let trace_target = TraceTarget::category("codegen");
1167        let mut path = masm::PathBuf::with_capacity(256);
1168        if let Some(parent) = parent {
1169            path = self.component.modules[parent].path().to_path_buf();
1170        }
1171        let mut components = module_path.components().peekable();
1172        while let Some(component) = components.next() {
1173            let name = component.unwrap().as_str();
1174            // Ignore the root component
1175            if name == "::" {
1176                continue;
1177            }
1178            path.push_component(name);
1179            if !path.is_absolute() {
1180                path = path.to_absolute().unwrap().into_owned();
1181            }
1182            // Use the input visibility for the last module we crate, for parent modules, we must
1183            // specify public visibility so that references to this module are valid.
1184            let visibility = if components.peek().is_none() {
1185                visibility
1186            } else {
1187                masm::Visibility::Public
1188            };
1189            let module_path = &path;
1190            if let Some(parent_index) = parent {
1191                let parent_module = Arc::get_mut(&mut self.component.modules[parent_index])
1192                    .expect("expected unique reference");
1193                if parent_module.submodules().iter().any(|sm| sm.name.as_str() == name) {
1194                    // Already defined, look up the submodule as the new `parent`
1195                    parent = Some(
1196                        self.component
1197                            .modules
1198                            .iter()
1199                            .position(|m| m.path() == module_path.as_path())
1200                            .expect(
1201                                "submodule was already defined, but not registered with component",
1202                            ),
1203                    );
1204                } else {
1205                    // Create the submodule
1206                    let submodule =
1207                        Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
1208                    let name = masm::Ident::new(submodule.name()).unwrap();
1209                    log::debug!(target: &trace_target, "declaring submodule '{name}' of '{}'", parent_module.path());
1210                    parent_module.declare_submodule(name, visibility)?;
1211                    parent = Some(self.component.modules.len());
1212                    self.component.modules.push(Arc::from(submodule));
1213                }
1214            } else {
1215                log::debug!(target: &trace_target, "declaring module '{module_path}'");
1216                let module = Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
1217                parent = Some(self.component.modules.len());
1218                self.component.modules.push(Arc::from(module));
1219            }
1220        }
1221
1222        Ok(parent.unwrap())
1223    }
1224
1225    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
1226        let builder = MasmFunctionBuilder::new(function)?;
1227        let procedure = builder.build(
1228            function,
1229            self.analysis_manager.nest(function.as_operation_ref()),
1230            self.link_info,
1231            FunctionLoweringMode::Normal,
1232        )?;
1233
1234        let module =
1235            Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
1236        let expected_path_len = if module.path().is_absolute() { 2 } else { 1 };
1237        assert_eq!(
1238            module.path().len(),
1239            expected_path_len,
1240            "expected top-level namespace module, but one has not been defined (in '{}' of '{}')",
1241            module.path(),
1242            function.path()
1243        );
1244        module
1245            .define_procedure(procedure, self.source_manager.clone())
1246            .into_diagnostic()
1247            .wrap_err("failed to define MASM procedure")?;
1248
1249        Ok(())
1250    }
1251
1252    /// Emit the sequence of instructions necessary to consume rodata from the advice stack and
1253    /// populate the global heap with the data segments of this component, verifying that the
1254    /// commitments match.
1255    fn emit_data_segment_initialization(&mut self) {
1256        use masm::{Instruction as Inst, InvocationTarget, Op};
1257
1258        // Emit data segment initialization code
1259        //
1260        // NOTE: This depends on the program being executed with the data for all data segments
1261        // having been placed in the advice map with the same commitment and encoding used here.
1262        // The program will fail to execute if this is not set up correctly.
1263        let span = SourceSpan::default();
1264        let pipe_preimage_to_memory = {
1265            let name = masm::ProcedureName::new("pipe_preimage_to_memory").unwrap();
1266            let module = masm::LibraryPath::new("::miden::core::mem").unwrap();
1267            let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
1268            InvocationTarget::Path(Span::new(span, qualified.into_inner()))
1269        };
1270        for rodata in self.component.rodata.iter() {
1271            // Push the commitment hash (`COM`) for this data onto the operand stack
1272
1273            // WARNING: These two are equivalent, shouldn't this be a no-op?
1274            let word = rodata.digest.as_elements();
1275            let word_value = [word[0], word[1], word[2], word[3]];
1276
1277            self.init_body.push(Op::Inst(Span::new(
1278                span,
1279                Inst::Push(masm::Immediate::Value(Span::unknown(WordValue(word_value).into()))),
1280            )));
1281            // Move rodata from the advice map, using the commitment as key, to the advice stack
1282            self.init_body
1283                .push(Op::Inst(Span::new(span, Inst::SysEvent(masm::SystemEventNode::PushMapVal))));
1284            // write_ptr
1285            assert!(rodata.start.is_word_aligned(), "rodata segments must be word-aligned");
1286            self.init_body.push(Op::Inst(Span::new(
1287                span,
1288                Inst::Push(masm::Immediate::Value(Span::unknown(rodata.start.addr.into()))),
1289            )));
1290            // num_words
1291            self.init_body.push(Op::Inst(Span::new(
1292                span,
1293                Inst::Push(masm::Immediate::Value(Span::unknown(
1294                    (rodata.size_in_words() as u32).into(),
1295                ))),
1296            )));
1297            // [num_words, write_ptr, COM, ..] -> [write_ptr']
1298            self.init_body.push(Op::Inst(Span::new(
1299                span,
1300                Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
1301            )));
1302            self.init_body
1303                .push(Op::Inst(Span::new(span, Inst::Exec(pipe_preimage_to_memory.clone()))));
1304            self.init_body.push(Op::Inst(Span::new(
1305                span,
1306                Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
1307            )));
1308            // drop write_ptr'
1309            self.init_body.push(Op::Inst(Span::new(span, Inst::Drop)));
1310        }
1311    }
1312
1313    /// Build the slot-initialization code for every function table in the component, grouped by
1314    /// the module that *defines the callee* whose MAST root fills each slot.
1315    ///
1316    /// Grouping by callee rather than by table is what keeps every `procref` intra-module: a
1317    /// table in one module may name a callee in another, and it is the `procref` — not the
1318    /// store — that the assembler resolves against visibility.
1319    ///
1320    /// Only the entries [`builtin::FunctionTable::live_entries`] considers live are written: a
1321    /// later entry at the same index overwrites an earlier one, so the earlier one is dead. That
1322    /// is a soundness requirement rather than a saving. The `hir.exec_indirect` verifier compares
1323    /// signatures only for the entry that wins a slot, so a dead entry's callee has never been
1324    /// checked against any call site's stack contract — and grouping by owning module means store
1325    /// order no longer follows the entries' textual order, so "the last store wins" would not even
1326    /// pick the entry the verifier looked at.
1327    ///
1328    /// Uninitialized (null) slots are left as the zero word, since VM memory is
1329    /// zero-initialized; `dynexec` on such a slot fails at runtime.
1330    ///
1331    /// Each fragment carries the `procref`s its stores consume, which its procedure declares as
1332    /// invocations: that set is what the assembler's linker reads to build its call graph. It is
1333    /// a declaration of the dependency, not what creates it — the linker also resolves an
1334    /// invocation target from the instruction itself, so an omission here is a call graph missing
1335    /// an edge rather than an unresolved symbol.
1336    fn build_function_table_fragments(
1337        &self,
1338    ) -> Result<BTreeMap<masm::PathBuf, FunctionTableFragment>, Report> {
1339        use masm::{Instruction as Inst, Op};
1340
1341        let span = SourceSpan::default();
1342        let mut fragments: BTreeMap<masm::PathBuf, FunctionTableFragment> = BTreeMap::new();
1343        let layout = self.link_info.function_tables();
1344        for (table_ref, _) in layout.iter() {
1345            let base_addr = layout
1346                .element_addr_of(table_ref)
1347                .expect("link error: missing function table in computed layout");
1348            let table = table_ref.borrow();
1349            // Dead entries are skipped, not merely overwritten; see this function's doc comment.
1350            // A dead entry is therefore also unvalidated, which costs nothing for the bounds check
1351            // below — an overwritten entry shares its slot index with the entry that overwrote it,
1352            // so an out-of-bounds slot is still reported — and is if anything the right answer for
1353            // the tag: a slot explicitly nulled and then reassigned is not an error.
1354            let live_entries = table.live_entries().map_err(|op_name| {
1355                Report::msg(format!(
1356                    "invalid function table entry: '{op_name}' is not supported in a function \
1357                     table body"
1358                ))
1359            })?;
1360            for (slot, entry) in live_entries {
1361                let entry = entry.borrow();
1362                if slot >= *table.get_num_slots() {
1363                    return Err(Report::msg(format!(
1364                        "invalid function table entry: slot {slot} is out of bounds for table \
1365                         '{}' with {} slots",
1366                        table.get_name().as_str(),
1367                        *table.get_num_slots()
1368                    )));
1369                }
1370                let type_tag = *entry.get_type_tag();
1371                if type_tag == 0 {
1372                    return Err(Report::msg(format!(
1373                        "invalid function table entry: slot {slot} of table '{}' uses signature \
1374                         tag 0, which is reserved for null slots",
1375                        table.get_name().as_str(),
1376                    )));
1377                }
1378                let Some(callee) = entry.resolve_callee() else {
1379                    return Err(Report::msg(format!(
1380                        "invalid function table entry: unable to resolve callee '{}'",
1381                        entry.callee().path()
1382                    )));
1383                };
1384                let callee_path = callee.borrow().path();
1385                let target =
1386                    super::lowering::invocation_target_from_symbol_path(&callee_path, span);
1387
1388                // The fragment belongs to the module defining the callee: `procref` there needs
1389                // no visibility beyond what the callee already has
1390                let owner = callee_path.without_leaf().to_library_path();
1391                let owner = owner.to_absolute().unwrap().into_owned();
1392                let fragment = fragments.entry(owner).or_insert_with(|| FunctionTableFragment {
1393                    body: Default::default(),
1394                    invoked: Default::default(),
1395                    a_callee: callee_path.to_string(),
1396                });
1397                let FunctionTableFragment { body, invoked, .. } = fragment;
1398                invoked.insert(masm::Invoke::new(masm::InvokeKind::ProcRef, target.clone()));
1399
1400                // `procref` pushes the callee's MAST root word (`root[0]` on top),
1401                // `mem_storew_le` writes it to the slot's element address (leaving the word on
1402                // the stack), and `dropw` cleans up; the slot's signature tag is then stored in
1403                // the element right after the digest. The base is word-aligned and each slot is
1404                // exactly two words, so every slot address stays word-aligned as `dynexec`
1405                // requires.
1406                let slot_addr = base_addr + slot * FunctionTableLayout::SLOT_SIZE_ELEMENTS;
1407                let tag_addr = slot_addr + FunctionTableLayout::TYPE_TAG_OFFSET_ELEMENTS;
1408                body.push(Op::Inst(Span::new(span, Inst::ProcRef(target))));
1409                body.push(Op::Inst(Span::new(span, Inst::MemStoreWLeImm(slot_addr.into()))));
1410                body.push(Op::Inst(Span::new(span, Inst::DropW)));
1411                body.push(Op::Inst(Span::new(
1412                    span,
1413                    Inst::Push(masm::Immediate::Value(Span::new(span, type_tag.into()))),
1414                )));
1415                body.push(Op::Inst(Span::new(span, Inst::MemStoreImm(tag_addr.into()))));
1416            }
1417        }
1418
1419        Ok(fragments)
1420    }
1421}
1422
1423/// The slot-initialization code one module contributes, for the callees *it* defines.
1424struct FunctionTableFragment {
1425    /// The stores that fill those slots.
1426    body: Vec<masm::Op>,
1427    /// The `procref`s those stores consume, for the assembler's linker.
1428    invoked: BTreeSet<masm::Invoke>,
1429    /// One of the callees that put this fragment here, for diagnostics. Any of them identifies
1430    /// the module as well as another, and reporting one is more use than reporting the module
1431    /// path alone.
1432    a_callee: String,
1433}
1434
1435struct MasmModuleBuilder<'a> {
1436    module: &'a mut masm::Module,
1437    analysis_manager: AnalysisManager,
1438    link_info: &'a LinkInfo,
1439    source_manager: Arc<dyn midenc_session::SourceManager>,
1440    init_body: &'a mut Vec<masm::Op>,
1441    invoked_from_init: &'a mut BTreeSet<masm::Invoke>,
1442}
1443
1444impl MasmModuleBuilder<'_> {
1445    /// Lower `module`'s body, returning any modules nested within it.
1446    ///
1447    /// A nested module is not lowered here: MASM's module set is flat and keyed by path, and
1448    /// [`MasmComponentBuilder::define_module`] is what turns a fully-qualified HIR module path
1449    /// into that set's entry. Returning them lets the component builder recurse without this
1450    /// builder having to know how modules are rooted.
1451    pub fn build(mut self, module: &builtin::Module) -> Result<Vec<builtin::ModuleRef>, Report> {
1452        let mut nested = Vec::new();
1453        let region = module.body();
1454        let block = region.entry();
1455        for op in block.body() {
1456            if let Some(function) = op.downcast_ref::<builtin::Function>() {
1457                self.define_function(function)?;
1458            } else if let Some(gv) = op.downcast_ref::<builtin::GlobalVariable>() {
1459                self.emit_global_variable_initializer(gv)?;
1460            } else if let Some(nested_module) = op.downcast_ref::<builtin::Module>() {
1461                nested.push(nested_module.as_module_ref());
1462            } else if op.is::<builtin::Segment>() {
1463                continue;
1464            } else if op.is::<builtin::FunctionTable>() {
1465                // Laid out by the linker; slots are filled by the `__init_function_table`
1466                // procedures `MasmComponentBuilder::build` attaches to the modules defining the
1467                // callees, from fragments `build_function_table_fragments` produces
1468                continue;
1469            } else {
1470                panic!(
1471                    "invalid module-level operation: '{}' is not legal in a MASM module body",
1472                    op.name()
1473                )
1474            }
1475        }
1476
1477        Ok(nested)
1478    }
1479
1480    pub fn build_from_interface(mut self, interface: &builtin::Interface) -> Result<(), Report> {
1481        let region = interface.body();
1482        let block = region.entry();
1483        for op in block.body() {
1484            if let Some(function) = op.downcast_ref::<builtin::Function>() {
1485                self.define_function(function)?;
1486            } else {
1487                panic!(
1488                    "invalid interface-level operation: '{}' is not legal in a MASM module body",
1489                    op.name()
1490                )
1491            }
1492        }
1493
1494        Ok(())
1495    }
1496
1497    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
1498        let builder = MasmFunctionBuilder::new(function)?;
1499
1500        let procedure = builder.build(
1501            function,
1502            self.analysis_manager.nest(function.as_operation_ref()),
1503            self.link_info,
1504            FunctionLoweringMode::Normal,
1505        )?;
1506
1507        self.module
1508            .define_procedure(procedure, self.source_manager.clone())
1509            .map_err(|e| Report::msg(e.to_string()))?;
1510
1511        Ok(())
1512    }
1513
1514    fn emit_global_variable_initializer(
1515        &mut self,
1516        gv: &builtin::GlobalVariable,
1517    ) -> Result<(), Report> {
1518        // We don't emit anything for declarations
1519        if gv.is_declaration() {
1520            return Ok(());
1521        }
1522
1523        // We compute liveness for global variables independently
1524        let analysis_manager = self.analysis_manager.nest(gv.as_operation_ref());
1525        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
1526
1527        // Emit the initializer block
1528        let initializer_region = gv.region(0);
1529        let initializer_block = initializer_region.entry();
1530
1531        let mut block_emitter = BlockEmitter {
1532            liveness: &liveness,
1533            link_info: self.link_info,
1534            invoked: self.invoked_from_init,
1535            target: Default::default(),
1536            stack: OperandStack::new(gv.as_operation().context_rc()),
1537            trace_target: TraceTarget::category("codegen")
1538                .with_relevant_symbol(gv.name().as_symbol()),
1539        };
1540        block_emitter.emit_inline(&initializer_block);
1541
1542        // Sanity checks
1543        assert_eq!(block_emitter.stack.len(), 1, "expected only global variable value on stack");
1544        let return_ty = block_emitter.stack.peek().unwrap().ty();
1545        assert_eq!(
1546            &return_ty,
1547            &*gv.get_ty(),
1548            "expected initializer to return value of same type as declaration"
1549        );
1550
1551        // Write the initialized value to the computed storage offset for this global
1552        let computed_addr = self
1553            .link_info
1554            .globals_layout()
1555            .get_computed_addr(gv.as_global_var_ref())
1556            .expect("undefined global variable");
1557        block_emitter.emitter().store_imm(computed_addr, gv.span());
1558
1559        // Extend the generated init function with the code to initialize this global
1560        let mut body = core::mem::take(&mut block_emitter.target);
1561        self.init_body.append(&mut body);
1562
1563        Ok(())
1564    }
1565}
1566
1567struct MasmFunctionBuilder {
1568    span: midenc_hir::SourceSpan,
1569    name: masm::ProcedureName,
1570    signature: masm::FunctionType,
1571    visibility: masm::Visibility,
1572    num_locals: u16,
1573}
1574
1575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1576enum FunctionLoweringMode {
1577    /// Emit a procedure with all prologues and public metadata implied by its HIR function.
1578    Normal,
1579    /// Emit the selected canonical entry body for generated executable `main`, which has already
1580    /// initialized the component and must not repeat the public wrapper's `init` prologue.
1581    ExecutableEntrypointWithoutInit,
1582}
1583
1584impl MasmFunctionBuilder {
1585    /// Prepare to translate `function`, or report why it cannot be translated.
1586    ///
1587    /// This is the single point every function reaches, whichever kind of item declares it —
1588    /// `MasmComponentBuilder::define_function` for a component-level function,
1589    /// `MasmModuleBuilder::define_function` for one in a module or an interface — which is why the
1590    /// check below lives here rather than at any one of them.
1591    pub fn new(function: &builtin::Function) -> Result<Self, Report> {
1592        use midenc_hir::{Symbol, Visibility};
1593
1594        if function.is_declaration() {
1595            return Err(function_without_a_body(function));
1596        }
1597
1598        let name = *function.get_name();
1599        let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
1600            name.span,
1601            name.as_ref().into(),
1602        )));
1603        let visibility = match function.visibility() {
1604            Visibility::Public => masm::Visibility::Public,
1605            // TODO(pauls): Support internal visibility in MASM
1606            Visibility::Internal => masm::Visibility::Public,
1607            Visibility::Private => masm::Visibility::Private,
1608        };
1609        let locals_required = function.locals().iter().map(|ty| ty.size_in_felts()).sum::<usize>();
1610        let num_locals = u16::try_from(locals_required).map_err(|_| {
1611            let context = function.as_operation().context();
1612            context
1613                .diagnostics()
1614                .diagnostic(miden_assembly::diagnostics::Severity::Error)
1615                .with_message("cannot emit masm for function")
1616                .with_primary_label(
1617                    function.span(),
1618                    "local storage exceeds procedure limit: no more than u16::MAX elements are \
1619                     supported",
1620                )
1621                .into_report()
1622        })?;
1623
1624        let signature =
1625            semantic_debug_signature(function).unwrap_or_else(|| lowered_signature(function));
1626
1627        Ok(Self {
1628            span: function.span(),
1629            name,
1630            signature,
1631            visibility,
1632            num_locals,
1633        })
1634    }
1635
1636    pub fn build(
1637        self,
1638        function: &builtin::Function,
1639        analysis_manager: AnalysisManager,
1640        link_info: &LinkInfo,
1641        mode: FunctionLoweringMode,
1642    ) -> Result<masm::Procedure, Report> {
1643        use alloc::collections::BTreeSet;
1644
1645        use midenc_hir_analysis::analyses::LivenessAnalysis;
1646
1647        let demangled_symbol_name = midenc_hir::demangle::demangle(function.get_name().as_str());
1648        let trace_target = TraceTarget::category("codegen")
1649            .with_relevant_symbol(midenc_hir::SymbolName::intern(demangled_symbol_name));
1650
1651        log::trace!(target: &trace_target, "lowering {}", function.as_operation());
1652
1653        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
1654
1655        let mut invoked = BTreeSet::default();
1656        let entry = function.entry_block();
1657        let mut stack = crate::OperandStack::new(function.as_operation().context_rc());
1658        {
1659            let entry_block = entry.borrow();
1660            for arg in entry_block.arguments().iter().rev().copied() {
1661                stack.push(arg as ValueRef);
1662            }
1663        }
1664        let mut emitter = BlockEmitter {
1665            liveness: &liveness,
1666            link_info,
1667            invoked: &mut invoked,
1668            target: Default::default(),
1669            stack,
1670            trace_target,
1671        };
1672
1673        // For component export functions, invoke the `init` procedure first if needed.
1674        // It loads the data segments, global vars, and function tables into memory.
1675        if mode == FunctionLoweringMode::Normal
1676            && function.signature().cc.is_wasm_canonical_abi()
1677            && link_info.requires_init()
1678        {
1679            // Resolve `init` symbolically within the containing module instead of through a
1680            // fully-qualified component path, which depends on the (user-editable)
1681            // `[lib].namespace` matching the component's library identity.
1682            //
1683            // INVARIANT: this relies on the canonical-ABI export wrappers being emitted into the
1684            // root component module — the same module where `MasmComponentBuilder` defines
1685            // `init` (`self.component.modules[0]`); the inner lifted functions in interface and
1686            // core child modules carry no init prologue. If export wrappers ever move into child
1687            // modules, this symbol stops resolving and the init target must be threaded in as a
1688            // qualified path instead. A user-exported method named `init` collides with the
1689            // generated procedure at definition time ("symbol conflict: found duplicate
1690            // definitions"), so it cannot silently shadow this target.
1691            let init = InvocationTarget::Symbol("init".parse().unwrap());
1692            // Add init call to the emitter's target before emitting the function body; `emit`
1693            // also registers the invocation so the assembler can resolve the symbolic target.
1694            emitter.emitter().emit(masm::Instruction::Exec(init), SourceSpan::default());
1695        }
1696
1697        let mut body = emitter.emit(&entry.borrow());
1698
1699        if function.signature().cc.is_wasm_canonical_abi() {
1700            // Truncate the stack to 16 elements on exit in the component export function
1701            // since it is expected to be `call`ed so it has a requirement to have
1702            // no more than 16 elements on the stack when it returns.
1703            // See https://0xmiden.github.io/miden-vm/user_docs/assembly/execution_contexts.html
1704            // Since the VM's `drop` instruction not letting stack size go beyond the 16 elements
1705            // we most likely end up with stack size > 16 elements at the end.
1706            // See https://github.com/0xPolygonMiden/miden-vm/blob/c4acf49510fda9ba80f20cee1a9fb1727f410f47/processor/src/stack/mod.rs?plain=1#L226-L253
1707            let truncate_stack = {
1708                let name = masm::ProcedureName::new("truncate_stack").unwrap();
1709                let module = masm::LibraryPath::new("::miden::core::sys").unwrap();
1710                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
1711                InvocationTarget::Path(Span::new(SourceSpan::default(), qualified.into_inner()))
1712            };
1713            let span = SourceSpan::default();
1714            invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
1715            body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(truncate_stack))));
1716        }
1717        let Self {
1718            span,
1719            name,
1720            signature,
1721            visibility,
1722            num_locals,
1723        } = self;
1724
1725        // Align num_locals to WORD_SIZE, matching the assembler's FMP frame sizing.
1726        // num_locals already counts all HIR locals (including those allocated for params).
1727        // The assembler rounds up to next_multiple_of(WORD_SIZE) when advancing FMP
1728        // (see fmp.rs fmp_start_frame_sequence and mem_ops.rs locaddr), so we must use
1729        // the same alignment for debug var offset computation.
1730        let aligned_num_locals = num_locals.next_multiple_of(miden_core::WORD_SIZE as u16);
1731
1732        // Resolve FrameBase global_index → Miden memory address.
1733        // Use the stack pointer offset from the linker's global layout.
1734        let stack_pointer_addr = link_info.globals_layout().stack_pointer_offset();
1735
1736        // Patch DebugVar Local locations to compute FMP offset.
1737        // During lowering, Local(idx) stores the raw WASM local index.
1738        // Now convert to FMP offset: idx - aligned_num_locals
1739        // This matches locaddr.N which computes -(aligned_num_locals - N).
1740        patch_debug_var_locals_in_block(&mut body, aligned_num_locals, stack_pointer_addr);
1741
1742        // If a function body after lowering produces a MASM procedure with an empty body aside
1743        // from debug decorators, then we must emit a `nop` at the end of the block which will
1744        // act as the anchor for those decorators. Such a procedure is basically useless, as it is
1745        // just passing through arguments as results - but the assembler currently rejects empty
1746        // procedures (not counting decorators), so we must handle this edge case.
1747        if !block_has_real_instructions(&body) {
1748            body.push(masm::Op::Inst(Span::unknown(masm::Instruction::Nop)));
1749        }
1750
1751        let mut procedure = masm::Procedure::new(span, visibility, name, num_locals, body);
1752        procedure.set_signature(signature);
1753        if mode == FunctionLoweringMode::Normal {
1754            for attribute in [
1755                midenc_dialect_hir::ACCOUNT_PROCEDURE_EXPORT_ATTR,
1756                midenc_dialect_hir::AUTH_SCRIPT_EXPORT_ATTR,
1757                midenc_dialect_hir::NOTE_SCRIPT_EXPORT_ATTR,
1758                midenc_dialect_hir::TRANSACTION_SCRIPT_EXPORT_ATTR,
1759            ] {
1760                if function.has_attribute(attribute) {
1761                    procedure
1762                        .attributes_mut()
1763                        .insert(Attribute::Marker(masm::Ident::new(attribute).unwrap()));
1764                }
1765            }
1766        }
1767        procedure.extend_invoked(invoked);
1768
1769        Ok(procedure)
1770    }
1771}
1772
1773fn lowered_signature(function: &builtin::Function) -> masm::FunctionType {
1774    let sig = function.signature();
1775    let args = sig.params.iter().map(|param| masm::TypeExpr::from(param.ty.clone())).collect();
1776    let results = sig
1777        .results
1778        .iter()
1779        .map(|result| masm::TypeExpr::from(result.ty.clone()))
1780        .collect();
1781    masm::FunctionType::new(sig.cc, args, results)
1782}
1783
1784fn semantic_debug_signature(function: &builtin::Function) -> Option<masm::FunctionType> {
1785    let subprogram = function
1786        .as_operation()
1787        .get_attribute("di.subprogram")?
1788        .try_downcast_attr::<SubprogramAttr>()
1789        .ok()?;
1790    let subprogram = subprogram.borrow();
1791    let Type::Function(ty) = subprogram.ty.as_ref()? else {
1792        return None;
1793    };
1794
1795    let args = ty.params().iter().cloned().map(masm::TypeExpr::from).collect();
1796    let results = ty.results().iter().cloned().map(masm::TypeExpr::from).collect();
1797    Some(masm::FunctionType::new(ty.calling_convention(), args, results))
1798}
1799
1800/// Returns true if the block contains at least one real (non-decorator) instruction.
1801///
1802/// DebugVar instructions are decorator-only and don't produce MAST nodes. If a procedure
1803/// body contains only DebugVar ops, the assembler will reject it.
1804fn block_has_real_instructions(block: &masm::Block) -> bool {
1805    block.iter().any(|op| match op {
1806        masm::Op::Inst(inst) => !matches!(inst.inner(), masm::Instruction::DebugVar(_)),
1807        masm::Op::If {
1808            then_blk, else_blk, ..
1809        } => block_has_real_instructions(then_blk) || block_has_real_instructions(else_blk),
1810        masm::Op::While { body, .. } => block_has_real_instructions(body),
1811        masm::Op::DoWhile {
1812            body, condition, ..
1813        } => block_has_real_instructions(body) || block_has_real_instructions(condition),
1814        masm::Op::Repeat { body, .. } => block_has_real_instructions(body),
1815    })
1816}
1817
1818/// Recursively patch DebugVar locations in a block.
1819///
1820/// Converts `Local(idx)` where idx is the raw WASM local index to `Local(offset)` where
1821/// `offset = idx - aligned_num_locals` (the FMP-relative offset, typically negative). This matches
1822/// the assembler's `locaddr.N` formula, i.e. `FMP - aligned_num_locals + N`.
1823///
1824/// Also resolves Wasm frame bases to the Miden local/global encoding understood by the debugger.
1825/// Locations that require resolution but cannot be represented are converted to explicit kill
1826/// markers so the unresolved Wasm location cannot remain active.
1827fn patch_debug_var_locals_in_block(
1828    block: &mut masm::Block,
1829    aligned_num_locals: u16,
1830    stack_pointer_addr: Option<u32>,
1831) {
1832    for op in block.iter_mut() {
1833        match op {
1834            masm::Op::Inst(span_inst) => {
1835                // Use DerefMut to get mutable access to the inner Instruction
1836                if let masm::Instruction::DebugVar(info) = &mut **span_inst {
1837                    let location = patch_debug_var_location(
1838                        info.value_location(),
1839                        aligned_num_locals,
1840                        stack_pointer_addr,
1841                    );
1842                    info.set_value_location(location);
1843                }
1844            }
1845            masm::Op::If {
1846                then_blk, else_blk, ..
1847            } => {
1848                patch_debug_var_locals_in_block(then_blk, aligned_num_locals, stack_pointer_addr);
1849                patch_debug_var_locals_in_block(else_blk, aligned_num_locals, stack_pointer_addr);
1850            }
1851            masm::Op::While {
1852                body: while_body, ..
1853            } => {
1854                patch_debug_var_locals_in_block(while_body, aligned_num_locals, stack_pointer_addr);
1855            }
1856            masm::Op::DoWhile {
1857                body, condition, ..
1858            } => {
1859                patch_debug_var_locals_in_block(body, aligned_num_locals, stack_pointer_addr);
1860                patch_debug_var_locals_in_block(condition, aligned_num_locals, stack_pointer_addr);
1861            }
1862            masm::Op::Repeat {
1863                body: repeat_body, ..
1864            } => {
1865                patch_debug_var_locals_in_block(
1866                    repeat_body,
1867                    aligned_num_locals,
1868                    stack_pointer_addr,
1869                );
1870            }
1871        }
1872    }
1873}
1874
1875fn patch_debug_var_location(
1876    location: &DebugVarLocation,
1877    aligned_num_locals: u16,
1878    stack_pointer_addr: Option<u32>,
1879) -> DebugVarLocation {
1880    match location {
1881        DebugVarLocation::Local(index) => {
1882            checked_fmp_local_offset(i64::from(*index), aligned_num_locals)
1883                .map(DebugVarLocation::Local)
1884                .unwrap_or_else(debug_var_kill_location)
1885        }
1886        DebugVarLocation::FrameBase { byte_offset, .. } => {
1887            if let Some(resolved_addr) = stack_pointer_addr.filter(|addr| *addr < (1 << 31)) {
1888                DebugVarLocation::FrameBase {
1889                    global_index: resolved_addr,
1890                    byte_offset: *byte_offset,
1891                }
1892            } else {
1893                debug_var_kill_location()
1894            }
1895        }
1896        DebugVarLocation::Expression(bytes) => {
1897            let Ok(expression) = Expression::read_from_bytes_with_budget(bytes, bytes.len()) else {
1898                return location.clone();
1899            };
1900            let [
1901                ExpressionOp::FrameBase {
1902                    base: FrameBase::Local(local_index),
1903                    byte_offset,
1904                },
1905            ] = expression.operations.as_slice()
1906            else {
1907                return location.clone();
1908            };
1909            checked_fmp_local_offset(i64::from(*local_index), aligned_num_locals)
1910                .map(|local_offset| DebugVarLocation::FrameBase {
1911                    global_index: encode_frame_base_local_offset(local_offset),
1912                    byte_offset: *byte_offset,
1913                })
1914                .unwrap_or_else(debug_var_kill_location)
1915        }
1916        DebugVarLocation::Stack(_) | DebugVarLocation::Memory(_) | DebugVarLocation::Const(_) => {
1917            location.clone()
1918        }
1919    }
1920}
1921
1922fn checked_fmp_local_offset(index: i64, aligned_num_locals: u16) -> Option<i16> {
1923    i16::try_from(index - i64::from(aligned_num_locals)).ok()
1924}
1925
1926fn debug_var_kill_location() -> DebugVarLocation {
1927    DebugVarLocation::Expression(super::DEBUG_VAR_KILL_SENTINEL.to_vec())
1928}
1929
1930const FRAME_BASE_LOCAL_MARKER: u32 = 1 << 31;
1931
1932fn encode_frame_base_local_offset(local_offset: i16) -> u32 {
1933    FRAME_BASE_LOCAL_MARKER | u32::from(u16::from_le_bytes(local_offset.to_le_bytes()))
1934}