#[cfg(test)]
mod tests;
use alloc::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
vec::Vec,
};
use miden_assembly::{PathBuf as LibraryPath, ast::InvocationTarget};
use miden_assembly_syntax::{
ast::{Attribute, DebugVarLocation},
parser::WordValue,
};
use miden_core::serde::Deserializable;
use midenc_hir::{
FunctionIdent, Op, OpExt, SourceSpan, Span, Symbol, TraceTarget, Type, ValueRef,
diagnostics::IntoDiagnostic,
dialects::{
builtin,
debuginfo::attributes::{Expression, ExpressionOp, FrameBase, SubprogramAttr},
},
interner,
pass::AnalysisManager,
};
use midenc_hir_analysis::analyses::LivenessAnalysis;
use midenc_session::diagnostics::{Report, Spanned, WrapErr};
use smallvec::SmallVec;
use crate::{
Event, OperandStack,
artifact::MasmComponent,
emitter::BlockEmitter,
linker::{FunctionTableLayout, LinkInfo, Linker},
masm,
};
const FUNCTION_TABLE_INIT_PROC: &str = "__init_function_table";
const EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC: &str = "__midenc_entrypoint_without_init";
pub trait ToMasmComponent {
fn to_masm_component(&self, analysis_manager: AnalysisManager)
-> Result<MasmComponent, Report>;
}
impl ToMasmComponent for builtin::World {
fn to_masm_component(
&self,
analysis_manager: AnalysisManager,
) -> Result<MasmComponent, Report> {
let mut components = Vec::new();
let mut siblings = Vec::new();
for op in self.body().entry().body().iter() {
match op.as_operation_ref().try_downcast_op::<builtin::Component>() {
Ok(component) => components.push(component),
Err(op) => siblings.push(op),
}
}
match components.len() {
0 => world_body_to_masm_component(self, analysis_manager),
1 => {
let (supporting, owns_memory) = classify_siblings(&siblings);
let lowered = component_to_masm_component(
&components[0].borrow(),
analysis_manager,
&supporting,
)?;
report_siblings_that_own_memory(self, &owns_memory);
Ok(lowered)
}
_ => Err(too_many_components(self, &components)),
}
}
}
fn is_declaration_only(op: &midenc_hir::OperationRef) -> bool {
fn body_is_all_declarations(region: &midenc_hir::Region) -> bool {
region.entry().body().iter().all(|item| {
if let Some(function) = item.downcast_ref::<builtin::Function>() {
function.is_declaration()
} else if let Some(gv) = item.downcast_ref::<builtin::GlobalVariable>() {
gv.is_declaration()
} else {
false
}
})
}
if let Ok(module) = op.try_downcast_op::<builtin::Module>() {
let module = module.borrow();
body_is_all_declarations(&module.body())
} else if let Ok(interface) = op.try_downcast_op::<builtin::Interface>() {
let interface = interface.borrow();
body_is_all_declarations(&interface.body())
} else if let Ok(function) = op.try_downcast_op::<builtin::Function>() {
let function = function.borrow();
function.is_declaration()
} else {
false
}
}
fn module_owns_memory(module: &builtin::Module) -> bool {
module.body().entry().body().iter().any(|item| {
if item.is::<builtin::GlobalVariable>() || item.is::<builtin::Segment>() {
return true;
}
!item.is::<builtin::Function>()
})
}
fn classify_siblings(
siblings: &[midenc_hir::OperationRef],
) -> (SmallVec<[builtin::ModuleRef; 4]>, SmallVec<[midenc_hir::OperationRef; 4]>) {
let mut supporting = SmallVec::<[builtin::ModuleRef; 4]>::new();
let mut owns_memory = SmallVec::<[midenc_hir::OperationRef; 4]>::new();
for op in siblings.iter().copied() {
if is_declaration_only(&op) {
continue;
}
match op.try_downcast_op::<builtin::Module>() {
Ok(module) if !module_owns_memory(&module.borrow()) => supporting.push(module),
_ => owns_memory.push(op),
}
}
(supporting, owns_memory)
}
fn report_siblings_that_own_memory(world: &builtin::World, siblings: &[midenc_hir::OperationRef]) {
if siblings.is_empty() {
return;
}
let mut diagnostic = world
.as_operation()
.context()
.diagnostics()
.diagnostic(miden_assembly::diagnostics::Severity::Warning)
.with_message(
"a top-level module beside a component cannot own global variables or data segments",
);
for (index, op) in siblings.iter().enumerate() {
let op = op.borrow();
let label = format!("this '{}' is omitted from the generated package", op.name());
diagnostic = if index == 0 {
diagnostic.with_primary_label(op.span(), label)
} else {
diagnostic.with_secondary_label(op.span(), label)
};
}
diagnostic
.with_help(
"global variables and data segments belong to a component: the component is what lays \
out memory for them and emits the code that initializes it. A module declared at the \
top level of a world has no parent component to own them, so this build omits the \
module, and code that calls into it will fail to resolve. A top-level module that \
declares neither is a supporting module, and is translated 1:1 to a Miden Assembly \
module and linked into the final assembly as an ad-hoc module. Top-level items that \
only declare symbols — external dependencies represented in the IR — contribute no \
Miden Assembly and are ignored by design; they are not reported here. Any other \
top-level item is reported here as well, rather than translated on a guess.",
)
.emit();
}
fn too_many_components(world: &builtin::World, components: &[builtin::ComponentRef]) -> Report {
let mut diagnostic = world
.as_operation()
.context()
.diagnostics()
.diagnostic(miden_assembly::diagnostics::Severity::Error)
.with_message(format!(
"lowering a world containing {} components is not yet implemented",
components.len()
))
.with_primary_label(world.span(), "in this world");
for component in components {
let component = component.borrow();
diagnostic = diagnostic.with_secondary_label(component.span(), "this component");
}
diagnostic
.with_help(
"this is a known limitation of the compiler rather than a problem with this input: a \
Miden package's metadata can currently describe only one component, so a build emits \
one component per package. Support for multiple components in a package is being \
worked on; until it lands, compile each component separately.",
)
.into_report()
}
fn function_without_a_body(function: &builtin::Function) -> Report {
function
.as_operation()
.context()
.diagnostics()
.diagnostic(miden_assembly::diagnostics::Severity::Error)
.with_message(
"cannot emit masm for a function with no body: nothing can provide its definition",
)
.with_primary_label(function.span(), "this function is declared but never defined")
.with_help(
"a declaration names a procedure whose implementation comes from elsewhere, and Miden \
Assembly has no later step that could supply one. Either give this function a body, \
or remove it along with whatever refers to it.",
)
.into_report()
}
fn world_body_to_masm_component(
world: &builtin::World,
analysis_manager: AnalysisManager,
) -> Result<MasmComponent, Report> {
let context = world.as_operation().context_rc();
let link_info = Linker::default().link(None, world.as_operation()).map_err(Report::msg)?;
let entrypoint = match context.session().options.entrypoint.as_deref() {
Some(entry) => {
let entry_id = entry
.parse::<FunctionIdent>()
.map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
entry_id.function.span,
entry_id.function.as_str().into(),
)));
let path = LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?;
let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
Some(masm::InvocationTarget::Path(Span::new(
entry_id.function.span,
qualified.into_inner(),
)))
}
None => None,
};
let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
world.as_operation_ref(),
&[],
&link_info,
entrypoint.as_ref(),
)?;
let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
executable_entrypoint,
&analysis_manager,
&link_info,
)?;
let requires_init = link_info.requires_init();
let toplevel_namespaces = world
.body()
.entry()
.body()
.iter()
.filter_map(|op| {
if op.is::<builtin::Module>() {
Some(op.as_operation_ref())
} else {
None
}
})
.collect::<Vec<_>>();
let init = if requires_init {
let name = masm::ProcedureName::new("init").unwrap();
let qualified = match toplevel_namespaces.len() {
1 => {
let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
masm::QualifiedProcedureName::new(format!("::{namespace}").as_str(), name)
}
_ => masm::QualifiedProcedureName::new("::init", name),
};
Some(masm::InvocationTarget::Path(Span::new(
SourceSpan::default(),
qualified.into_inner(),
)))
} else {
None
};
let root = match toplevel_namespaces.len() {
1 => {
let namespace = toplevel_namespaces[0].borrow().symbol_name_if_symbol().unwrap();
Arc::from(
masm::PathBuf::new(&format!("::{namespace}"))
.expect("invalid namespace")
.into_boxed_path(),
)
}
_ => Arc::<masm::Path>::from(masm::Path::new("::init")),
};
let init_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
let modules = vec![init_module];
let rodata = data_segments_to_rodata(&link_info)?;
let heap_base = link_info.heap_base();
let stack_pointer = link_info.globals_layout().stack_pointer_offset();
let mut masm_component = MasmComponent {
id: None,
synthetic_wrapper: false,
root,
init,
entrypoint,
executable_entrypoint_without_init,
rodata,
heap_base,
stack_pointer,
modules,
};
let builder = MasmComponentBuilder {
analysis_manager,
component: &mut masm_component,
link_info: &link_info,
source_manager: context.session().source_manager.clone(),
init_body: Default::default(),
invoked_from_init: Default::default(),
};
builder.build(world.as_operation(), &[])?;
Ok(masm_component)
}
impl ToMasmComponent for builtin::Component {
fn to_masm_component(
&self,
analysis_manager: AnalysisManager,
) -> Result<MasmComponent, Report> {
component_to_masm_component(self, analysis_manager, &[])
}
}
fn component_to_masm_component(
component: &builtin::Component,
analysis_manager: AnalysisManager,
supporting: &[builtin::ModuleRef],
) -> Result<MasmComponent, Report> {
let context = component.as_operation().context_rc();
let synthetic_wrapper = component.is_synthetic_wrapper();
let id = component.id();
let link_info = Linker::default()
.link(Some(id.clone()), component.as_operation())
.map_err(Report::msg)?;
let component_path = id
.to_library_path()
.to_absolute()
.map_err(|err| {
Report::msg(format!("unable to canonicalize '{}': {err}", id.to_library_path()))
})?
.into_owned();
let entrypoint = match context.session().options.entrypoint.as_deref() {
Some(entry) => {
let entry_id = entry
.parse::<FunctionIdent>()
.map_err(|_| Report::msg(format!("invalid entrypoint identifier: '{entry}'")))?;
let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
entry_id.function.span,
entry_id.function.as_str().into(),
)));
let path = if synthetic_wrapper {
component_path.join(entry_id.module.as_str())
} else {
LibraryPath::new(entry_id.module.as_str()).into_diagnostic()?
};
let qualified = masm::QualifiedProcedureName::new(path.as_path(), name);
Some(masm::InvocationTarget::Path(Span::new(
entry_id.function.span,
qualified.into_inner(),
)))
}
None => None,
};
let executable_entrypoint = classify_marked_canonical_abi_entrypoint(
component.as_operation_ref(),
supporting,
&link_info,
entrypoint.as_ref(),
)?;
let executable_entrypoint_without_init = lower_executable_entrypoint_without_init(
executable_entrypoint,
&analysis_manager,
&link_info,
)?;
let requires_init = link_info.requires_init();
let init = if requires_init {
let name = masm::ProcedureName::new("init").unwrap();
let qualified = masm::QualifiedProcedureName::new(&component_path, name);
Some(masm::InvocationTarget::Path(Span::new(
SourceSpan::default(),
qualified.into_inner(),
)))
} else {
None
};
let root: Arc<miden_assembly_syntax::Path> = component_path.into_boxed_path().into();
let root_module = Arc::new(masm::Module::new(masm::ModuleKind::Library, &root));
let modules = vec![root_module];
let rodata = data_segments_to_rodata(&link_info)?;
let heap_base = link_info.heap_base();
let stack_pointer = link_info.globals_layout().stack_pointer_offset();
let mut masm_component = MasmComponent {
id: Some(id),
synthetic_wrapper,
root,
init,
entrypoint,
executable_entrypoint_without_init,
rodata,
heap_base,
stack_pointer,
modules,
};
let builder = MasmComponentBuilder {
analysis_manager,
component: &mut masm_component,
link_info: &link_info,
source_manager: context.session().source_manager.clone(),
init_body: Default::default(),
invoked_from_init: Default::default(),
};
builder.build(component.as_operation(), supporting)?;
Ok(masm_component)
}
fn data_segments_to_rodata(link_info: &LinkInfo) -> Result<Vec<crate::Rodata>, Report> {
use midenc_hir::constants::ConstantData;
use crate::data_segments::{ResolvedDataSegment, merge_data_segments};
let mut resolved = SmallVec::<[ResolvedDataSegment; 2]>::new();
for sref in link_info.segment_layout().iter() {
let s = sref.borrow();
resolved.push(ResolvedDataSegment {
offset: *s.get_offset(),
data: s.initializer().as_slice().to_vec(),
readonly: *s.get_readonly(),
});
}
Ok(match merge_data_segments(resolved).map_err(Report::msg)? {
None => alloc::vec::Vec::new(),
Some(merged) => {
let data = alloc::sync::Arc::new(ConstantData::from(merged.data));
let felts = crate::Rodata::bytes_to_elements(data.as_slice());
let digest = miden_core::crypto::hash::Poseidon2::hash_elements(&felts);
alloc::vec![crate::Rodata {
component: link_info.component().cloned().unwrap_or(builtin::ComponentId {
namespace: interner::Symbol::intern("root_ns"),
name: interner::Symbol::intern("root"),
version: midenc_hir::version::Version::new(1, 0, 0)
}),
digest,
start: super::NativePtr::from_ptr(merged.offset),
data,
}]
}
})
}
fn classify_marked_canonical_abi_entrypoint(
component: midenc_hir::OperationRef,
supporting: &[builtin::ModuleRef],
link_info: &LinkInfo,
entrypoint: Option<&masm::InvocationTarget>,
) -> Result<Option<builtin::FunctionRef>, Report> {
let (Some(_), Some(entrypoint)) = (link_info.component_start(), entrypoint) else {
return Ok(None);
};
let entrypoint_path = entrypoint
.unwrap_path()
.to_absolute()
.map_err(|err| Report::msg(format!("invalid executable entrypoint path: {err}")))?;
let find_canonical_entrypoint = |root: midenc_hir::OperationRef| {
let mut canonical_entrypoint = None;
root.borrow().prewalk_all(|op| {
let Some(function) = op.downcast_ref::<builtin::Function>() else {
return;
};
if !function.signature().cc.is_wasm_canonical_abi() {
return;
}
let function_target = super::lowering::invocation_target_from_symbol_path(
&function.path(),
function.span(),
);
if function_target.unwrap_path() == entrypoint_path.as_ref() {
canonical_entrypoint = Some(function.as_function_ref());
}
});
canonical_entrypoint
};
if let Some(function) = find_canonical_entrypoint(component) {
if function.borrow().as_operation().parent_op() != Some(component) {
let path = function.borrow().path();
return Err(Report::msg(format!(
"unsupported executable entrypoint '{path}': a canonical-ABI entrypoint for a \
component with a core Wasm start function must be defined directly in the \
selected component"
)));
}
return Ok(Some(function));
}
let supporting_entrypoint = supporting
.iter()
.find_map(|module| find_canonical_entrypoint(module.borrow().as_operation_ref()));
if let Some(function) = supporting_entrypoint {
let path = function.borrow().path();
return Err(Report::msg(format!(
"unsupported executable entrypoint '{path}': a canonical-ABI entrypoint cannot be \
selected for a component with a core Wasm start function because it would execute \
component initialization twice in the same context"
)));
}
Ok(None)
}
fn lower_executable_entrypoint_without_init(
function: Option<builtin::FunctionRef>,
analysis_manager: &AnalysisManager,
link_info: &LinkInfo,
) -> Result<Option<masm::Procedure>, Report> {
let Some(function) = function else {
return Ok(None);
};
let function = function.borrow();
let mut builder = MasmFunctionBuilder::new(&function)?;
builder.name = masm::ProcedureName::new(EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC).unwrap();
builder.visibility = masm::Visibility::Private;
builder
.build(
&function,
analysis_manager.nest(function.as_operation_ref()),
link_info,
FunctionLoweringMode::ExecutableEntrypointWithoutInit,
)
.map(Some)
}
struct MasmComponentBuilder<'a> {
component: &'a mut MasmComponent,
analysis_manager: AnalysisManager,
link_info: &'a LinkInfo,
source_manager: Arc<dyn midenc_session::SourceManager>,
init_body: Vec<masm::Op>,
invoked_from_init: BTreeSet<masm::Invoke>,
}
impl MasmComponentBuilder<'_> {
pub fn build(
mut self,
component: &midenc_hir::Operation,
supporting: &[builtin::ModuleRef],
) -> Result<(), Report> {
use masm::{Instruction as Inst, InvocationTarget, Op};
crate::legalization::validate_procedure_roots(component)?;
for module in supporting {
crate::legalization::validate_procedure_roots(module.borrow().as_operation())?;
}
if self.component.init.is_some() {
let span = component.span();
let heap_base = self.component.heap_base;
self.init_body.push(masm::Op::Inst(Span::new(
span,
Inst::Push(masm::Immediate::Value(Span::unknown(heap_base.into()))),
)));
let heap_init = {
let name = masm::ProcedureName::new("heap_init").unwrap();
let module = masm::LibraryPath::new("::intrinsics::mem").unwrap();
let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
InvocationTarget::Path(Span::new(span, qualified.into_inner()))
};
self.init_body.push(Op::Inst(Span::new(
span,
Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
)));
self.init_body.push(Op::Inst(Span::new(span, Inst::Exec(heap_init))));
self.init_body.push(Op::Inst(Span::new(
span,
Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
)));
self.emit_data_segment_initialization();
}
let region = component.region(0);
let block = region.entry();
for op in block.body() {
if let Some(module) = op.downcast_ref::<builtin::Module>() {
self.define_module(module)?;
} else if let Some(interface) = op.downcast_ref::<builtin::Interface>() {
self.define_interface(interface)?;
} else if let Some(function) = op.downcast_ref::<builtin::Function>() {
self.define_function(function)?;
} else {
panic!(
"invalid component-level operation: '{}' is not supported in a component body",
op.name()
)
}
}
for module in supporting {
self.define_module(&module.borrow())?;
}
if self.component.init.is_some() {
let fragments = self.build_function_table_fragments()?;
let owners = fragments.keys().cloned().collect::<Vec<_>>();
let mut child_calls: BTreeMap<masm::PathBuf, Vec<masm::PathBuf>> = BTreeMap::new();
let mut roots: Vec<masm::PathBuf> = Vec::new();
for owner in owners.iter() {
match owners
.iter()
.filter(|candidate| {
*candidate != owner
&& owner.as_path().starts_with_exactly(candidate.as_path())
})
.max_by_key(|candidate| candidate.as_path().components().count())
{
Some(parent) => {
child_calls.entry(parent.clone()).or_default().push(owner.clone())
}
None => roots.push(owner.clone()),
}
}
let span = SourceSpan::default();
let proc_name = masm::ProcedureName::new(FUNCTION_TABLE_INIT_PROC).unwrap();
for (
owner,
FunctionTableFragment {
mut body,
mut invoked,
a_callee,
},
) in fragments
{
for child in child_calls.remove(&owner).unwrap_or_default() {
let qualified =
masm::QualifiedProcedureName::new(child.as_path(), proc_name.clone());
let target =
masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
}
let Some(index) = self
.component
.modules
.iter()
.position(|module| module.path() == owner.as_path())
else {
return Err(Report::msg(format!(
"invalid function table entry: callee '{a_callee}' is defined in module \
'{owner}', which was not lowered because it holds only declarations — a \
function table cannot name a callee that has no definition to take the \
address of"
)));
};
let module = Arc::get_mut(&mut self.component.modules[index])
.expect("expected unique reference");
let mut procedure = masm::Procedure::new(
span,
masm::Visibility::Public,
proc_name.clone(),
0,
masm::Block::new(span, body),
)
.with_signature(masm::FunctionType::new(
midenc_hir::CallConv::Fast,
vec![],
vec![],
));
procedure.extend_invoked(invoked);
module
.define_procedure(procedure, self.source_manager.clone())
.into_diagnostic()
.wrap_err("failed to define a function table initializer")?;
}
for root in roots {
let qualified =
masm::QualifiedProcedureName::new(root.as_path(), proc_name.clone());
let target = masm::InvocationTarget::Path(Span::new(span, qualified.into_inner()));
self.invoked_from_init
.insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
self.init_body
.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(target))));
}
if let Some(start) = self.link_info.component_start() {
let start = start.borrow();
let target = super::lowering::invocation_target_from_symbol_path(
&start.path(),
start.span(),
);
self.invoked_from_init
.insert(masm::Invoke::new(masm::InvokeKind::Exec, target.clone()));
self.init_body
.push(masm::Op::Inst(Span::new(start.span(), masm::Instruction::Exec(target))));
}
let module =
Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
let init_name = masm::ProcedureName::new("init").unwrap();
let init_body = core::mem::take(&mut self.init_body);
let mut init = masm::Procedure::new(
Default::default(),
masm::Visibility::Public,
init_name,
0,
masm::Block::new(component.span(), init_body),
)
.with_signature(masm::FunctionType::new(
midenc_hir::CallConv::Fast,
vec![],
vec![],
));
init.extend_invoked(core::mem::take(&mut self.invoked_from_init));
module
.define_procedure(init, self.source_manager.clone())
.into_diagnostic()
.wrap_err("failed to define component `init` procedure")?;
} else {
assert!(
self.init_body.is_empty(),
"the need for an 'init' function was not expected, but code was generated for one"
);
}
Ok(())
}
fn define_interface(&mut self, interface: &builtin::Interface) -> Result<(), Report> {
let interface_path = if let Some(id) = self.component.id.as_ref() {
let mut path = id.to_library_path();
path.push(interface.name().as_str());
path
} else {
interface.path().to_library_path()
};
let mut masm_module =
Box::new(masm::Module::new(masm::ModuleKind::Library, interface_path));
let builder = MasmModuleBuilder {
module: &mut masm_module,
analysis_manager: self
.analysis_manager
.nest(interface.as_operation().as_operation_ref()),
link_info: self.link_info,
source_manager: self.source_manager.clone(),
init_body: &mut self.init_body,
invoked_from_init: &mut self.invoked_from_init,
};
builder.build_from_interface(interface)?;
self.component.modules.push(Arc::from(masm_module));
Ok(())
}
fn define_module(&mut self, module: &builtin::Module) -> Result<(), Report> {
let module_path = module.path().to_library_path();
let module_path = module_path.to_absolute().unwrap();
let trace_target = TraceTarget::category("codegen");
log::debug!(target: &trace_target, "defining module '{module_path}'");
let is_artifact_interface = self.component.id.is_none() || self.component.synthetic_wrapper;
let visibility = if is_artifact_interface {
masm::Visibility::Public
} else {
match *module.get_visibility() {
midenc_hir::Visibility::Public => masm::Visibility::Public,
midenc_hir::Visibility::Internal | midenc_hir::Visibility::Private => {
masm::Visibility::Private
}
}
};
let module_index = if let Some(rest) = module_path.strip_prefix(&self.component.root) {
self.define_module_tree(rest, Some(0), visibility)?
} else {
self.define_module_tree(&module_path, None, visibility)?
};
let masm_module = Arc::get_mut(&mut self.component.modules[module_index])
.expect("expected unique reference");
let builder = MasmModuleBuilder {
module: masm_module,
analysis_manager: self.analysis_manager.nest(module.as_operation_ref()),
link_info: self.link_info,
source_manager: self.source_manager.clone(),
init_body: &mut self.init_body,
invoked_from_init: &mut self.invoked_from_init,
};
let nested = builder.build(module)?;
for nested_module in nested {
self.define_module(&nested_module.borrow())?;
}
Ok(())
}
fn define_module_tree(
&mut self,
module_path: &masm::Path,
mut parent: Option<usize>,
visibility: masm::Visibility,
) -> Result<usize, Report> {
let trace_target = TraceTarget::category("codegen");
let mut path = masm::PathBuf::with_capacity(256);
if let Some(parent) = parent {
path = self.component.modules[parent].path().to_path_buf();
}
let mut components = module_path.components().peekable();
while let Some(component) = components.next() {
let name = component.unwrap().as_str();
if name == "::" {
continue;
}
path.push_component(name);
if !path.is_absolute() {
path = path.to_absolute().unwrap().into_owned();
}
let visibility = if components.peek().is_none() {
visibility
} else {
masm::Visibility::Public
};
let module_path = &path;
if let Some(parent_index) = parent {
let parent_module = Arc::get_mut(&mut self.component.modules[parent_index])
.expect("expected unique reference");
if parent_module.submodules().iter().any(|sm| sm.name.as_str() == name) {
parent = Some(
self.component
.modules
.iter()
.position(|m| m.path() == module_path.as_path())
.expect(
"submodule was already defined, but not registered with component",
),
);
} else {
let submodule =
Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
let name = masm::Ident::new(submodule.name()).unwrap();
log::debug!(target: &trace_target, "declaring submodule '{name}' of '{}'", parent_module.path());
parent_module.declare_submodule(name, visibility)?;
parent = Some(self.component.modules.len());
self.component.modules.push(Arc::from(submodule));
}
} else {
log::debug!(target: &trace_target, "declaring module '{module_path}'");
let module = Box::new(masm::Module::new(masm::ModuleKind::Library, module_path));
parent = Some(self.component.modules.len());
self.component.modules.push(Arc::from(module));
}
}
Ok(parent.unwrap())
}
fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
let builder = MasmFunctionBuilder::new(function)?;
let procedure = builder.build(
function,
self.analysis_manager.nest(function.as_operation_ref()),
self.link_info,
FunctionLoweringMode::Normal,
)?;
let module =
Arc::get_mut(&mut self.component.modules[0]).expect("expected unique reference");
let expected_path_len = if module.path().is_absolute() { 2 } else { 1 };
assert_eq!(
module.path().len(),
expected_path_len,
"expected top-level namespace module, but one has not been defined (in '{}' of '{}')",
module.path(),
function.path()
);
module
.define_procedure(procedure, self.source_manager.clone())
.into_diagnostic()
.wrap_err("failed to define MASM procedure")?;
Ok(())
}
fn emit_data_segment_initialization(&mut self) {
use masm::{Instruction as Inst, InvocationTarget, Op};
let span = SourceSpan::default();
let pipe_preimage_to_memory = {
let name = masm::ProcedureName::new("pipe_preimage_to_memory").unwrap();
let module = masm::LibraryPath::new("::miden::core::mem").unwrap();
let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
InvocationTarget::Path(Span::new(span, qualified.into_inner()))
};
for rodata in self.component.rodata.iter() {
let word = rodata.digest.as_elements();
let word_value = [word[0], word[1], word[2], word[3]];
self.init_body.push(Op::Inst(Span::new(
span,
Inst::Push(masm::Immediate::Value(Span::unknown(WordValue(word_value).into()))),
)));
self.init_body
.push(Op::Inst(Span::new(span, Inst::SysEvent(masm::SystemEventNode::PushMapVal))));
assert!(rodata.start.is_word_aligned(), "rodata segments must be word-aligned");
self.init_body.push(Op::Inst(Span::new(
span,
Inst::Push(masm::Immediate::Value(Span::unknown(rodata.start.addr.into()))),
)));
self.init_body.push(Op::Inst(Span::new(
span,
Inst::Push(masm::Immediate::Value(Span::unknown(
(rodata.size_in_words() as u32).into(),
))),
)));
self.init_body.push(Op::Inst(Span::new(
span,
Inst::EmitImm(Event::FrameStart.as_event_id().as_felt().into()),
)));
self.init_body
.push(Op::Inst(Span::new(span, Inst::Exec(pipe_preimage_to_memory.clone()))));
self.init_body.push(Op::Inst(Span::new(
span,
Inst::EmitImm(Event::FrameEnd.as_event_id().as_felt().into()),
)));
self.init_body.push(Op::Inst(Span::new(span, Inst::Drop)));
}
}
fn build_function_table_fragments(
&self,
) -> Result<BTreeMap<masm::PathBuf, FunctionTableFragment>, Report> {
use masm::{Instruction as Inst, Op};
let span = SourceSpan::default();
let mut fragments: BTreeMap<masm::PathBuf, FunctionTableFragment> = BTreeMap::new();
let layout = self.link_info.function_tables();
for (table_ref, _) in layout.iter() {
let base_addr = layout
.element_addr_of(table_ref)
.expect("link error: missing function table in computed layout");
let table = table_ref.borrow();
let live_entries = table.live_entries().map_err(|op_name| {
Report::msg(format!(
"invalid function table entry: '{op_name}' is not supported in a function \
table body"
))
})?;
for (slot, entry) in live_entries {
let entry = entry.borrow();
if slot >= *table.get_num_slots() {
return Err(Report::msg(format!(
"invalid function table entry: slot {slot} is out of bounds for table \
'{}' with {} slots",
table.get_name().as_str(),
*table.get_num_slots()
)));
}
let type_tag = *entry.get_type_tag();
if type_tag == 0 {
return Err(Report::msg(format!(
"invalid function table entry: slot {slot} of table '{}' uses signature \
tag 0, which is reserved for null slots",
table.get_name().as_str(),
)));
}
let Some(callee) = entry.resolve_callee() else {
return Err(Report::msg(format!(
"invalid function table entry: unable to resolve callee '{}'",
entry.callee().path()
)));
};
let callee_path = callee.borrow().path();
let target =
super::lowering::invocation_target_from_symbol_path(&callee_path, span);
let owner = callee_path.without_leaf().to_library_path();
let owner = owner.to_absolute().unwrap().into_owned();
let fragment = fragments.entry(owner).or_insert_with(|| FunctionTableFragment {
body: Default::default(),
invoked: Default::default(),
a_callee: callee_path.to_string(),
});
let FunctionTableFragment { body, invoked, .. } = fragment;
invoked.insert(masm::Invoke::new(masm::InvokeKind::ProcRef, target.clone()));
let slot_addr = base_addr + slot * FunctionTableLayout::SLOT_SIZE_ELEMENTS;
let tag_addr = slot_addr + FunctionTableLayout::TYPE_TAG_OFFSET_ELEMENTS;
body.push(Op::Inst(Span::new(span, Inst::ProcRef(target))));
body.push(Op::Inst(Span::new(span, Inst::MemStoreWLeImm(slot_addr.into()))));
body.push(Op::Inst(Span::new(span, Inst::DropW)));
body.push(Op::Inst(Span::new(
span,
Inst::Push(masm::Immediate::Value(Span::new(span, type_tag.into()))),
)));
body.push(Op::Inst(Span::new(span, Inst::MemStoreImm(tag_addr.into()))));
}
}
Ok(fragments)
}
}
struct FunctionTableFragment {
body: Vec<masm::Op>,
invoked: BTreeSet<masm::Invoke>,
a_callee: String,
}
struct MasmModuleBuilder<'a> {
module: &'a mut masm::Module,
analysis_manager: AnalysisManager,
link_info: &'a LinkInfo,
source_manager: Arc<dyn midenc_session::SourceManager>,
init_body: &'a mut Vec<masm::Op>,
invoked_from_init: &'a mut BTreeSet<masm::Invoke>,
}
impl MasmModuleBuilder<'_> {
pub fn build(mut self, module: &builtin::Module) -> Result<Vec<builtin::ModuleRef>, Report> {
let mut nested = Vec::new();
let region = module.body();
let block = region.entry();
for op in block.body() {
if let Some(function) = op.downcast_ref::<builtin::Function>() {
self.define_function(function)?;
} else if let Some(gv) = op.downcast_ref::<builtin::GlobalVariable>() {
self.emit_global_variable_initializer(gv)?;
} else if let Some(nested_module) = op.downcast_ref::<builtin::Module>() {
nested.push(nested_module.as_module_ref());
} else if op.is::<builtin::Segment>() {
continue;
} else if op.is::<builtin::FunctionTable>() {
continue;
} else {
panic!(
"invalid module-level operation: '{}' is not legal in a MASM module body",
op.name()
)
}
}
Ok(nested)
}
pub fn build_from_interface(mut self, interface: &builtin::Interface) -> Result<(), Report> {
let region = interface.body();
let block = region.entry();
for op in block.body() {
if let Some(function) = op.downcast_ref::<builtin::Function>() {
self.define_function(function)?;
} else {
panic!(
"invalid interface-level operation: '{}' is not legal in a MASM module body",
op.name()
)
}
}
Ok(())
}
fn define_function(&mut self, function: &builtin::Function) -> Result<(), Report> {
let builder = MasmFunctionBuilder::new(function)?;
let procedure = builder.build(
function,
self.analysis_manager.nest(function.as_operation_ref()),
self.link_info,
FunctionLoweringMode::Normal,
)?;
self.module
.define_procedure(procedure, self.source_manager.clone())
.map_err(|e| Report::msg(e.to_string()))?;
Ok(())
}
fn emit_global_variable_initializer(
&mut self,
gv: &builtin::GlobalVariable,
) -> Result<(), Report> {
if gv.is_declaration() {
return Ok(());
}
let analysis_manager = self.analysis_manager.nest(gv.as_operation_ref());
let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
let initializer_region = gv.region(0);
let initializer_block = initializer_region.entry();
let mut block_emitter = BlockEmitter {
liveness: &liveness,
link_info: self.link_info,
invoked: self.invoked_from_init,
target: Default::default(),
stack: OperandStack::new(gv.as_operation().context_rc()),
trace_target: TraceTarget::category("codegen")
.with_relevant_symbol(gv.name().as_symbol()),
};
block_emitter.emit_inline(&initializer_block);
assert_eq!(block_emitter.stack.len(), 1, "expected only global variable value on stack");
let return_ty = block_emitter.stack.peek().unwrap().ty();
assert_eq!(
&return_ty,
&*gv.get_ty(),
"expected initializer to return value of same type as declaration"
);
let computed_addr = self
.link_info
.globals_layout()
.get_computed_addr(gv.as_global_var_ref())
.expect("undefined global variable");
block_emitter.emitter().store_imm(computed_addr, gv.span());
let mut body = core::mem::take(&mut block_emitter.target);
self.init_body.append(&mut body);
Ok(())
}
}
struct MasmFunctionBuilder {
span: midenc_hir::SourceSpan,
name: masm::ProcedureName,
signature: masm::FunctionType,
visibility: masm::Visibility,
num_locals: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FunctionLoweringMode {
Normal,
ExecutableEntrypointWithoutInit,
}
impl MasmFunctionBuilder {
pub fn new(function: &builtin::Function) -> Result<Self, Report> {
use midenc_hir::{Symbol, Visibility};
if function.is_declaration() {
return Err(function_without_a_body(function));
}
let name = *function.get_name();
let name = masm::ProcedureName::from_raw_parts(masm::Ident::from_raw_parts(Span::new(
name.span,
name.as_ref().into(),
)));
let visibility = match function.visibility() {
Visibility::Public => masm::Visibility::Public,
Visibility::Internal => masm::Visibility::Public,
Visibility::Private => masm::Visibility::Private,
};
let locals_required = function.locals().iter().map(|ty| ty.size_in_felts()).sum::<usize>();
let num_locals = u16::try_from(locals_required).map_err(|_| {
let context = function.as_operation().context();
context
.diagnostics()
.diagnostic(miden_assembly::diagnostics::Severity::Error)
.with_message("cannot emit masm for function")
.with_primary_label(
function.span(),
"local storage exceeds procedure limit: no more than u16::MAX elements are \
supported",
)
.into_report()
})?;
let signature =
semantic_debug_signature(function).unwrap_or_else(|| lowered_signature(function));
Ok(Self {
span: function.span(),
name,
signature,
visibility,
num_locals,
})
}
pub fn build(
self,
function: &builtin::Function,
analysis_manager: AnalysisManager,
link_info: &LinkInfo,
mode: FunctionLoweringMode,
) -> Result<masm::Procedure, Report> {
use alloc::collections::BTreeSet;
use midenc_hir_analysis::analyses::LivenessAnalysis;
let demangled_symbol_name = midenc_hir::demangle::demangle(function.get_name().as_str());
let trace_target = TraceTarget::category("codegen")
.with_relevant_symbol(midenc_hir::SymbolName::intern(demangled_symbol_name));
log::trace!(target: &trace_target, "lowering {}", function.as_operation());
let liveness = analysis_manager.get_analysis::<LivenessAnalysis>()?;
let mut invoked = BTreeSet::default();
let entry = function.entry_block();
let mut stack = crate::OperandStack::new(function.as_operation().context_rc());
{
let entry_block = entry.borrow();
for arg in entry_block.arguments().iter().rev().copied() {
stack.push(arg as ValueRef);
}
}
let mut emitter = BlockEmitter {
liveness: &liveness,
link_info,
invoked: &mut invoked,
target: Default::default(),
stack,
trace_target,
};
if mode == FunctionLoweringMode::Normal
&& function.signature().cc.is_wasm_canonical_abi()
&& link_info.requires_init()
{
let init = InvocationTarget::Symbol("init".parse().unwrap());
emitter.emitter().emit(masm::Instruction::Exec(init), SourceSpan::default());
}
let mut body = emitter.emit(&entry.borrow());
if function.signature().cc.is_wasm_canonical_abi() {
let truncate_stack = {
let name = masm::ProcedureName::new("truncate_stack").unwrap();
let module = masm::LibraryPath::new("::miden::core::sys").unwrap();
let qualified = masm::QualifiedProcedureName::new(module.as_path(), name);
InvocationTarget::Path(Span::new(SourceSpan::default(), qualified.into_inner()))
};
let span = SourceSpan::default();
invoked.insert(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
body.push(masm::Op::Inst(Span::new(span, masm::Instruction::Exec(truncate_stack))));
}
let Self {
span,
name,
signature,
visibility,
num_locals,
} = self;
let aligned_num_locals = num_locals.next_multiple_of(miden_core::WORD_SIZE as u16);
let stack_pointer_addr = link_info.globals_layout().stack_pointer_offset();
patch_debug_var_locals_in_block(&mut body, aligned_num_locals, stack_pointer_addr);
if !block_has_real_instructions(&body) {
body.push(masm::Op::Inst(Span::unknown(masm::Instruction::Nop)));
}
let mut procedure = masm::Procedure::new(span, visibility, name, num_locals, body);
procedure.set_signature(signature);
if mode == FunctionLoweringMode::Normal {
for attribute in [
midenc_dialect_hir::ACCOUNT_PROCEDURE_EXPORT_ATTR,
midenc_dialect_hir::AUTH_SCRIPT_EXPORT_ATTR,
midenc_dialect_hir::NOTE_SCRIPT_EXPORT_ATTR,
midenc_dialect_hir::TRANSACTION_SCRIPT_EXPORT_ATTR,
] {
if function.has_attribute(attribute) {
procedure
.attributes_mut()
.insert(Attribute::Marker(masm::Ident::new(attribute).unwrap()));
}
}
}
procedure.extend_invoked(invoked);
Ok(procedure)
}
}
fn lowered_signature(function: &builtin::Function) -> masm::FunctionType {
let sig = function.signature();
let args = sig.params.iter().map(|param| masm::TypeExpr::from(param.ty.clone())).collect();
let results = sig
.results
.iter()
.map(|result| masm::TypeExpr::from(result.ty.clone()))
.collect();
masm::FunctionType::new(sig.cc, args, results)
}
fn semantic_debug_signature(function: &builtin::Function) -> Option<masm::FunctionType> {
let subprogram = function
.as_operation()
.get_attribute("di.subprogram")?
.try_downcast_attr::<SubprogramAttr>()
.ok()?;
let subprogram = subprogram.borrow();
let Type::Function(ty) = subprogram.ty.as_ref()? else {
return None;
};
let args = ty.params().iter().cloned().map(masm::TypeExpr::from).collect();
let results = ty.results().iter().cloned().map(masm::TypeExpr::from).collect();
Some(masm::FunctionType::new(ty.calling_convention(), args, results))
}
fn block_has_real_instructions(block: &masm::Block) -> bool {
block.iter().any(|op| match op {
masm::Op::Inst(inst) => !matches!(inst.inner(), masm::Instruction::DebugVar(_)),
masm::Op::If {
then_blk, else_blk, ..
} => block_has_real_instructions(then_blk) || block_has_real_instructions(else_blk),
masm::Op::While { body, .. } => block_has_real_instructions(body),
masm::Op::DoWhile {
body, condition, ..
} => block_has_real_instructions(body) || block_has_real_instructions(condition),
masm::Op::Repeat { body, .. } => block_has_real_instructions(body),
})
}
fn patch_debug_var_locals_in_block(
block: &mut masm::Block,
aligned_num_locals: u16,
stack_pointer_addr: Option<u32>,
) {
for op in block.iter_mut() {
match op {
masm::Op::Inst(span_inst) => {
if let masm::Instruction::DebugVar(info) = &mut **span_inst {
let location = patch_debug_var_location(
info.value_location(),
aligned_num_locals,
stack_pointer_addr,
);
info.set_value_location(location);
}
}
masm::Op::If {
then_blk, else_blk, ..
} => {
patch_debug_var_locals_in_block(then_blk, aligned_num_locals, stack_pointer_addr);
patch_debug_var_locals_in_block(else_blk, aligned_num_locals, stack_pointer_addr);
}
masm::Op::While {
body: while_body, ..
} => {
patch_debug_var_locals_in_block(while_body, aligned_num_locals, stack_pointer_addr);
}
masm::Op::DoWhile {
body, condition, ..
} => {
patch_debug_var_locals_in_block(body, aligned_num_locals, stack_pointer_addr);
patch_debug_var_locals_in_block(condition, aligned_num_locals, stack_pointer_addr);
}
masm::Op::Repeat {
body: repeat_body, ..
} => {
patch_debug_var_locals_in_block(
repeat_body,
aligned_num_locals,
stack_pointer_addr,
);
}
}
}
}
fn patch_debug_var_location(
location: &DebugVarLocation,
aligned_num_locals: u16,
stack_pointer_addr: Option<u32>,
) -> DebugVarLocation {
match location {
DebugVarLocation::Local(index) => {
checked_fmp_local_offset(i64::from(*index), aligned_num_locals)
.map(DebugVarLocation::Local)
.unwrap_or_else(debug_var_kill_location)
}
DebugVarLocation::FrameBase { byte_offset, .. } => {
if let Some(resolved_addr) = stack_pointer_addr.filter(|addr| *addr < (1 << 31)) {
DebugVarLocation::FrameBase {
global_index: resolved_addr,
byte_offset: *byte_offset,
}
} else {
debug_var_kill_location()
}
}
DebugVarLocation::Expression(bytes) => {
let Ok(expression) = Expression::read_from_bytes_with_budget(bytes, bytes.len()) else {
return location.clone();
};
let [
ExpressionOp::FrameBase {
base: FrameBase::Local(local_index),
byte_offset,
},
] = expression.operations.as_slice()
else {
return location.clone();
};
checked_fmp_local_offset(i64::from(*local_index), aligned_num_locals)
.map(|local_offset| DebugVarLocation::FrameBase {
global_index: encode_frame_base_local_offset(local_offset),
byte_offset: *byte_offset,
})
.unwrap_or_else(debug_var_kill_location)
}
DebugVarLocation::Stack(_) | DebugVarLocation::Memory(_) | DebugVarLocation::Const(_) => {
location.clone()
}
}
}
fn checked_fmp_local_offset(index: i64, aligned_num_locals: u16) -> Option<i16> {
i16::try_from(index - i64::from(aligned_num_locals)).ok()
}
fn debug_var_kill_location() -> DebugVarLocation {
DebugVarLocation::Expression(super::DEBUG_VAR_KILL_SENTINEL.to_vec())
}
const FRAME_BASE_LOCAL_MARKER: u32 = 1 << 31;
fn encode_frame_base_local_offset(local_offset: i16) -> u32 {
FRAME_BASE_LOCAL_MARKER | u32::from(u16::from_le_bytes(local_offset.to_le_bytes()))
}