Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions codegen/masm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- keep internal component procedures out of compiled MASM package exports so library packages expose
only lifted Component Model wrappers
- preserve generated component initializer callees in MASM procedure metadata

## [0.5.1](https://github.com/0xMiden/compiler/compare/midenc-codegen-masm-v0.5.0...midenc-codegen-masm-v0.5.1) - 2025-11-13

### Other
Expand Down
169 changes: 28 additions & 141 deletions codegen/masm/src/artifact.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
use alloc::sync::Arc;
use core::fmt;

use miden_assembly::{Path, ast::InvocationTarget};
use miden_assembly::Path;
use miden_core::Word;
use miden_mast_package::Package;
use midenc_hir::{constants::ConstantData, dialects::builtin, interner::Symbol};
use midenc_session::{
Emit, OutputMode, OutputType, Session, Writer,
diagnostics::{IntoDiagnostic, Report, SourceSpan, Span, WrapErr},
};
use midenc_session::{Emit, OutputMode, OutputType, Session, Writer, diagnostics::Report};

use crate::{TraceEvent, lower::NativePtr, masm};
use crate::{lower::NativePtr, masm};

mod project_support;

Expand All @@ -20,12 +17,14 @@ pub struct MasmComponent {
///
/// All components must have a canonical root module, even if empty
pub root: Arc<Path>,
/// The symbol name of the component initializer function
/// Whether this component requires an initializer procedure.
///
/// This function is responsible for initializing global variables and writing data segments
/// The initializer is responsible for initializing global variables and writing data segments
/// into memory at program startup, and at cross-context call boundaries (in callee prologue).
pub init: Option<masm::InvocationTarget>,
/// The symbol name of the program entrypoint, if this component is executable.
/// When set, a private root-local `init` procedure is generated and invoked by the lifted
/// export wrappers (and the generated executable entrypoint) via a same-module symbol.
pub requires_init: bool,
/// The invocation target for the program entrypoint, if this component is executable.
///
/// If unset, it indicates that the component is a library, even if it could be made executable.
pub entrypoint: Option<masm::InvocationTarget>,
Expand All @@ -37,6 +36,14 @@ pub struct MasmComponent {
pub heap_base: u32,
/// The address of the `__stack_pointer` global, if such a global has been defined
pub stack_pointer: Option<u32>,
/// Whether non-root support modules are implementation details of the component package.
///
/// When true, support modules are statically linked into library packages instead of being
/// surfaced as public package modules. This static linking — not procedure visibility — is
/// what prunes the lowered core Wasm procedures from the package export surface in the common
/// (non-synthetic-wrapper) component-library case, because MASM currently lowers `Internal`
/// visibility to public.
pub link_support_modules_privately: bool,
/// The set of modules in this component
pub modules: Vec<Arc<masm::Module>>,
}
Expand Down Expand Up @@ -177,6 +184,17 @@ impl fmt::Display for MasmComponent {
}

impl MasmComponent {
/// Get a mutable reference to the component root module (`modules[0]`).
///
/// The root module is never cloned during lowering, so the unique reference is guaranteed.
pub(crate) fn root_module_mut(&mut self) -> &mut masm::Module {
debug_assert!(
self.modules[0].path() == self.root.as_ref(),
"modules[0] must be the component root module"
);
Arc::get_mut(&mut self.modules[0]).expect("expected unique reference")
}

/// Assemble this component into a Miden package.
pub fn assemble(
&self,
Expand All @@ -200,137 +218,6 @@ impl MasmComponent {
registry,
)
}

/// Generate an executable module which when run expects the raw data segment data to be
/// provided on the advice stack in the same order as initialization, and the operands of
/// the entrypoint function on the operand stack.
fn generate_main(
&self,
entrypoint: &InvocationTarget,
emit_test_harness: bool,
source_manager: Arc<dyn midenc_session::SourceManager + Send + Sync>,
) -> Result<Box<masm::Module>, Report> {
use masm::{Instruction as Inst, Op};

let mut exe = Box::new(masm::Module::new_executable());
let span = SourceSpan::default();
let mut invoked = Vec::new();
let body = {
let mut block = masm::Block::new(span, Vec::with_capacity(64));
// Invoke component initializer, if present
if let Some(init) = self.init.as_ref() {
invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, init.clone()));
block.push(Op::Inst(Span::new(span, Inst::Exec(init.clone()))));
}

// Initialize test harness, if requested
if emit_test_harness {
self.emit_test_harness(&mut block);
}

// Invoke the program entrypoint
block.push(Op::Inst(Span::new(
span,
Inst::Trace(TraceEvent::FrameStart.as_u32().into()),
)));
invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, entrypoint.clone()));
block.push(Op::Inst(Span::new(span, Inst::Exec(entrypoint.clone()))));
block
.push(Op::Inst(Span::new(span, Inst::Trace(TraceEvent::FrameEnd.as_u32().into()))));

// Truncate the stack to 16 elements on exit
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(span, qualified.into_inner()))
};
invoked.push(masm::Invoke::new(masm::InvokeKind::Exec, truncate_stack.clone()));
block.push(Op::Inst(Span::new(span, Inst::Exec(truncate_stack))));
block
};
let mut start = masm::Procedure::new(
span,
masm::Visibility::Public,
masm::ProcedureName::main(),
0,
body,
);
start.extend_invoked(invoked);
exe.define_procedure(start, source_manager)
.into_diagnostic()
.wrap_err("failed to define executable `main` procedure")?;
Ok(exe)
}

fn emit_test_harness(&self, block: &mut masm::Block) {
use masm::{Instruction as Inst, IntValue, Op, PushValue};
use miden_core::Felt;

let span = SourceSpan::default();

let pipe_words_to_memory = {
let name = masm::ProcedureName::new("pipe_words_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()))
};

// Step 1: Get the number of initializers to run
// => [inits] on operand stack
block.push(Op::Inst(Span::new(span, Inst::AdvPush)));

// Step 2: Evaluate the initial state of the loop condition `inits > 0`
// => [inits, inits]
block.push(Op::Inst(Span::new(span, Inst::Dup0)));
// => [inits > 0, inits]
block.push(Op::Inst(Span::new(span, Inst::Push(PushValue::Int(IntValue::U8(0)).into()))));
block.push(Op::Inst(Span::new(span, Inst::Gt)));

// Step 3: Loop until `inits == 0`
let mut loop_body = Vec::with_capacity(16);

// State of operand stack on entry to `loop_body`: [inits]
// State of advice stack on entry to `loop_body`: [dest_ptr, num_words, ...]
//
// Step 3a: Compute next value of `inits`, i.e. `inits'`
// => [inits - 1]
loop_body.push(Op::Inst(Span::new(span, Inst::SubImm(Felt::ONE.into()))));

// Step 3b: Copy initializer data to memory
// => [num_words, dest_ptr, inits']
loop_body.push(Op::Inst(Span::new(span, Inst::AdvPush)));
loop_body.push(Op::Inst(Span::new(span, Inst::AdvPush)));
// => [C, B, A, dest_ptr, inits'] on operand stack
loop_body
.push(Op::Inst(Span::new(span, Inst::Trace(TraceEvent::FrameStart.as_u32().into()))));
loop_body.push(Op::Inst(Span::new(span, Inst::Exec(pipe_words_to_memory))));
loop_body
.push(Op::Inst(Span::new(span, Inst::Trace(TraceEvent::FrameEnd.as_u32().into()))));
// Drop C, B, A
loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
loop_body.push(Op::Inst(Span::new(span, Inst::DropW)));
// => [inits']
loop_body.push(Op::Inst(Span::new(span, Inst::Drop)));

// Step 3c: Evaluate loop condition `inits' > 0`
// => [inits', inits']
loop_body.push(Op::Inst(Span::new(span, Inst::Dup0)));
// => [inits' > 0, inits']
loop_body
.push(Op::Inst(Span::new(span, Inst::Push(PushValue::Int(IntValue::U8(0)).into()))));
loop_body.push(Op::Inst(Span::new(span, Inst::Gt)));

// Step 4: Enter (or skip) loop
block.push(Op::While {
span,
body: masm::Block::new(span, loop_body),
});

// Step 5: Drop `inits` after loop is evaluated
block.push(Op::Inst(Span::new(span, Inst::Drop)));
}
}

#[cfg(test)]
Expand Down
57 changes: 28 additions & 29 deletions codegen/masm/src/artifact/project_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,8 @@ pub(super) fn assemble_with_registry(
}
}

let is_executable_target = session.options.target_type.is_some_and(|tt| tt.is_executable())
|| project_package.library_target().is_none()
|| session.options.target.as_deref().is_some_and(|tname| {
project_package.executable_targets().iter().any(|t| tname == &**t.name)
});
let sources = prepare_sources(
component,
&mut assembler,
session.get_flag("test_harness"),
session,
is_executable_target,
)?;
let is_executable_target = session.is_executable_target();
let sources = prepare_sources(component, &mut assembler, is_executable_target)?;
let mut project_assembler = assembler.for_project(project_package.clone(), registry)?;

let selector = if is_executable_target {
Expand Down Expand Up @@ -111,20 +101,28 @@ fn selected_executable_target_name<'a>(
Ok(session.name.as_ref())
}

/// Prepare the synthetic project target and source inputs used to assemble compiler-generated MASM.
/// Partition the component's modules into the project root, surfaced support sources, and modules
/// statically linked into the assembler, producing the source inputs for project assembly.
fn prepare_sources(
component: &MasmComponent,
assembler: &mut Assembler,
emit_test_harness: bool,
session: &Session,
generate_executable_main: bool,
is_executable_target: bool,
) -> Result<ProjectSourceInputs, Report> {
// Intrinsics must be linked into the assembler context directly so they do not become part of
// the assembled package surface.
// Non-root support modules hold the lowered core Wasm procedures called by lifted wrappers.
// For a component library they are implementation details, so statically link them into the
// assembler (below) rather than surfacing them as package source. This static linking — not
// procedure visibility — is what keeps them off the package export surface, because MASM
// currently lowers `Internal` to public. Executable targets and synthetic-wrapper/standalone
// components keep them as source inputs instead.
let link_support_modules_privately =
!is_executable_target && component.link_support_modules_privately;

let mut support = Vec::with_capacity(component.modules.len());
let mut root = None;
for module in component.modules.iter() {
if is_intrinsics_module(module) {
// Intrinsics must be linked into the assembler context directly so they do not become
// part of the assembled package surface.
log::debug!(
target: "assembly",
"adding intrinsics '{}' to assembler",
Expand All @@ -139,18 +137,19 @@ fn prepare_sources(
continue;
}

support.push(Box::new(Arc::unwrap_or_clone(module.clone())));
}
// Component library support modules contain the lowered core Wasm procedures called by
// lifted wrappers, but they are not part of the component package export surface.
if link_support_modules_privately {
log::debug!(
target: "assembly",
"adding component support module '{}' to assembler",
module.path()
);
assembler.compile_and_statically_link(module.clone())?;
continue;
}

if generate_executable_main && let Some(entrypoint) = component.entrypoint.as_ref() {
// Our generated main module takes precedence here, so move the root module into support
support.extend(root);
let root = component.generate_main(
entrypoint,
emit_test_harness,
session.source_manager.clone(),
)?;
return Ok(ProjectSourceInputs { root, support });
support.push(Box::new(Arc::unwrap_or_clone(module.clone())));
}

let root = root.expect("components must always have a root module");
Expand Down
6 changes: 6 additions & 0 deletions codegen/masm/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ impl LinkInfo {
!self.segment_layout.is_empty()
}

/// Returns true if the component needs an initializer procedure to populate global variables
/// and data segments into memory before its exports run.
pub fn requires_init(&self) -> bool {
self.has_globals() || self.has_data_segments()
}

pub fn globals_layout(&self) -> &GlobalVariableLayout {
&self.globals_layout
}
Expand Down
Loading
Loading