Skip to main content

midenc_codegen_masm/lower/
component.rs

1use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};
2
3use miden_assembly::{PathBuf as LibraryPath, ast::InvocationTarget};
4use miden_assembly_syntax::{ast::Attribute, parser::WordValue};
5use miden_core::operations::DebugVarLocation;
6use midenc_hir::{
7    FunctionIdent, Op, OpExt, SourceSpan, Span, Symbol, TraceTarget, Type, ValueRef,
8    diagnostics::IntoDiagnostic,
9    dialects::{
10        builtin,
11        debuginfo::attributes::{
12            SubprogramAttr, decode_frame_base_local_index, encode_frame_base_local_offset,
13        },
14    },
15    interner,
16    pass::AnalysisManager,
17};
18use midenc_hir_analysis::analyses::LivenessAnalysis;
19use midenc_session::diagnostics::{Report, Spanned, WrapErr};
20use smallvec::SmallVec;
21
22use crate::{
23    OperandStack, TraceEvent,
24    artifact::MasmComponent,
25    emitter::BlockEmitter,
26    linker::{LinkInfo, Linker},
27    masm,
28};
29
30/// This trait represents a conversion pass from some HIR entity to a Miden Assembly component.
31pub trait ToMasmComponent {
32    fn to_masm_component(&self, analysis_manager: AnalysisManager)
33    -> Result<MasmComponent, Report>;
34}
35
36/// Derivation of a MASM component from an HIR world
37///
38/// This currently works by treating all definition-carrying modules in the world as part of a
39/// single logical component.
40impl ToMasmComponent for builtin::World {
41    fn to_masm_component(
42        &self,
43        analysis_manager: AnalysisManager,
44    ) -> Result<MasmComponent, Report> {
45        // Get the current compiler context
46        let context = self.as_operation().context_rc();
47
48        // Run the linker for this component in order to compute its data layout
49        let link_info = Linker::default().link(None, self.as_operation()).map_err(Report::msg)?;
50
51        // Get the entrypoint, if specified
52        let entrypoint = match context.session().options.entrypoint.as_deref() {
53            Some(entry) => {
54                let entry_id = entry.parse::<FunctionIdent>().map_err(|_| {
55                    Report::msg(format!("invalid entrypoint identifier: '{entry}'"))
56                })?;
57                let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(
58                    Span::new(entry_id.function.span, entry_id.function.as_str().into()),
59                ));
60
61                let path = LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?;
62                let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
63                Some(masm::InvocationTarget::Path(Span::new(
64                    entry_id.function.span,
65                    qualified.into_inner(),
66                )))
67            }
68            None => None,
69        };
70
71        // If we have global variables or data segments, we will require a component initializer
72        // function, as well as a module to hold component-level functions such as init
73        let requires_init = link_info.has_globals() || link_info.has_data_segments();
74        let init = if requires_init {
75            let name = masm::ProcedureName::new("init").unwrap();
76            let qualified = masm::QualifiedProcedureName::new("::init", name);
77            Some(masm::InvocationTarget::Path(Span::new(
78                SourceSpan::default(),
79                qualified.into_inner(),
80            )))
81        } else {
82            None
83        };
84
85        // Define the initial component modules set
86        //
87        // The top-level component module is always defined, but may be empty
88        let root =
89            Arc::<miden_assembly_syntax::Path>::from(miden_assembly_syntax::Path::new("::init"));
90        let init_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
91        let modules = vec![init_module];
92
93        let rodata = data_segments_to_rodata(&link_info)?;
94
95        let kernel = if context.session().options.target_requires_protocol() {
96            Some(miden_protocol::transaction::TransactionKernel::kernel())
97        } else {
98            None
99        };
100
101        // Compute the first page boundary after the end of the globals table (or reserved memory
102        // if no globals) to use as the start of the dynamic heap when the program is executed
103        let heap_base = core::cmp::max(
104            link_info.reserved_memory_bytes(),
105            link_info.globals_layout().next_page_boundary() as usize,
106        );
107        let heap_base = u32::try_from(heap_base)
108            .expect("unable to allocate dynamic heap: global table too large");
109        let stack_pointer = link_info.globals_layout().stack_pointer_offset();
110        let mut masm_component = MasmComponent {
111            id: None,
112            root,
113            init,
114            entrypoint,
115            kernel,
116            rodata,
117            heap_base,
118            stack_pointer,
119            modules,
120        };
121        let builder = MasmComponentBuilder {
122            analysis_manager,
123            component: &mut masm_component,
124            link_info: &link_info,
125            source_manager: context.session().source_manager.clone(),
126            init_body: Default::default(),
127            invoked_from_init: Default::default(),
128        };
129
130        builder.build(self.as_operation())?;
131
132        Ok(masm_component)
133    }
134}
135
136/// 1:1 conversion from HIR component to MASM component
137impl ToMasmComponent for builtin::Component {
138    fn to_masm_component(
139        &self,
140        analysis_manager: AnalysisManager,
141    ) -> Result<MasmComponent, Report> {
142        // Get the current compiler context
143        let context = self.as_operation().context_rc();
144
145        // Run the linker for this component in order to compute its data layout
146        let id = self.id();
147        let link_info = Linker::default()
148            .link(Some(id.clone()), self.as_operation())
149            .map_err(Report::msg)?;
150
151        // Get the library path of the component
152        let component_path = id.to_library_path();
153
154        // Get the entrypoint, if specified
155        let entrypoint = match context.session().options.entrypoint.as_deref() {
156            Some(entry) => {
157                let entry_id = entry.parse::<FunctionIdent>().map_err(|_| {
158                    Report::msg(format!("invalid entrypoint identifier: '{entry}'"))
159                })?;
160                let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(
161                    Span::new(entry_id.function.span, entry_id.function.as_str().into()),
162                ));
163
164                // Check if we're inside the synthetic "wrapper" component used for pure Rust
165                // compilation. Since the user does not know about it, their entrypoint does not
166                // include the synthetic component path. We append the user-provided path to the
167                // root component path here if needed.
168                //
169                // TODO(pauls): Narrow this to only be true if the target env is not 'rollup', we
170                // cannot currently do so because we do not have sufficient Cargo metadata yet in
171                // 'cargo miden build' to detect the target env, and we default it to 'rollup'
172                let is_wrapper = id.is_synthetic_wrapper();
173                let path = if is_wrapper {
174                    let mut path = component_path.clone();
175                    path.push(entry_id.module.as_str());
176                    path
177                } else {
178                    // We're compiling a Wasm component and the component id is included
179                    // in the entrypoint.
180                    LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?
181                };
182                let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
183                Some(masm::InvocationTarget::Path(Span::new(
184                    entry_id.function.span,
185                    qualified.into_inner(),
186                )))
187            }
188            None => None,
189        };
190
191        // If we have global variables or data segments, we will require a component initializer
192        // function, as well as a module to hold component-level functions such as init
193        let requires_init = link_info.has_globals() || link_info.has_data_segments();
194        let init = if requires_init {
195            let name = masm::ProcedureName::new("init").unwrap();
196            let qualified =
197                masm::QualifiedProcedureName::new(component_path.as_path().to_absolute(), name);
198            Some(masm::InvocationTarget::Path(Span::new(
199                SourceSpan::default(),
200                qualified.into_inner(),
201            )))
202        } else {
203            None
204        };
205
206        // Define the initial component modules set
207        //
208        // The top-level component module is always defined, but may be empty
209        let root: Arc<miden_assembly_syntax::Path> =
210            id.to_library_path().to_absolute().into_owned().into();
211        let modules = vec![Arc::new(masm::Module::new(masm::ModuleKind::Library, &root))];
212
213        let rodata = data_segments_to_rodata(&link_info)?;
214
215        let kernel = if context.session().options.target_requires_protocol() {
216            Some(miden_protocol::transaction::TransactionKernel::kernel())
217        } else {
218            None
219        };
220
221        // Compute the first page boundary after the end of the globals table (or reserved memory
222        // if no globals) to use as the start of the dynamic heap when the program is executed
223        let heap_base = core::cmp::max(
224            link_info.reserved_memory_bytes(),
225            link_info.globals_layout().next_page_boundary() as usize,
226        );
227        let heap_base = u32::try_from(heap_base)
228            .expect("unable to allocate dynamic heap: global table too large");
229        let stack_pointer = link_info.globals_layout().stack_pointer_offset();
230        let mut masm_component = MasmComponent {
231            id: Some(id),
232            root,
233            init,
234            entrypoint,
235            kernel,
236            rodata,
237            heap_base,
238            stack_pointer,
239            modules,
240        };
241        let builder = MasmComponentBuilder {
242            analysis_manager,
243            component: &mut masm_component,
244            link_info: &link_info,
245            source_manager: context.session().source_manager.clone(),
246            init_body: Default::default(),
247            invoked_from_init: Default::default(),
248        };
249
250        builder.build(self.as_operation())?;
251
252        Ok(masm_component)
253    }
254}
255
256fn data_segments_to_rodata(link_info: &LinkInfo) -> Result<Vec<crate::Rodata>, Report> {
257    use midenc_hir::constants::ConstantData;
258
259    use crate::data_segments::{ResolvedDataSegment, merge_data_segments};
260    let mut resolved = SmallVec::<[ResolvedDataSegment; 2]>::new();
261    for sref in link_info.segment_layout().iter() {
262        let s = sref.borrow();
263        resolved.push(ResolvedDataSegment {
264            offset: *s.get_offset(),
265            data: s.initializer().as_slice().to_vec(),
266            readonly: *s.get_readonly(),
267        });
268    }
269    Ok(match merge_data_segments(resolved).map_err(Report::msg)? {
270        None => alloc::vec::Vec::new(),
271        Some(merged) => {
272            let data = alloc::sync::Arc::new(ConstantData::from(merged.data));
273            let felts = crate::Rodata::bytes_to_elements(data.as_slice());
274            let digest = miden_core::crypto::hash::Poseidon2::hash_elements(&felts);
275            alloc::vec![crate::Rodata {
276                component: link_info.component().cloned().unwrap_or(builtin::ComponentId {
277                    namespace: interner::Symbol::intern("root_ns"),
278                    name: interner::Symbol::intern("root"),
279                    version: midenc_hir::version::Version::new(1, 0, 0)
280                }),
281                digest,
282                start: super::NativePtr::from_ptr(merged.offset),
283                data,
284            }]
285        }
286    })
287}
288
289struct MasmComponentBuilder<'a> {
290    component: &'a mut MasmComponent,
291    analysis_manager: AnalysisManager,
292    link_info: &'a LinkInfo,
293    source_manager: Arc<dyn midenc_session::SourceManager + Send + Sync>,
294    init_body: Vec<masm::Op>,
295    invoked_from_init: BTreeSet<masm::Invoke>,
296}
297
298impl MasmComponentBuilder<'_> {
299    /// Convert the component body to Miden Assembly
300    pub fn build(mut self, component: &midenc_hir::Operation) -> Result<(), Report> {
301        use masm::{Instruction as Inst, InvocationTarget, Op};
302
303        // If a component-level init is required, emit code to initialize the heap before any other
304        // initialization code.
305        if self.component.init.is_some() {
306            let span = component.span();
307
308            // Heap metadata initialization
309            let heap_base = self.component.heap_base;
310            self.init_body.push(masm::Op::Inst(Span::new(
311                span,
312                Inst::Push(masm::Immediate::Value(Span::unknown(heap_base.into()))),
313            )));
314            let heap_init = {
315                let name = masm::ProcedureName::new("heap_init").unwrap();
316                let module = masm::LibraryPath::new("::intrinsics::mem").unwrap();
317                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
318                InvocationTarget::Path(Span::new(span, qualified.into_inner()))
319            };
320            self.init_body.push(Op::Inst(Span::new(
321                span,
322                Inst::Trace(TraceEvent::FrameStart.as_u32().into()),
323            )));
324            self.init_body.push(Op::Inst(Span::new(span, Inst::Exec(heap_init))));
325            self.init_body
326                .push(Op::Inst(Span::new(span, Inst::Trace(TraceEvent::FrameEnd.as_u32().into()))));
327
328            // Data segment initialization
329            self.emit_data_segment_initialization();
330        }
331
332        // Translate component body
333        let region = component.region(0);
334        let block = region.entry();
335        for op in block.body() {
336            if let Some(module) = op.downcast_ref::<builtin::Module>() {
337                self.define_module(module)?;
338            } else if let Some(interface) = op.downcast_ref::<builtin::Interface>() {
339                self.define_interface(interface)?;
340            } else if let Some(function) = op.downcast_ref::<builtin::Function>() {
341                self.define_function(function)?;
342            } else {
343                panic!(
344                    "invalid component-level operation: '{}' is not supported in a component body",
345                    op.name()
346                )
347            }
348        }
349
350        // Finalize the component-level init, if required
351        if self.component.init.is_some() {
352            let module =
353                Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
354
355            let init_name = masm::ProcedureName::new("init").unwrap();
356            let init_body = core::mem::take(&mut self.init_body);
357            let init = masm::Procedure::new(
358                Default::default(),
359                masm::Visibility::Public,
360                init_name,
361                0,
362                masm::Block::new(component.span(), init_body),
363            )
364            .with_signature(masm::FunctionType::new(
365                midenc_hir::CallConv::Fast,
366                vec![],
367                vec![],
368            ));
369
370            module
371                .define_procedure(init, self.source_manager.clone())
372                .into_diagnostic()
373                .wrap_err("failed to define component `init` procedure")?;
374        } else {
375            assert!(
376                self.init_body.is_empty(),
377                "the need for an 'init' function was not expected, but code was generated for one"
378            );
379        }
380
381        Ok(())
382    }
383
384    fn define_interface(&mut self, interface: &builtin::Interface) -> Result<(), Report> {
385        let interface_path = if let Some(id) = self.component.id.as_ref() {
386            let mut path = id.to_library_path();
387            path.push(interface.name().as_str());
388            path
389        } else {
390            interface.path().to_library_path()
391        };
392        let mut masm_module =
393            Box::new(masm::Module::new(masm::ModuleKind::Library, interface_path));
394        let builder = MasmModuleBuilder {
395            module: &mut masm_module,
396            analysis_manager: self
397                .analysis_manager
398                .nest(interface.as_operation().as_operation_ref()),
399            link_info: self.link_info,
400            source_manager: self.source_manager.clone(),
401            init_body: &mut self.init_body,
402            invoked_from_init: &mut self.invoked_from_init,
403        };
404        builder.build_from_interface(interface)?;
405
406        self.component.modules.push(Arc::from(masm_module));
407
408        Ok(())
409    }
410
411    fn define_module(&mut self, module: &builtin::Module) -> Result<(), Report> {
412        let module_path = if let Some(id) = self.component.id.as_ref() {
413            let mut path = id.to_library_path();
414            path.push(module.name().as_str());
415            path
416        } else {
417            module.path().to_library_path()
418        };
419        let mut masm_module = Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
420        let builder = MasmModuleBuilder {
421            module: &mut masm_module,
422            analysis_manager: self.analysis_manager.nest(module.as_operation_ref()),
423            link_info: self.link_info,
424            source_manager: self.source_manager.clone(),
425            init_body: &mut self.init_body,
426            invoked_from_init: &mut self.invoked_from_init,
427        };
428        builder.build(module)?;
429
430        self.component.modules.push(Arc::from(masm_module));
431
432        Ok(())
433    }
434
435    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
436        let builder = MasmFunctionBuilder::new(function)?;
437        let procedure = builder.build(
438            function,
439            self.analysis_manager.nest(function.as_operation_ref()),
440            self.link_info,
441        )?;
442
443        let module =
444            Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
445        let expected_path_len = if module.path().is_absolute() { 2 } else { 1 };
446        assert_eq!(
447            module.path().len(),
448            expected_path_len,
449            "expected top-level namespace module, but one has not been defined (in '{}' of '{}')",
450            module.path(),
451            function.path()
452        );
453        module
454            .define_procedure(procedure, self.source_manager.clone())
455            .into_diagnostic()
456            .wrap_err("failed to define MASM procedure")?;
457
458        Ok(())
459    }
460
461    /// Emit the sequence of instructions necessary to consume rodata from the advice stack and
462    /// populate the global heap with the data segments of this component, verifying that the
463    /// commitments match.
464    fn emit_data_segment_initialization(&mut self) {
465        use masm::{Instruction as Inst, InvocationTarget, Op};
466
467        // Emit data segment initialization code
468        //
469        // NOTE: This depends on the program being executed with the data for all data segments
470        // having been placed in the advice map with the same commitment and encoding used here.
471        // The program will fail to execute if this is not set up correctly.
472        let span = SourceSpan::default();
473        let pipe_preimage_to_memory = {
474            let name = masm::ProcedureName::new("pipe_preimage_to_memory").unwrap();
475            let module = masm::LibraryPath::new("::miden::core::mem").unwrap();
476            let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
477            InvocationTarget::Path(Span::new(span, qualified.into_inner()))
478        };
479        for rodata in self.component.rodata.iter() {
480            // Push the commitment hash (`COM`) for this data onto the operand stack
481
482            // WARNING: These two are equivalent, shouldn't this be a no-op?
483            let word = rodata.digest.as_elements();
484            let word_value = [word[0], word[1], word[2], word[3]];
485
486            self.init_body.push(Op::Inst(Span::new(
487                span,
488                Inst::Push(masm::Immediate::Value(Span::unknown(WordValue(word_value).into()))),
489            )));
490            // Move rodata from the advice map, using the commitment as key, to the advice stack
491            self.init_body
492                .push(Op::Inst(Span::new(span, Inst::SysEvent(masm::SystemEventNode::PushMapVal))));
493            // write_ptr
494            assert!(rodata.start.is_word_aligned(), "rodata segments must be word-aligned");
495            self.init_body.push(Op::Inst(Span::new(
496                span,
497                Inst::Push(masm::Immediate::Value(Span::unknown(rodata.start.addr.into()))),
498            )));
499            // num_words
500            self.init_body.push(Op::Inst(Span::new(
501                span,
502                Inst::Push(masm::Immediate::Value(Span::unknown(
503                    (rodata.size_in_words() as u32).into(),
504                ))),
505            )));
506            // [num_words, write_ptr, COM, ..] -> [write_ptr']
507            self.init_body.push(Op::Inst(Span::new(
508                span,
509                Inst::Trace(TraceEvent::FrameStart.as_u32().into()),
510            )));
511            self.init_body
512                .push(Op::Inst(Span::new(span, Inst::Exec(pipe_preimage_to_memory.clone()))));
513            self.init_body
514                .push(Op::Inst(Span::new(span, Inst::Trace(TraceEvent::FrameEnd.as_u32().into()))));
515            // drop write_ptr'
516            self.init_body.push(Op::Inst(Span::new(span, Inst::Drop)));
517        }
518    }
519}
520
521struct MasmModuleBuilder<'a> {
522    module: &'a mut masm::Module,
523    analysis_manager: AnalysisManager,
524    link_info: &'a LinkInfo,
525    source_manager: Arc<dyn midenc_session::SourceManager + Send + Sync>,
526    init_body: &'a mut Vec<masm::Op>,
527    invoked_from_init: &'a mut BTreeSet<masm::Invoke>,
528}
529
530impl MasmModuleBuilder<'_> {
531    pub fn build(mut self, module: &builtin::Module) -> Result<(), Report> {
532        let region = module.body();
533        let block = region.entry();
534        for op in block.body() {
535            if let Some(function) = op.downcast_ref::<builtin::Function>() {
536                self.define_function(function)?;
537            } else if let Some(gv) = op.downcast_ref::<builtin::GlobalVariable>() {
538                self.emit_global_variable_initializer(gv)?;
539            } else if op.is::<builtin::Segment>() {
540                continue;
541            } else {
542                panic!(
543                    "invalid module-level operation: '{}' is not legal in a MASM module body",
544                    op.name()
545                )
546            }
547        }
548
549        Ok(())
550    }
551
552    pub fn build_from_interface(mut self, interface: &builtin::Interface) -> Result<(), Report> {
553        let region = interface.body();
554        let block = region.entry();
555        for op in block.body() {
556            if let Some(function) = op.downcast_ref::<builtin::Function>() {
557                self.define_function(function)?;
558            } else {
559                panic!(
560                    "invalid interface-level operation: '{}' is not legal in a MASM module body",
561                    op.name()
562                )
563            }
564        }
565
566        Ok(())
567    }
568
569    fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
570        let builder = MasmFunctionBuilder::new(function)?;
571
572        let procedure = builder.build(
573            function,
574            self.analysis_manager.nest(function.as_operation_ref()),
575            self.link_info,
576        )?;
577
578        self.module
579            .define_procedure(procedure, self.source_manager.clone())
580            .map_err(|e| Report::msg(e.to_string()))?;
581
582        Ok(())
583    }
584
585    fn emit_global_variable_initializer(
586        &mut self,
587        gv: &builtin::GlobalVariable,
588    ) -> Result<(), Report> {
589        // We don't emit anything for declarations
590        if gv.is_declaration() {
591            return Ok(());
592        }
593
594        // We compute liveness for global variables independently
595        let analysis_manager = self.analysis_manager.nest(gv.as_operation_ref());
596        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
597
598        // Emit the initializer block
599        let initializer_region = gv.region(0);
600        let initializer_block = initializer_region.entry();
601
602        let mut block_emitter = BlockEmitter {
603            liveness: &liveness,
604            link_info: self.link_info,
605            invoked: self.invoked_from_init,
606            target: Default::default(),
607            stack: OperandStack::new(gv.as_operation().context_rc()),
608            trace_target: TraceTarget::category("codegen")
609                .with_relevant_symbol(gv.name().as_symbol()),
610        };
611        block_emitter.emit_inline(&initializer_block);
612
613        // Sanity checks
614        assert_eq!(block_emitter.stack.len(), 1, "expected only global variable value on stack");
615        let return_ty = block_emitter.stack.peek().unwrap().ty();
616        assert_eq!(
617            &return_ty,
618            &*gv.get_ty(),
619            "expected initializer to return value of same type as declaration"
620        );
621
622        // Write the initialized value to the computed storage offset for this global
623        let computed_addr = self
624            .link_info
625            .globals_layout()
626            .get_computed_addr(gv.as_global_var_ref())
627            .expect("undefined global variable");
628        block_emitter.emitter().store_imm(computed_addr, gv.span());
629
630        // Extend the generated init function with the code to initialize this global
631        let mut body = core::mem::take(&mut block_emitter.target);
632        self.init_body.append(&mut body);
633
634        Ok(())
635    }
636}
637
638struct MasmFunctionBuilder {
639    span: midenc_hir::SourceSpan,
640    name: masm::ProcedureName,
641    signature: masm::FunctionType,
642    visibility: masm::Visibility,
643    num_locals: u16,
644}
645
646impl MasmFunctionBuilder {
647    pub fn new(function: &builtin::Function) -> Result<Self, Report> {
648        use midenc_hir::{Symbol, Visibility};
649
650        let name = *function.get_name();
651        let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
652            name.span,
653            name.as_ref().into(),
654        )));
655        let visibility = match function.visibility() {
656            Visibility::Public => masm::Visibility::Public,
657            // TODO(pauls): Support internal visibility in MASM
658            Visibility::Internal => masm::Visibility::Public,
659            Visibility::Private => masm::Visibility::Private,
660        };
661        let locals_required = function.locals().iter().map(|ty| ty.size_in_felts()).sum::<usize>();
662        let num_locals = u16::try_from(locals_required).map_err(|_| {
663            let context = function.as_operation().context();
664            context
665                .diagnostics()
666                .diagnostic(miden_assembly::diagnostics::Severity::Error)
667                .with_message("cannot emit masm for function")
668                .with_primary_label(
669                    function.span(),
670                    "local storage exceeds procedure limit: no more than u16::MAX elements are \
671                     supported",
672                )
673                .into_report()
674        })?;
675
676        let signature =
677            semantic_debug_signature(function).unwrap_or_else(|| lowered_signature(function));
678
679        Ok(Self {
680            span: function.span(),
681            name,
682            signature,
683            visibility,
684            num_locals,
685        })
686    }
687
688    pub fn build(
689        self,
690        function: &builtin::Function,
691        analysis_manager: AnalysisManager,
692        link_info: &LinkInfo,
693    ) -> Result<masm::Procedure, Report> {
694        use alloc::collections::BTreeSet;
695
696        use midenc_hir_analysis::analyses::LivenessAnalysis;
697
698        let demangled_symbol_name = midenc_hir::demangle::demangle(function.get_name().as_str());
699        let trace_target = TraceTarget::category("codegen")
700            .with_relevant_symbol(midenc_hir::SymbolName::intern(demangled_symbol_name));
701
702        log::trace!(target: &trace_target, "lowering {}", function.as_operation());
703
704        let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
705
706        let mut invoked = BTreeSet::default();
707        let entry = function.entry_block();
708        let mut stack = crate::OperandStack::new(function.as_operation().context_rc());
709        {
710            let entry_block = entry.borrow();
711            for arg in entry_block.arguments().iter().rev().copied() {
712                stack.push(arg as ValueRef);
713            }
714        }
715        let mut emitter = BlockEmitter {
716            liveness: &liveness,
717            link_info,
718            invoked: &mut invoked,
719            target: Default::default(),
720            stack,
721            trace_target,
722        };
723
724        // For component export functions, invoke the `init` procedure first if needed.
725        // It loads the data segments and global vars into memory.
726        if function.signature().cc.is_wasm_canonical_abi()
727            && (link_info.has_globals() || link_info.has_data_segments())
728        {
729            // Resolve `init` symbolically within the containing module instead of through a
730            // fully-qualified component path, which depends on the (user-editable)
731            // `[lib].namespace` matching the component's library identity.
732            //
733            // INVARIANT: this relies on the canonical-ABI export wrappers being emitted into the
734            // root component module — the same module where `MasmComponentBuilder` defines
735            // `init` (`self.component.modules[0]`); the inner lifted functions in interface and
736            // core child modules carry no init prologue. If export wrappers ever move into child
737            // modules, this symbol stops resolving and the init target must be threaded in as a
738            // qualified path instead. A user-exported method named `init` collides with the
739            // generated procedure at definition time ("symbol conflict: found duplicate
740            // definitions"), so it cannot silently shadow this target.
741            let init = InvocationTarget::Symbol("init".parse().unwrap());
742            // Add init call to the emitter's target before emitting the function body; `emit`
743            // also registers the invocation so the assembler can resolve the symbolic target.
744            emitter.emitter().emit(masm::Instruction::Exec(init), SourceSpan::default());
745        }
746
747        let mut body = emitter.emit(&entry.borrow());
748
749        if function.signature().cc.is_wasm_canonical_abi() {
750            // Truncate the stack to 16 elements on exit in the component export function
751            // since it is expected to be `call`ed so it has a requirement to have
752            // no more than 16 elements on the stack when it returns.
753            // See https://0xmiden.github.io/miden-vm/user_docs/assembly/execution_contexts.html
754            // Since the VM's `drop` instruction not letting stack size go beyond the 16 elements
755            // we most likely end up with stack size > 16 elements at the end.
756            // See https://github.com/0xPolygonMiden/miden-vm/blob/c4acf49510fda9ba80f20cee1a9fb1727f410f47/processor/src/stack/mod.rs?plain=1#L226-L253
757            let truncate_stack = {
758                let name = masm::ProcedureName::new("truncate_stack").unwrap();
759                let module = masm::LibraryPath::new("::miden::core::sys").unwrap();
760                let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
761                InvocationTarget::Path(Span::new(SourceSpan::default(), qualified.into_inner()))
762            };
763            let span = SourceSpan::default();
764            invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
765            body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(truncate_stack))));
766        }
767        let Self {
768            span,
769            name,
770            signature,
771            visibility,
772            num_locals,
773        } = self;
774
775        // Align num_locals to WORD_SIZE, matching the assembler's FMP frame sizing.
776        // num_locals already counts all HIR locals (including those allocated for params).
777        // The assembler rounds up to next_multiple_of(WORD_SIZE) when advancing FMP
778        // (see fmp.rs fmp_start_frame_sequence and mem_ops.rs locaddr), so we must use
779        // the same alignment for debug var offset computation.
780        let aligned_num_locals = num_locals.next_multiple_of(miden_core::WORD_SIZE as u16);
781
782        // Resolve FrameBase global_index → Miden memory address.
783        // Use the stack pointer offset from the linker's global layout.
784        let stack_pointer_addr = link_info.globals_layout().stack_pointer_offset();
785
786        // Patch DebugVar Local locations to compute FMP offset.
787        // During lowering, Local(idx) stores the raw WASM local index.
788        // Now convert to FMP offset: idx - aligned_num_locals
789        // This matches locaddr.N which computes -(aligned_num_locals - N).
790        patch_debug_var_locals_in_block(&mut body, aligned_num_locals, stack_pointer_addr);
791
792        // If a function body after lowering produces a MASM procedure with an empty body aside
793        // from debug decorators, then we must emit a `nop` at the end of the block which will
794        // act as the anchor for those decorators. Such a procedure is basically useless, as it is
795        // just passing through arguments as results - but the assembler currently rejects empty
796        // procedures (not counting decorators), so we must handle this edge case.
797        if !block_has_real_instructions(&body) {
798            body.push(masm::Op::Inst(Span::unknown(masm::Instruction::Nop)));
799        }
800
801        let mut procedure = masm::Procedure::new(span, visibility, name, num_locals, body);
802        procedure.set_signature(signature);
803        for attribute in ["auth_script", "note_script"] {
804            if function.has_attribute(attribute) {
805                procedure
806                    .attributes_mut()
807                    .insert(Attribute::Marker(masm::Ident::new(attribute).unwrap()));
808            }
809        }
810        procedure.extend_invoked(invoked);
811
812        Ok(procedure)
813    }
814}
815
816fn lowered_signature(function: &builtin::Function) -> masm::FunctionType {
817    let sig = function.signature();
818    let args = sig.params.iter().map(|param| masm::TypeExpr::from(param.ty.clone())).collect();
819    let results = sig
820        .results
821        .iter()
822        .map(|result| masm::TypeExpr::from(result.ty.clone()))
823        .collect();
824    masm::FunctionType::new(sig.cc, args, results)
825}
826
827fn semantic_debug_signature(function: &builtin::Function) -> Option<masm::FunctionType> {
828    let subprogram = function
829        .as_operation()
830        .get_attribute("di.subprogram")?
831        .try_downcast_attr::<SubprogramAttr>()
832        .ok()?;
833    let subprogram = subprogram.borrow();
834    let Type::Function(ty) = subprogram.ty.as_ref()? else {
835        return None;
836    };
837
838    let args = ty.params().iter().map(component_abi_type_expr_from_hir).collect();
839    let results = ty.results().iter().map(component_abi_type_expr_from_hir).collect();
840    Some(masm::FunctionType::new(ty.calling_convention(), args, results))
841}
842
843/// Convert HIR types from a Component Model/WIT signature into MASM syntax types.
844///
845/// This intentionally differs from `From<Type> for TypeExpr`, which describes the lowered MASM
846/// representation and expands wide integer primitives like `u64`/`u128` into 32-bit limb arrays.
847/// Component export metadata should preserve the Component ABI shape instead, including nominal
848/// struct and field names used by debuggers and typed clients.
849///
850/// TODO(pauls): Remove once miden-vm#XXXX is merged and ships in the next stable release,
851/// expected to be v0.24.
852fn component_abi_type_expr_from_hir(ty: &Type) -> masm::TypeExpr {
853    match ty {
854        Type::Array(array) => masm::TypeExpr::Array(masm::ArrayType::new(
855            component_abi_type_expr_from_hir(array.element_type()),
856            array.len(),
857        )),
858        Type::Struct(struct_ty) => {
859            let name = struct_ty.name().and_then(|name| masm::Ident::new(name.as_ref()).ok());
860            let fields = struct_ty.fields().iter().enumerate().map(|(index, field)| {
861                let name = field
862                    .name
863                    .as_deref()
864                    .map(masm::Ident::new)
865                    .and_then(Result::ok)
866                    .unwrap_or_else(|| masm::Ident::new(format!("field{index}")).unwrap());
867                masm::StructField {
868                    span: SourceSpan::UNKNOWN,
869                    name,
870                    ty: component_abi_type_expr_from_hir(&field.ty),
871                }
872            });
873            masm::TypeExpr::Struct(
874                masm::StructType::new(name, fields)
875                    .with_repr(Span::unknown(struct_ty.repr()))
876                    .with_span(SourceSpan::UNKNOWN),
877            )
878        }
879        Type::Ptr(ptr) => masm::TypeExpr::Ptr(
880            masm::PointerType::new(component_abi_type_expr_from_hir(ptr.pointee()))
881                .with_address_space(ptr.addrspace()),
882        ),
883        Type::Function(_) => masm::TypeExpr::Ptr(masm::PointerType::new(
884            masm::TypeExpr::Primitive(Span::unknown(Type::Felt)),
885        )),
886        Type::List(element_ty) => masm::TypeExpr::Ptr(
887            masm::PointerType::new(component_abi_type_expr_from_hir(element_ty))
888                .with_address_space(masm::types::AddressSpace::Byte),
889        ),
890        Type::Unknown | Type::Never | Type::F64 => panic!("unrepresentable type value: {ty}"),
891        ty => masm::TypeExpr::Primitive(Span::unknown(ty.clone())),
892    }
893}
894
895/// Returns true if the block contains at least one real (non-decorator) instruction.
896///
897/// DebugVar instructions are decorator-only and don't produce MAST nodes. If a procedure
898/// body contains only DebugVar ops, the assembler will reject it.
899fn block_has_real_instructions(block: &masm::Block) -> bool {
900    block.iter().any(|op| match op {
901        masm::Op::Inst(inst) => !matches!(
902            inst.inner(),
903            masm::Instruction::Debug(_)
904                | masm::Instruction::DebugVar(_)
905                | masm::Instruction::Trace(_)
906        ),
907        masm::Op::If {
908            then_blk, else_blk, ..
909        } => block_has_real_instructions(then_blk) || block_has_real_instructions(else_blk),
910        masm::Op::While { body, .. } => block_has_real_instructions(body),
911        masm::Op::Repeat { body, .. } => block_has_real_instructions(body),
912    })
913}
914
915/// Recursively patch DebugVar locations in a block.
916///
917/// Converts `Local(idx)` where idx is the raw WASM local index to `Local(offset)` where
918/// `offset = idx - aligned_num_locals` (the FMP-relative offset, typically negative). This matches
919/// the assembler's `locaddr.N` formula, i.e. `FMP - aligned_num_locals + N`.
920///
921/// Also resolves `FrameBase { global_index, byte_offset }` by replacing the WASM global index with
922/// the resolved Miden memory address of the stack pointer.
923fn patch_debug_var_locals_in_block(
924    block: &mut masm::Block,
925    aligned_num_locals: u16,
926    stack_pointer_addr: Option<u32>,
927) {
928    for op in block.iter_mut() {
929        match op {
930            masm::Op::Inst(span_inst) => {
931                // Use DerefMut to get mutable access to the inner Instruction
932                if let masm::Instruction::DebugVar(info) = &mut **span_inst {
933                    if let DebugVarLocation::Local(idx) = info.value_location() {
934                        // Convert raw WASM local index to FMP offset
935                        let fmp_offset = *idx - (aligned_num_locals as i16);
936                        info.set_value_location(DebugVarLocation::Local(fmp_offset));
937                    } else if let DebugVarLocation::FrameBase {
938                        global_index,
939                        byte_offset,
940                    } = info.value_location()
941                    {
942                        let byte_offset = *byte_offset;
943                        if let Some(local_index) = decode_frame_base_local_index(*global_index) {
944                            if let Ok(local_index) = i16::try_from(local_index) {
945                                let local_offset = local_index - (aligned_num_locals as i16);
946                                info.set_value_location(DebugVarLocation::FrameBase {
947                                    global_index: encode_frame_base_local_offset(local_offset),
948                                    byte_offset,
949                                });
950                            }
951                        } else {
952                            // Resolve FrameBase: replace WASM global index with
953                            // the Miden memory address of the stack pointer global.
954                            if let Some(resolved_addr) = stack_pointer_addr {
955                                info.set_value_location(DebugVarLocation::FrameBase {
956                                    global_index: resolved_addr,
957                                    byte_offset,
958                                });
959                            }
960                        }
961                    }
962                }
963            }
964            masm::Op::If {
965                then_blk, else_blk, ..
966            } => {
967                patch_debug_var_locals_in_block(then_blk, aligned_num_locals, stack_pointer_addr);
968                patch_debug_var_locals_in_block(else_blk, aligned_num_locals, stack_pointer_addr);
969            }
970            masm::Op::While {
971                body: while_body, ..
972            } => {
973                patch_debug_var_locals_in_block(while_body, aligned_num_locals, stack_pointer_addr);
974            }
975            masm::Op::Repeat {
976                body: repeat_body, ..
977            } => {
978                patch_debug_var_locals_in_block(
979                    repeat_body,
980                    aligned_num_locals,
981                    stack_pointer_addr,
982                );
983            }
984        }
985    }
986}
987
988#[cfg(test)]
989mod tests {
990    use alloc::sync::Arc;
991
992    use midenc_hir::{PointerType, StructType, TypeRepr};
993
994    use super::*;
995
996    #[test]
997    fn type_expr_from_hir_pointer_conversion_preserves_address_space() {
998        for addrspace in [masm::types::AddressSpace::Byte, masm::types::AddressSpace::Element] {
999            let ty = Type::from(PointerType::new_with_address_space(Type::U32, addrspace));
1000
1001            let masm::TypeExpr::Ptr(ptr) = component_abi_type_expr_from_hir(&ty) else {
1002                panic!("expected pointer type expression");
1003            };
1004            assert_eq!(ptr.address_space(), addrspace);
1005
1006            let masm::TypeExpr::Ptr(ptr) = masm::TypeExpr::from(ty) else {
1007                panic!("expected pointer type expression");
1008            };
1009            assert_eq!(ptr.address_space(), addrspace);
1010        }
1011    }
1012
1013    #[test]
1014    fn component_abi_type_conversion_preserves_wide_primitives() {
1015        let masm::TypeExpr::Primitive(ty) = component_abi_type_expr_from_hir(&Type::U64) else {
1016            panic!("expected primitive component ABI type");
1017        };
1018        assert_eq!(ty.inner(), &Type::U64);
1019
1020        let masm::TypeExpr::Array(ty) = masm::TypeExpr::from(Type::U64) else {
1021            panic!("expected lowered MASM type");
1022        };
1023        assert_eq!(ty.arity, 2);
1024        let masm::TypeExpr::Primitive(element_ty) = ty.elem.as_ref() else {
1025            panic!("expected primitive array element type");
1026        };
1027        assert_eq!(element_ty.inner(), &Type::U32);
1028    }
1029
1030    #[test]
1031    fn component_abi_type_conversion_preserves_nominal_struct_metadata() {
1032        let ty = Type::Struct(Arc::new(StructType::from_parts(
1033            Some(Arc::from("miden:base/core-types@1.0.0/account-id")),
1034            TypeRepr::Default,
1035            [
1036                (Arc::<str>::from("prefix"), Type::Felt),
1037                (Arc::<str>::from("suffix"), Type::Felt),
1038            ],
1039        )));
1040
1041        let masm::TypeExpr::Struct(struct_ty) = component_abi_type_expr_from_hir(&ty) else {
1042            panic!("expected struct component ABI type");
1043        };
1044        assert_eq!(
1045            struct_ty.name.as_ref().map(|name| name.as_str()),
1046            Some("miden:base/core-types@1.0.0/account-id"),
1047        );
1048        assert_eq!(struct_ty.fields[0].name.as_str(), "prefix");
1049        assert_eq!(struct_ty.fields[1].name.as_str(), "suffix");
1050
1051        let masm::TypeExpr::Struct(struct_ty) = masm::TypeExpr::from(ty) else {
1052            panic!("expected lowered struct type");
1053        };
1054        assert!(struct_ty.name.is_none());
1055        assert_eq!(struct_ty.fields[0].name.as_str(), "field0");
1056        assert_eq!(struct_ty.fields[1].name.as_str(), "field1");
1057    }
1058}