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}