use alloc::{collections::BTreeMap, format, rc::Rc, string::String};
use core::cell::RefCell;
use midenc_hir::{
Context, OperationRef, PointerType,
diagnostics::Uri,
dialects::builtin::{FunctionRef, attributes::UnitAttr},
};
use midenc_session::{
InputFile, Options, Session,
diagnostics::{CaptureEmitter, DefaultSourceManager},
};
use super::*;
const COMPONENT: &str = r#"
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @test {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
};
"#;
const WORLD: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @test {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
};
};
"#;
const WORLD_WITH_DECLARATION_ONLY_SIBLING: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @test {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
};
builtin.module public @external_dep {
builtin.function public extern("C") @sibling() {
};
};
};
"#;
const WORLD_WITH_SUPPORTING_SIBLING: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @test {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
};
builtin.module public @supporting {
builtin.function public extern("C") @sibling() {
builtin.ret;
};
};
};
"#;
const WORLD_CALLING_ITS_SUPPORTING_SIBLING: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @test {
builtin.function public extern("C") @main() {
hir.exec ::@supporting::@sibling() : extern("C") () -> ();
builtin.ret;
};
};
};
builtin.module public @supporting {
builtin.function public extern("C") @sibling() {
builtin.ret;
};
};
};
"#;
fn world_with_a_sibling_defining_a_global() -> String {
WORLD_WITH_SUPPORTING_SIBLING.replace(
"builtin.module public @supporting {",
"builtin.module public @supporting {\n builtin.global_variable public @g : i32 \
{\n builtin.ret_imm 1 : i32;\n };",
)
}
fn world_with_a_sibling_declaring_a_global() -> String {
WORLD_WITH_SUPPORTING_SIBLING.replace(
"builtin.module public @supporting {",
"builtin.module public @supporting {\n builtin.global_variable public @g : i32;",
)
}
fn wrapper_world_with_a_supporting_sibling() -> String {
WORLD_WITH_SUPPORTING_SIBLING.replace("hir_ns:test@1.0.0", "root_ns:root@1.0.0")
}
fn mark_as_synthetic_wrapper(world: builtin::WorldRef) {
let mut component = {
let world = world.borrow();
let body = world.body();
let component = body
.entry()
.body()
.iter()
.find_map(|op| op.as_operation_ref().try_downcast_op::<builtin::Component>().ok());
component.expect("the fixture must declare a component to mark")
};
component.borrow_mut().mark_synthetic_wrapper();
}
fn world_with_a_sibling_declaring_one_of_its_functions() -> String {
WORLD_WITH_SUPPORTING_SIBLING.replace(
"builtin.module public @supporting {",
"builtin.module public @supporting {\n builtin.function public extern(\"C\") \
@undefined() {\n };",
)
}
fn component_with_a_declared_function_in_its_module() -> String {
COMPONENT.replace(
"builtin.module private @test {",
"builtin.module private @test {\n builtin.function public extern(\"C\") \
@undefined() {\n };",
)
}
fn component_with_a_declared_component_level_function() -> String {
COMPONENT.replace(
"builtin.module private @test {",
"builtin.function public extern(\"C\") @undefined() {\n };\n builtin.module private \
@test {",
)
}
fn define_a_data_segment_in(module: builtin::ModuleRef) {
use midenc_hir::dialects::builtin::ModuleBuilder;
ModuleBuilder::new(module)
.define_data_segment(1024, alloc::vec![1u8, 2, 3, 4], true, SourceSpan::default())
.expect("should define a data segment");
}
fn top_level_module(world: &builtin::World, name: &str) -> builtin::ModuleRef {
world
.body()
.entry()
.body()
.iter()
.find_map(|op| {
op.as_operation_ref()
.try_downcast_op::<builtin::Module>()
.ok()
.filter(|module| module.borrow().name().as_str() == name)
})
.unwrap_or_else(|| panic!("the fixture declares a top-level module named '{name}'"))
}
fn module_paths(component: &MasmComponent) -> Vec<String> {
component.modules.iter().map(|module| module.path().to_string()).collect()
}
const TWO_COMPONENT_WORLD: &str = r#"
builtin.world {
builtin.component private @"hir_ns:first@1.0.0" {
builtin.module private @first {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
};
builtin.component private @"hir_ns:second@1.0.0" {
builtin.module private @second {
builtin.function public extern("C") @other() {
builtin.ret;
};
};
};
};
"#;
const MODULE: &str = r#"
builtin.module public @lib {
builtin.function public extern("C") @main() {
builtin.ret;
};
};
"#;
fn two_module_world() -> String {
format!("builtin.world {{{}{}}};\n", MODULE, MODULE.replace("@lib", "@second"))
}
fn library_target(namespace: &str) -> midenc_session::miden_project::Target {
midenc_session::miden_project::Target::library(
Arc::<masm::Path>::from(
masm::LibraryPath::new(namespace)
.unwrap()
.to_absolute()
.unwrap()
.into_owned()
.into_boxed_path(),
),
Uri::new("lib.hir"),
)
}
fn assembled_library_exports(
context: &Rc<Context>,
component: &MasmComponent,
namespace: &str,
) -> Vec<String> {
let target = library_target(namespace);
let sources = component
.source_inputs(&target, context.session())
.expect("the lowered component should provide assembler inputs");
let package = miden_assembly::Assembler::new(context.session().source_manager.clone())
.assemble_library(namespace, sources.root, sources.support)
.expect("the lowered component should assemble as a library");
let mut exports = package
.manifest
.exports()
.map(|export| export.path().as_ref().as_str().to_string())
.collect::<Vec<_>>();
exports.sort();
exports
}
fn parse(context: &Rc<Context>, text: &str) -> OperationRef {
let config = midenc_hir::parse::ParserConfig {
context: context.clone(),
verify: true,
};
midenc_hir::parse::parse_any(config, Uri::new("test.hir"), text)
.expect("the fixture should parse")
}
fn lower_world(world: builtin::WorldRef) -> Result<MasmComponent, Report> {
let analysis_manager = AnalysisManager::new(world.as_operation_ref(), None);
let world = world.borrow();
world.to_masm_component(analysis_manager)
}
fn lower_component(context: &Rc<Context>, text: &str) -> Result<MasmComponent, Report> {
let op = parse(context, text);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
let analysis_manager = AnalysisManager::new(op, None);
component.borrow().to_masm_component(analysis_manager)
}
fn legalize_and_lower_component(
context: &Rc<Context>,
text: &str,
) -> Result<MasmComponent, Report> {
use midenc_hir::pass::{Nesting, PassManager};
let op = parse(context, text);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
let mut pm = PassManager::on::<builtin::Component>(context.clone(), Nesting::Implicit);
pm.add_pass(alloc::boxed::Box::new(crate::LegalizeForMasm));
pm.enable_verifier(false);
pm.run(component.as_operation_ref())?;
let analysis_manager = AnalysisManager::new(op, None);
component.borrow().to_masm_component(analysis_manager)
}
fn legalize_and_lower_world(context: &Rc<Context>, text: &str) -> Result<MasmComponent, Report> {
use midenc_hir::pass::{Nesting, PassManager};
let world = parse_world(context, text);
let mut pm = PassManager::on::<builtin::World>(context.clone(), Nesting::Implicit);
pm.add_pass(alloc::boxed::Box::new(crate::LegalizeForMasm));
pm.enable_verifier(false);
pm.run(world.as_operation_ref())?;
lower_world(world)
}
fn parse_world(context: &Rc<Context>, text: &str) -> builtin::WorldRef {
parse(context, text)
.try_downcast_op::<builtin::World>()
.unwrap_or_else(|_| panic!("the fixture should parse as a world"))
}
fn anchoring_world(op: OperationRef) -> builtin::WorldRef {
op.parent_op()
.expect("the parser anchors every non-world top-level operation at a world it creates")
.try_downcast_op::<builtin::World>()
.unwrap_or_else(|_| panic!("and that anchor is a world"))
}
fn capturing_context() -> (Rc<Context>, alloc::sync::Arc<CaptureEmitter>) {
let emitter = alloc::sync::Arc::new(CaptureEmitter::new());
let options = alloc::boxed::Box::new(Options::default());
let source_manager = alloc::sync::Arc::new(DefaultSourceManager::default());
let session = Session::new(InputFile::empty(), options, Some(emitter.clone()), source_manager)
.expect("should build a session");
(Rc::new(Context::new(Rc::new(session))), emitter)
}
fn context_with_entrypoint(entrypoint: &str) -> Rc<Context> {
let options = Options {
entrypoint: Some(entrypoint.to_string()),
..Default::default()
};
let source_manager = alloc::sync::Arc::new(DefaultSourceManager::default());
let session =
Session::new(InputFile::empty(), alloc::boxed::Box::new(options), None, source_manager)
.expect("should build a session");
Rc::new(Context::new(Rc::new(session)))
}
fn context_with_entrypoint_and_test_harness(entrypoint: &str) -> Rc<Context> {
let options = Options {
entrypoint: Some(entrypoint.to_string()),
flags: midenc_session::CompileFlags::new(["--test-harness"])
.expect("the test-harness flag must parse"),
..Default::default()
};
let source_manager = alloc::sync::Arc::new(DefaultSourceManager::default());
let session =
Session::new(InputFile::empty(), alloc::boxed::Box::new(options), None, source_manager)
.expect("should build a session");
Rc::new(Context::new(Rc::new(session)))
}
fn summarize(component: &MasmComponent) -> String {
format!(
"id: {:?}\nroot: {}\ninit: {:?}\nentrypoint: {:?}\nheap_base: {}\nstack_pointer: \
{:?}\nrodata: {:?}\n{component}",
component.id.as_ref().map(|id| id.to_string()),
component.root,
component.init,
component.entrypoint,
component.heap_base,
component.stack_pointer,
component.rodata,
)
}
fn mark_start_function(context: &Rc<Context>, root: OperationRef, name: &str) -> FunctionRef {
let mut found = None;
root.borrow().prewalk_all(|op| {
let Some(function) = op.downcast_ref::<builtin::Function>() else {
return;
};
if function.name().as_str() == name {
assert!(found.is_none(), "fixture function name '{name}' must be unique");
found = Some(function.as_function_ref());
}
});
let mut function = found.unwrap_or_else(|| panic!("fixture must define function '{name}'"));
let marker = context.create_attribute::<UnitAttr, _>(());
function
.borrow_mut()
.as_operation_mut()
.set_attribute(midenc_dialect_hir::WASM_COMPONENT_START_ATTR, marker);
function
}
fn exec_paths(block: &masm::Block) -> Vec<String> {
block
.iter()
.filter_map(|op| match op {
masm::Op::Inst(inst) => match inst.inner() {
masm::Instruction::Exec(masm::InvocationTarget::Path(path)) => {
Some(path.inner().as_str().to_string())
}
masm::Instruction::Exec(masm::InvocationTarget::Symbol(name)) => {
Some(name.as_str().to_string())
}
_ => None,
},
_ => None,
})
.collect()
}
fn component_init(component: &MasmComponent) -> &masm::Procedure {
component.modules[0]
.procedures()
.find(|procedure| procedure.name().as_str() == "init")
.expect("a marked component must define `init`")
}
#[test]
fn a_world_holding_one_component_lowers_as_that_component() {
let context = Rc::new(Context::default());
let from_world =
lower_world(parse_world(&context, WORLD)).expect("a single-component world lowers");
let context = Rc::new(Context::default());
let op = parse(&context, COMPONENT);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture parses as a component"));
let analysis_manager = AnalysisManager::new(op, None);
let from_component = component
.borrow()
.to_masm_component(analysis_manager)
.expect("and so does the component on its own");
assert_eq!(
summarize(&from_world),
summarize(&from_component),
"a world holding one component must lower to what that component lowers to"
);
}
#[test]
fn a_component_lowers_rooted_at_its_own_id() {
let context = Rc::new(Context::default());
let op = parse(&context, COMPONENT);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture parses as a component"));
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("the component lowers");
let id = lowered.id.as_ref().expect("a component knows its own id");
assert_eq!(id.to_string(), "hir_ns:test@1.0.0");
assert_eq!(
lowered.root.to_string(),
"::\"hir_ns:test@1.0.0\"",
"a component's Miden Assembly is rooted at its id, as one quoted path component"
);
assert!(
format!("{lowered}").contains("main"),
"and its function must have been lowered: {lowered}"
);
}
#[test]
fn a_marked_start_is_the_final_component_initialization_step() {
let context = Rc::new(Context::default());
let op = parse(&context, COMPONENT);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
mark_start_function(&context, op, "main");
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("a component whose only initialization is its start must lower");
let init = component_init(&lowered);
let execs = exec_paths(init.body());
assert_eq!(
execs.last().map(String::as_str),
Some("::\"hir_ns:test@1.0.0\"::test::main"),
"the start function must be the final `exec` in `init`: {execs:?}"
);
assert!(
init.invoked().any(|invoke| {
invoke.kind == masm::InvokeKind::Exec
&& invoke.target.unwrap_path().as_str() == "::\"hir_ns:test@1.0.0\"::test::main"
}),
"the start edge must be present in `init`'s invocation metadata"
);
let lowered_start = lowered
.modules
.iter()
.find(|module| module.path().as_str().ends_with("::test"))
.and_then(|module| {
module.procedures().find(|procedure| procedure.name().as_str() == "main")
})
.expect("the marked function must still be lowered");
assert!(
!lowered_start.has_attribute(midenc_dialect_hir::WASM_COMPONENT_START_ATTR),
"the HIR-only marker must be consumed rather than copied to MASM"
);
}
#[test]
fn a_marked_start_remains_resolvable_when_a_synthetic_wrapper_is_rebased() {
let context = Rc::new(Context::default());
let op = parse(&context, COMPONENT);
let mut component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
component.borrow_mut().mark_synthetic_wrapper();
mark_start_function(&context, op, "main");
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("a marked synthetic wrapper must lower");
let target = library_target("rebased");
let sources = lowered
.source_inputs(&target, context.session())
.expect("the marked wrapper must produce rebased assembler inputs");
let mut assembler = miden_assembly::Assembler::new(context.session().source_manager.clone());
assembler
.link_package(crate::intrinsics::load(), miden_assembly::Linkage::Static)
.expect("the compiler intrinsics should link");
assembler
.assemble_library("rebased", sources.root, sources.support)
.expect("rebasing must update the start target recorded in `init`");
}
fn component_with_global_table_and_start() -> String {
WORLD_WITH_A_PRIVATE_TABLE_CALLEE.replace(
"builtin.function private extern(\"C\") @private_callee() {",
"builtin.global_variable private @g : i32 {\n builtin.ret_imm 1 : i32;\n };\n\n builtin.function public extern(\"C\") @component_start() {\n builtin.ret;\n };\n\n builtin.function private extern(\"C\") @private_callee() {",
)
}
#[test]
fn component_start_runs_after_globals_and_function_tables() {
let context = Rc::new(Context::default());
let world = parse_world(&context, &component_with_global_table_and_start());
mark_start_function(&context, world.as_operation_ref(), "component_start");
let lowered = lower_world(world).expect("a fully initialized marked component must lower");
let execs = exec_paths(component_init(&lowered).body());
let table = execs
.iter()
.position(|target| target.ends_with("::wasm::__init_function_table"))
.expect("function-table initialization must be invoked");
let start = execs
.iter()
.position(|target| target.ends_with("::wasm::component_start"))
.expect("the component start must be invoked");
assert!(table < start, "table initialization must precede start: {execs:?}");
assert_eq!(start + 1, execs.len(), "nothing may execute after start in `init`: {execs:?}");
}
const COMPONENT_WITH_CANONICAL_ENTRYPOINT_AND_START: &str = r#"
builtin.component private @"hir_ns:test@1.0.0" {
builtin.function public extern("component-model") @entry() {
builtin.ret;
};
builtin.module private @core {
builtin.global_variable private @g : i32 {
builtin.ret_imm 1 : i32;
};
builtin.function public extern("C") @component_start() {
builtin.ret;
};
builtin.function public extern("C") @core_entry() {
builtin.ret;
};
};
};
"#;
#[test]
fn marked_component_uses_private_no_init_canonical_executable_entrypoint() {
let context = context_with_entrypoint_and_test_harness("\"hir_ns:test@1.0.0\"::entry");
let op = parse(&context, COMPONENT_WITH_CANONICAL_ENTRYPOINT_AND_START);
mark_start_function(&context, op, "component_start");
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("a marked canonical executable entrypoint must lower through its private copy");
let public_entry = lowered.modules[0]
.procedures()
.find(|procedure| procedure.name().as_str() == "entry")
.expect("the public canonical wrapper must remain defined");
assert_eq!(
exec_paths(public_entry.body()).first().map(String::as_str),
Some("init"),
"fresh-context calls through the public wrapper must still initialize"
);
let private_entry = lowered
.executable_entrypoint_without_init
.as_ref()
.expect("the marked executable must carry a private no-init entry body");
assert_eq!(private_entry.name().as_str(), EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC);
assert_eq!(private_entry.visibility(), masm::Visibility::Private);
assert!(
!exec_paths(private_entry.body()).iter().any(|target| target == "init"),
"the executable-only entry body must not repeat component initialization"
);
let target = midenc_session::miden_project::Target::executable(
"component-start",
Uri::new("component-start.hir"),
);
let sources = lowered
.source_inputs(&target, context.session())
.expect("the marked canonical executable should generate main and its private entry");
let private_entry = sources
.root
.procedures()
.find(|procedure| procedure.name().as_str() == EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC)
.expect("the no-init entry body must be private to the executable module");
assert_eq!(private_entry.visibility(), masm::Visibility::Private);
let main = sources
.root
.procedures()
.find(|procedure| procedure.name().is_main())
.expect("the executable root must define main");
let init = main
.body()
.iter()
.position(|op| {
matches!(
op,
masm::Op::Inst(inst)
if matches!(
inst.inner(),
masm::Instruction::Exec(masm::InvocationTarget::Path(path))
if path.inner().as_str() == "::\"hir_ns:test@1.0.0\"::init"
)
)
})
.expect("generated main must retain component init");
let harness = main
.body()
.iter()
.position(|op| {
matches!(
op,
masm::Op::Inst(inst) if matches!(inst.inner(), masm::Instruction::AdvPush)
)
})
.expect("the fixture enables test-harness initialization");
let entry = main
.body()
.iter()
.position(|op| {
matches!(
op,
masm::Op::Inst(inst)
if matches!(
inst.inner(),
masm::Instruction::Exec(masm::InvocationTarget::Symbol(name))
if name.as_str() == EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC
)
)
})
.expect("generated main must invoke the private no-init entry body");
assert!(
init < harness && harness < entry,
"generated main must preserve init -> harness -> entry ordering"
);
}
#[test]
fn unmarked_canonical_executable_keeps_the_existing_entrypoint_path() {
let context = context_with_entrypoint("\"hir_ns:test@1.0.0\"::entry");
let lowered = lower_component(&context, COMPONENT_WITH_CANONICAL_ENTRYPOINT_AND_START)
.expect("the unmarked canonical executable must remain supported");
assert!(lowered.executable_entrypoint_without_init.is_none());
let target = midenc_session::miden_project::Target::executable(
"component-start",
Uri::new("component-start.hir"),
);
let sources = lowered.source_inputs(&target, context.session()).unwrap();
let main = sources.root.procedures().find(|procedure| procedure.name().is_main()).unwrap();
let execs = exec_paths(main.body());
assert!(execs.iter().any(|target| target.ends_with("::init")));
assert!(execs.iter().any(|target| target.ends_with("::entry")));
assert!(!execs.iter().any(|target| target == EXECUTABLE_ENTRYPOINT_WITHOUT_INIT_PROC));
}
const COMPONENT_WITH_NESTED_CANONICAL_ENTRYPOINT_AND_START: &str = r#"
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @api {
builtin.function public extern("component-model") @entry() {
builtin.ret;
};
};
builtin.module private @core {
builtin.function public extern("C") @component_start() {
builtin.ret;
};
};
};
"#;
#[test]
fn nested_canonical_entrypoint_cannot_bypass_marked_component_rejection() {
let context = context_with_entrypoint("\"hir_ns:test@1.0.0\"::api::entry");
let op = parse(&context, COMPONENT_WITH_NESTED_CANONICAL_ENTRYPOINT_AND_START);
mark_start_function(&context, op, "component_start");
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
let analysis_manager = AnalysisManager::new(op, None);
let err = match component.borrow().to_masm_component(analysis_manager) {
Ok(_) => panic!("a nested canonical entrypoint has ambiguous init ownership"),
Err(err) => err,
};
assert!(err.to_string().contains("must be defined directly"), "{err}");
}
const WORLD_WITH_SUPPORTING_CANONICAL_ENTRYPOINT_AND_START: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @core {
builtin.function public extern("C") @component_start() {
builtin.ret;
};
};
};
builtin.module public @supporting {
builtin.function public extern("component-model") @entry() {
builtin.ret;
};
};
};
"#;
#[test]
fn supporting_canonical_entrypoint_cannot_bypass_marked_component_rejection() {
let context = context_with_entrypoint("supporting::entry");
let world = parse_world(&context, WORLD_WITH_SUPPORTING_CANONICAL_ENTRYPOINT_AND_START);
mark_start_function(&context, world.as_operation_ref(), "component_start");
let err = match lower_world(world) {
Ok(_) => {
panic!("a supporting canonical entrypoint would still initialize the component twice")
}
Err(err) => err,
};
assert!(err.to_string().contains("initialization twice"), "{err}");
}
#[test]
fn generated_executable_main_owns_marked_core_entrypoint_initialization_once() {
let context = context_with_entrypoint("\"hir_ns:test@1.0.0\"::core::core_entry");
let op = parse(&context, COMPONENT_WITH_CANONICAL_ENTRYPOINT_AND_START);
let component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
mark_start_function(&context, op, "component_start");
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("a marked core-C executable entrypoint remains supported");
let target = midenc_session::miden_project::Target::executable(
"component-start",
Uri::new("component-start.hir"),
);
let sources = lowered
.source_inputs(&target, context.session())
.expect("the marked executable should generate its main module");
let main = sources
.root
.procedures()
.find(|procedure| procedure.name().is_main())
.expect("the executable root should define main");
let execs = exec_paths(main.body());
let init = "::\"hir_ns:test@1.0.0\"::init";
let entry = "\"hir_ns:test@1.0.0\"::core::core_entry";
assert_eq!(execs.iter().filter(|target| target.as_str() == init).count(), 1);
let init = execs
.iter()
.position(|target| target == init)
.expect("generated main must invoke component init");
let entry = execs
.iter()
.position(|target| target == entry)
.unwrap_or_else(|| panic!("generated main must invoke the core entrypoint: {execs:?}"));
assert!(
init < entry,
"generated main must initialize once before executing the core entrypoint: {execs:?}"
);
}
#[test]
fn a_world_declaring_two_components_is_reported_as_unimplemented() {
let context = Rc::new(Context::default());
let op = parse(&context, TWO_COMPONENT_WORLD);
let world = op
.try_downcast_op::<builtin::World>()
.unwrap_or_else(|_| panic!("the fixture parses as a world"));
let err = lower_world(world)
.err()
.expect("lowering two components into one package is not implemented");
let msg = format!("{err}");
assert!(
msg.contains("lowering a world containing 2 components"),
"the report must say what it found, and how many of them: {msg}"
);
assert!(
msg.contains("not yet implemented"),
"and must read as a limitation of the compiler rather than a malformed input: {msg}"
);
}
#[test]
fn a_declaration_only_sibling_is_ignored() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, WORLD_WITH_DECLARATION_ONLY_SIBLING);
let with_sibling = lower_world(world).expect("a declaration-only sibling must not fail");
let context = Rc::new(Context::default());
let alone = lower_world(parse_world(&context, WORLD)).expect("and neither must its absence");
assert_eq!(
summarize(&with_sibling),
summarize(&alone),
"a sibling that only declares symbols contributes no Miden Assembly"
);
assert!(
emitter.captured().is_empty(),
"and it is ignored by design, so it must not be reported: {}",
emitter.captured()
);
}
#[test]
fn a_sibling_module_owning_no_memory_is_translated_beside_the_component() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, WORLD_WITH_SUPPORTING_SIBLING);
let lowered =
lower_world(world).expect("a supporting module beside a component must not fail the build");
assert!(
!context.session().diagnostics.has_errors(),
"and must not be reported as an error either"
);
assert!(
emitter.captured().is_empty(),
"a supporting module is translated now, so there is nothing to report: {}",
emitter.captured()
);
assert_eq!(
lowered.id.as_ref().map(|id| id.to_string()).as_deref(),
Some("hir_ns:test@1.0.0")
);
assert_eq!(lowered.root.to_string(), "::\"hir_ns:test@1.0.0\"");
assert_eq!(
module_paths(&lowered),
vec!["::\"hir_ns:test@1.0.0\"", "::\"hir_ns:test@1.0.0\"::test", "::supporting"],
"the sibling is a top-level module of its own, not a child of the component"
);
assert!(
format!("{lowered}").contains("sibling"),
"and its procedure must have been lowered: {lowered}"
);
let target = library_target("hir_ns:test@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
assert_eq!(sources.root.path().to_string(), "::\"hir_ns:test@1.0.0\"");
assert_eq!(
sources
.support
.iter()
.map(|module| module.path().to_string())
.collect::<Vec<_>>(),
vec!["::\"hir_ns:test@1.0.0\"::test", "::supporting"],
);
}
#[test]
fn a_sibling_module_defining_a_global_variable_is_diagnosed() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, &world_with_a_sibling_defining_a_global());
let lowered = lower_world(world).expect("and it must still not fail the build");
assert!(
!context.session().diagnostics.has_errors(),
"nor be reported as an error, which would reject a legitimate world"
);
let captured = emitter.captured();
assert!(
captured.contains("global variable"),
"the report must name the rule, not merely refuse: {captured}"
);
assert!(
captured.contains("component"),
"and must say who owns those items, which is what teaches the rule: {captured}"
);
assert!(
captured.contains("supporting"),
"and must name the item it left out: {captured}"
);
assert_eq!(
module_paths(&lowered),
vec!["::\"hir_ns:test@1.0.0\"", "::\"hir_ns:test@1.0.0\"::test"],
"a module that owns memory is left out of the generated package: {lowered}"
);
}
#[test]
fn a_sibling_module_declaring_a_global_variable_is_diagnosed() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, &world_with_a_sibling_declaring_a_global());
let lowered = lower_world(world).expect("and it must still not fail the build");
assert!(
emitter.captured().contains("global variable"),
"a declared global is still a global a component owns: {}",
emitter.captured()
);
assert_eq!(
module_paths(&lowered),
vec!["::\"hir_ns:test@1.0.0\"", "::\"hir_ns:test@1.0.0\"::test"],
);
}
#[test]
fn a_sibling_module_declaring_a_data_segment_is_diagnosed() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, WORLD_WITH_SUPPORTING_SIBLING);
define_a_data_segment_in(top_level_module(&world.borrow(), "supporting"));
let lowered = lower_world(world).expect("and it must still not fail the build");
assert!(!context.session().diagnostics.has_errors());
let captured = emitter.captured();
assert!(
captured.contains("data segment"),
"the report must name the rule, not merely refuse: {captured}"
);
assert!(captured.contains("component"), "and must say who owns those items: {captured}");
assert_eq!(
module_paths(&lowered),
vec!["::\"hir_ns:test@1.0.0\"", "::\"hir_ns:test@1.0.0\"::test"],
"a module that owns memory is left out of the generated package: {lowered}"
);
}
#[test]
fn a_supporting_sibling_does_not_move_when_the_component_is_re_rooted() {
let context = Rc::new(Context::default());
let world = parse_world(&context, &wrapper_world_with_a_supporting_sibling());
mark_as_synthetic_wrapper(world);
let lowered = lower_world(world).expect("a wrapper world with a sibling lowers");
assert_eq!(
lowered.root.to_string(),
"::\"root_ns:root@1.0.0\"",
"the fixture must really be the wrapper, or this test is about some other case"
);
let target = library_target("::example");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
assert_eq!(sources.root.path(), target.namespace.inner().as_ref());
assert_eq!(
sources
.support
.iter()
.map(|module| module.path().to_string())
.collect::<Vec<_>>(),
vec!["::example::test", "::supporting"],
"the component's own modules move with its root; the sibling, whose name the source \
declares and which was never under that root, does not"
);
}
#[test]
fn a_component_and_its_supporting_sibling_assemble() {
fn assembler(session: &Session) -> miden_assembly::Assembler {
let mut assembler = miden_assembly::Assembler::new(session.source_manager.clone());
assembler
.link_package(crate::intrinsics::load(), miden_assembly::Linkage::Static)
.expect("the compiler intrinsics should link");
assembler
}
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_CALLING_ITS_SUPPORTING_SIBLING);
let lowered = lower_world(world).expect("a supporting module beside a component lowers");
let target = library_target("hir_ns:test@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
assert!(
sources.support.iter().any(|module| module.path() == "::supporting"),
"the supporting module must be among the sources, or neither half proves anything"
);
assembler(context.session())
.assemble_library("hir_ns:test@1.0.0", sources.root, sources.support)
.unwrap_or_else(|err| {
panic!("a component and its supporting sibling should assemble: {err}")
});
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
let withheld = sources
.support
.into_iter()
.filter(|module| module.path() != "::supporting")
.collect::<Vec<_>>();
let err = assembler(context.session())
.assemble_library("hir_ns:test@1.0.0", sources.root, withheld)
.expect_err("without the supporting module, the component's call cannot resolve");
assert!(
format!("{err}").contains("undefined"),
"and it must fail for that reason, not some other: {err}"
);
}
#[test]
fn a_body_less_function_in_a_supporting_sibling_is_invalid_input() {
let (context, emitter) = capturing_context();
let world = parse_world(&context, &world_with_a_sibling_declaring_one_of_its_functions());
let err = lower_world(world)
.err()
.expect("a function with no body cannot be emitted as Miden Assembly");
let msg = format!("{err}");
assert!(
msg.contains("cannot emit masm for a function with no body"),
"the report must say what it cannot emit, and why there is nothing to emit: {msg}"
);
assert!(
msg.contains("nothing can provide its definition"),
"and must name the reason the input is invalid rather than unsupported: {msg}"
);
assert!(
!msg.contains("not yet implemented"),
"it is not a limitation of the compiler, which is what `too_many_components` is: {msg}"
);
assert!(
emitter.captured().is_empty(),
"and the module itself owns no memory, so the sibling rule has no quarrel with it and \
must not add one: {}",
emitter.captured()
);
let context = Rc::new(Context::default());
let lowered = lower_world(parse_world(&context, WORLD_WITH_SUPPORTING_SIBLING))
.expect("a sibling whose functions all have bodies is unaffected by this check");
assert!(
module_paths(&lowered).contains(&String::from("::supporting")),
"and is still translated beside the component: {lowered}"
);
}
#[test]
fn a_body_less_function_in_a_components_module_is_invalid_input() {
let context = Rc::new(Context::default());
let err = lower_component(&context, &component_with_a_declared_function_in_its_module())
.err()
.expect("the component path reaches the same check, by a different route");
let msg = format!("{err}");
assert!(
msg.contains("cannot emit masm for a function with no body"),
"and reports it identically, since it is the same defect in the input: {msg}"
);
let context = Rc::new(Context::default());
let lowered = lower_component(&context, COMPONENT)
.expect("a component whose functions all have bodies is unaffected by this check");
assert!(
format!("{lowered}").contains("main"),
"and its module's procedure is emitted as before: {lowered}"
);
}
#[test]
fn a_body_less_component_level_function_is_invalid_input() {
let context = Rc::new(Context::default());
let err = lower_component(&context, &component_with_a_declared_component_level_function())
.err()
.expect("a component-level function with no body cannot be emitted either");
assert!(
format!("{err}").contains("cannot emit masm for a function with no body"),
"whichever kind of item declares it, the answer is the same: {err}"
);
}
#[test]
fn a_world_of_modules_still_lowers_as_a_component_body() {
let context = Rc::new(Context::default());
let module = parse(&context, MODULE);
let lowered =
lower_world(anchoring_world(module)).expect("a world of modules lowers as it always did");
assert!(lowered.id.is_none(), "a world declares no component id of its own");
assert_eq!(
lowered.root.to_string(),
"::lib",
"its root is the single top-level namespace it holds"
);
assert!(
format!("{lowered}").contains("main"),
"and the module's function must have been lowered: {lowered}"
);
}
#[test]
fn a_world_of_several_modules_is_rooted_at_the_target_namespace() {
let context = Rc::new(Context::default());
let lowered = lower_world(parse_world(&context, &two_module_world()))
.expect("a world of several modules lowers");
assert_eq!(
lowered.root.to_string(),
"::init",
"lowering has no target to root at, so it still picks its placeholder"
);
let target = library_target("::example");
let sources = lowered
.source_inputs(&target, context.session())
.expect("and its source inputs are what the assembler is handed");
assert_eq!(
sources.root.path(),
target.namespace.inner().as_ref(),
"a world declaring no component has no identity of its own, so its root is the namespace \
its target names"
);
assert_eq!(
sources
.support
.iter()
.map(|module| module.path().to_string())
.collect::<Vec<_>>(),
vec!["::lib", "::second"],
"and the modules the world declares are siblings of the placeholder rather than children \
of it, so they do not move with it"
);
}
#[test]
fn a_world_of_one_module_already_at_its_targets_namespace_is_left_alone() {
let context = Rc::new(Context::default());
let module = parse(&context, MODULE);
let lowered = lower_world(anchoring_world(module)).expect("a world of one module lowers");
let emitted = format!("{}", lowered.modules[0]);
let target = library_target("::lib");
assert_eq!(
lowered.root.as_ref(),
target.namespace.inner().as_ref(),
"the module's own name and the target's namespace must really be the same path, or this \
test is about some other case"
);
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
assert_eq!(sources.root.path(), target.namespace.inner().as_ref());
assert_eq!(format!("{}", sources.root), emitted, "and nothing in it moved");
}
#[test]
fn a_world_holding_one_component_keeps_that_components_id() {
let context = Rc::new(Context::default());
let lowered =
lower_world(parse_world(&context, WORLD)).expect("a single-component world lowers");
let target = library_target("::example");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
assert_eq!(
sources.root.path().to_string(),
"::\"hir_ns:test@1.0.0\"",
"an authored component's root is its own library path, and a target named otherwise must \
fail the assembler's root-module check rather than be quietly accommodated"
);
}
#[test]
fn type_expr_from_hir_pointer_conversion_preserves_address_space() {
for addrspace in [masm::types::AddressSpace::Byte, masm::types::AddressSpace::Element] {
let ty = Type::from(PointerType::new_with_address_space(Type::U32, addrspace));
let masm::TypeExpr::Ptr(ptr) = masm::TypeExpr::from(ty) else {
panic!("expected pointer type expression");
};
assert_eq!(ptr.address_space(), addrspace);
}
}
#[derive(Clone, Copy, Debug)]
enum ProcedureRootCallerOwner {
Component,
Interface,
Module,
}
fn component_with_cross_module_procedure_root(
caller_owner: ProcedureRootCallerOwner,
callee_visibility: &str,
callee_first: bool,
) -> String {
let callee_module = format!(
r#" builtin.module private @callee_mod {{
builtin.function {callee_visibility} extern("C") @callee() {{
builtin.ret;
}};
}};"#
);
let caller_function = r#"builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root ::@"hir_ns:test@1.0.0"::@callee_mod::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};"#;
let caller = match caller_owner {
ProcedureRootCallerOwner::Component => format!(" {caller_function}"),
ProcedureRootCallerOwner::Interface => format!(
r#" builtin.interface @caller {{
{caller_function}
}};"#
),
ProcedureRootCallerOwner::Module => format!(
r#" builtin.module public @caller_mod {{
{caller_function}
}};"#
),
};
let (first, second) = if callee_first {
(callee_module.as_str(), caller.as_str())
} else {
(caller.as_str(), callee_module.as_str())
};
format!(
r#"builtin.component private @"hir_ns:test@1.0.0" {{
{first}
{second}
}};
"#
)
}
fn component_with_same_owner_private_procedure_root(
owner: ProcedureRootCallerOwner,
callee_first: bool,
) -> String {
let callee = r#"builtin.function private extern("C") @callee() {
builtin.ret;
};"#;
let root = r#"builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root @callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};"#;
let (first, second) = if callee_first {
(callee, root)
} else {
(root, callee)
};
let owner = match owner {
ProcedureRootCallerOwner::Component => format!(" {first}\n\n {second}"),
ProcedureRootCallerOwner::Interface => format!(
r#" builtin.interface @api {{
{first}
{second}
}};"#
),
ProcedureRootCallerOwner::Module => format!(
r#" builtin.module public @api {{
{first}
{second}
}};"#
),
};
format!(
r#"builtin.component private @"hir_ns:test@1.0.0" {{
{owner}
}};
"#
)
}
#[test]
fn cross_module_private_procedure_roots_are_rejected_for_every_owner_in_both_orders() {
for caller_owner in [
ProcedureRootCallerOwner::Component,
ProcedureRootCallerOwner::Interface,
ProcedureRootCallerOwner::Module,
] {
for callee_first in [true, false] {
let context = Rc::new(Context::default());
let source =
component_with_cross_module_procedure_root(caller_owner, "private", callee_first);
let err = legalize_and_lower_component(&context, &source)
.err()
.expect("a cross-module procedure_root must not target a private callee");
let message = err.to_string();
assert!(
message.contains("private callee")
&& message.contains("callee_mod/callee")
&& message.contains("not linkable from another Miden Assembly module"),
"owner: {caller_owner:?}, callee_first: {callee_first}, error: {message}"
);
}
}
}
#[test]
fn direct_lowering_reports_both_sides_of_a_private_cross_module_procedure_root() {
let context = Rc::new(Context::default());
let source = component_with_cross_module_procedure_root(
ProcedureRootCallerOwner::Module,
"private",
true,
);
let err = legalize_and_lower_component(&context, &source)
.err()
.expect("the compiler lowering path must preflight procedure_root visibility");
let labels = err
.labels()
.expect("the structured diagnostic must label both operations")
.filter_map(|label| label.label().map(str::to_string))
.collect::<Vec<_>>();
assert!(
labels
.iter()
.any(|label| { label == "this reference crosses a Miden Assembly module boundary" }),
"the root use must be the primary diagnostic site: {labels:?}"
);
assert!(
labels
.iter()
.any(|label| label == "this callee is private to its defining module"),
"the private callee must be identified as the secondary site: {labels:?}"
);
let help = err.help().expect("the diagnostic must explain the valid remedies").to_string();
assert!(
help.contains("declare the callee internal or public"),
"the diagnostic must explain both valid remedies: {help}"
);
}
const COMPONENT_WITH_PRIVATE_NESTED_PROCEDURE_ROOT_TARGET: &str = r#"
builtin.component private @"hir_ns:test@1.0.0" {
builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root ::@"hir_ns:test@1.0.0"::@outer::@hidden::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};
builtin.module public @outer {
builtin.module private @hidden {
builtin.function internal extern("C") @callee() {
builtin.ret;
};
};
};
};
"#;
#[test]
fn procedure_root_rejects_an_internal_callee_beneath_a_private_nested_module() {
let context = Rc::new(Context::default());
let err =
legalize_and_lower_component(&context, COMPONENT_WITH_PRIVATE_NESTED_PROCEDURE_ROOT_TARGET)
.err()
.expect("an internal procedure is not reachable through a private nested module");
let message = err.to_string();
assert!(
message.contains("callee")
&& message.contains("private module")
&& message.contains("hidden"),
"the diagnostic must identify the inaccessible module path: {message}"
);
}
#[test]
fn synthetic_wrapper_procedure_roots_use_effective_module_visibility() {
let context = Rc::new(Context::default());
let op = parse(&context, COMPONENT_WITH_PRIVATE_NESTED_PROCEDURE_ROOT_TARGET);
let mut component = op
.try_downcast_op::<builtin::Component>()
.unwrap_or_else(|_| panic!("the fixture should parse as a component"));
component.borrow_mut().mark_synthetic_wrapper();
let analysis_manager = AnalysisManager::new(op, None);
let lowered = component
.borrow()
.to_masm_component(analysis_manager)
.expect("a synthetic wrapper exposes its nested module path");
let exports = assembled_library_exports(&context, &lowered, "hir_ns:test@1.0.0");
assert!(exports.iter().any(|export| export.ends_with("root")), "exports: {exports:?}");
assert!(exports.iter().any(|export| export.ends_with("callee")), "exports: {exports:?}");
}
const COMPONENT_LESS_WORLD_WITH_COALESCED_PRIVATE_PROCEDURE_ROOT: &str = r#"
builtin.world {
builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root @only::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};
builtin.module public @only {
builtin.function private extern("C") @callee() {
builtin.ret;
};
};
};
"#;
#[test]
fn a_component_less_world_procedure_root_shares_its_single_modules_masm_root() {
let context = Rc::new(Context::default());
let lowered = legalize_and_lower_world(
&context,
COMPONENT_LESS_WORLD_WITH_COALESCED_PRIVATE_PROCEDURE_ROOT,
)
.expect("the world-level caller and sole module callee share one MASM module");
let exports = assembled_library_exports(&context, &lowered, "only");
assert_eq!(exports.len(), 1, "only the public root should be exported: {exports:?}");
assert!(exports[0].ends_with("root"), "unexpected package surface: {exports:?}");
}
const COMPONENT_LESS_WORLD_WITH_EFFECTIVELY_PUBLIC_NESTED_MODULES: &str = r#"
builtin.world {
builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root @only::@hidden::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};
builtin.module private @only {
builtin.module private @hidden {
builtin.function internal extern("C") @callee() {
builtin.ret;
};
};
};
};
"#;
#[test]
fn component_less_world_procedure_roots_use_effective_module_visibility() {
let context = Rc::new(Context::default());
let lowered = legalize_and_lower_world(
&context,
COMPONENT_LESS_WORLD_WITH_EFFECTIVELY_PUBLIC_NESTED_MODULES,
)
.expect("component-less world modules form the public artifact interface");
let exports = assembled_library_exports(&context, &lowered, "only");
assert!(exports.iter().any(|export| export.ends_with("root")), "exports: {exports:?}");
assert!(exports.iter().any(|export| export.ends_with("callee")), "exports: {exports:?}");
}
const COMPONENT_WITH_DEEP_CALLER_AND_PRIVATE_SIBLING_CALLEE_MODULE: &str = r#"
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module private @internal {
builtin.function internal extern("C") @callee() {
builtin.ret;
};
};
builtin.module public @api {
builtin.module public @deep {
builtin.function public extern("C") @root() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root ::@"hir_ns:test@1.0.0"::@internal::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};
};
};
};
"#;
#[test]
fn a_deep_procedure_root_caller_can_reach_its_ancestors_private_child() {
let context = Rc::new(Context::default());
let lowered = legalize_and_lower_component(
&context,
COMPONENT_WITH_DEEP_CALLER_AND_PRIVATE_SIBLING_CALLEE_MODULE,
)
.expect("a private child is visible to every descendant of its parent");
let exports = assembled_library_exports(&context, &lowered, "hir_ns:test@1.0.0");
assert_eq!(exports.len(), 1, "only the public root should be exported: {exports:?}");
assert!(exports[0].ends_with("root"), "unexpected package surface: {exports:?}");
}
const WORLD_WITH_OMITTED_INVALID_PROCEDURE_ROOT_USER: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @api {
builtin.function private extern("C") @callee() {
builtin.ret;
};
};
};
builtin.interface @omitted {
builtin.function public extern("C") @unused() -> (felt, felt, felt, felt) {
%r0, %r1, %r2, %r3 = hir.procedure_root ::@"hir_ns:test@1.0.0"::@api::@callee;
builtin.ret %r0, %r1, %r2, %r3 : (felt, felt, felt, felt);
};
};
};
"#;
#[test]
fn legalization_does_not_validate_procedure_roots_in_an_omitted_world_sibling() {
let (context, emitter) = capturing_context();
legalize_and_lower_world(&context, WORLD_WITH_OMITTED_INVALID_PROCEDURE_ROOT_USER)
.expect("an omitted sibling must not fail the selected component's build");
let captured = emitter.captured();
assert!(
captured.contains("this build omits"),
"the established omission warning must still be emitted: {captured}"
);
}
#[test]
fn cross_module_internal_procedure_roots_assemble_in_both_module_orders() {
for callee_first in [true, false] {
let context = Rc::new(Context::default());
let source = component_with_cross_module_procedure_root(
ProcedureRootCallerOwner::Component,
"internal",
callee_first,
);
let lowered = legalize_and_lower_component(&context, &source)
.expect("an explicitly internal cross-module procedure_root target must lower");
let exports = assembled_library_exports(&context, &lowered, "hir_ns:test@1.0.0");
assert_eq!(exports.len(), 1, "only the public root should be exported: {exports:?}");
assert!(exports[0].ends_with("root"), "unexpected package surface: {exports:?}");
}
}
#[test]
fn same_owner_private_procedure_roots_stay_private_in_both_orders() {
for owner in [ProcedureRootCallerOwner::Component, ProcedureRootCallerOwner::Module] {
for callee_first in [true, false] {
let context = Rc::new(Context::default());
let source = component_with_same_owner_private_procedure_root(owner, callee_first);
let lowered = legalize_and_lower_component(&context, &source)
.expect("a procedure_root may target a private callee in its own MASM module");
let exports = assembled_library_exports(&context, &lowered, "hir_ns:test@1.0.0");
assert_eq!(
exports.len(),
1,
"owner: {owner:?}, callee_first: {callee_first}, exports: {exports:?}"
);
assert!(
exports[0].ends_with("root"),
"owner: {owner:?}, callee_first: {callee_first}, exports: {exports:?}"
);
}
}
}
const WORLD_WITH_A_PRIVATE_MODULE_BEHIND_ITS_INTERFACE: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.function public extern("C") @entry() {
hir.exec ::@"hir_ns:test@1.0.0"::@test::@helper() : extern("C") () -> ();
builtin.ret;
};
builtin.module private @test {
builtin.function public extern("C") @helper() {
builtin.ret;
};
};
};
};
"#;
#[test]
fn a_private_module_is_not_part_of_the_package_surface() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_A_PRIVATE_MODULE_BEHIND_ITS_INTERFACE);
let lowered = lower_world(world).expect("a component with a private module lowers");
let target = library_target("hir_ns:test@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
let package = miden_assembly::Assembler::new(context.session().source_manager.clone())
.assemble_library("hir_ns:test@1.0.0", sources.root, sources.support)
.expect("a public interface calling into a private module assembles");
let exports = package
.manifest
.exports()
.map(|export| export.path().as_ref().as_str().to_string())
.collect::<Vec<_>>();
assert!(
exports.iter().any(|export| export.ends_with("entry")),
"the component-level function is the public surface, got exports: {exports:?}"
);
assert!(
!exports.iter().any(|export| export.contains("helper")),
"a public procedure of a private module must not be exported, got exports: {exports:?}"
);
}
fn world_with_an_authored_root_id() -> String {
WORLD_WITH_A_PRIVATE_MODULE_BEHIND_ITS_INTERFACE
.replace("hir_ns:test@1.0.0", "root_ns:root@1.0.0")
}
#[test]
fn an_authored_component_named_like_the_wrapper_keeps_private_modules_private() {
let context = Rc::new(Context::default());
let world = parse_world(&context, &world_with_an_authored_root_id());
let lowered = lower_world(world).expect("an authored root-named component lowers");
let target = library_target("root_ns:root@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
let package = miden_assembly::Assembler::new(context.session().source_manager.clone())
.assemble_library("root_ns:root@1.0.0", sources.root, sources.support)
.expect("it assembles");
let exports = package
.manifest
.exports()
.map(|export| export.path().as_ref().as_str().to_string())
.collect::<Vec<_>>();
assert!(
exports.iter().any(|export| export.ends_with("entry")),
"the component-level function is still the public surface, got exports: {exports:?}"
);
assert!(
!exports.iter().any(|export| export.contains("helper")),
"a private module of an authored component must not be exported, got: {exports:?}"
);
}
const WORLD_WITH_A_NESTED_MODULE: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @outer {
builtin.function public extern("C") @entry() {
builtin.ret;
};
builtin.module public @inner {
builtin.function public extern("C") @nested() {
builtin.ret;
};
};
};
};
};
"#;
#[test]
fn a_nested_module_is_lowered_at_its_own_path() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_A_NESTED_MODULE);
let lowered = lower_world(world).expect("a component with a nested module lowers");
let paths = lowered
.modules
.iter()
.map(|module| module.path().to_string())
.collect::<Vec<_>>();
assert!(
paths.iter().any(|path| path.ends_with("outer::inner")),
"the nested module must be lowered at its own path, got: {paths:?}"
);
}
fn world_with_a_private_nested_module() -> String {
WORLD_WITH_A_NESTED_MODULE
.replace("builtin.module public @inner", "builtin.module private @inner")
}
#[test]
fn a_private_nested_module_is_not_part_of_the_package_surface() {
let context = Rc::new(Context::default());
let world = parse_world(&context, &world_with_a_private_nested_module());
let lowered = lower_world(world).expect("a component with a private nested module lowers");
let target = library_target("hir_ns:test@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
let package = miden_assembly::Assembler::new(context.session().source_manager.clone())
.assemble_library("hir_ns:test@1.0.0", sources.root, sources.support)
.expect("a public module holding a private one assembles");
let exports = package
.manifest
.exports()
.map(|export| export.path().as_ref().as_str().to_string())
.collect::<Vec<_>>();
assert!(
exports.iter().any(|export| export.ends_with("entry")),
"the public outer module's procedure is the surface, got exports: {exports:?}"
);
assert!(
!exports.iter().any(|export| export.contains("nested")),
"a public procedure of a private *nested* module must not be exported, got: {exports:?}"
);
}
const WORLD_WITH_A_PRIVATE_TABLE_CALLEE: &str = r#"
builtin.world {
builtin.component private @"root_ns:root@1.0.0" {
builtin.module public @wasm {
builtin.function private extern("C") @private_callee() {
builtin.ret;
};
builtin.function_table private @tbl : 1 {
builtin.function_table_entry 0 @private_callee tag 1;
};
builtin.function public extern("C") @dispatch(%index: u32) {
hir.exec_indirect @tbl[%index] : extern("C") () -> () tag 1;
builtin.ret;
};
};
};
};
"#;
#[test]
fn a_private_table_callee_is_not_part_of_the_package_surface() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_A_PRIVATE_TABLE_CALLEE);
let lowered = lower_world(world).expect("a component with a private table callee lowers");
let target = library_target("root_ns:root@1.0.0");
let sources = lowered
.source_inputs(&target, context.session())
.expect("its source inputs are what the assembler is handed");
let mut assembler = miden_assembly::Assembler::new(context.session().source_manager.clone());
assembler
.link_package(crate::intrinsics::load(), miden_assembly::Linkage::Static)
.expect("the compiler intrinsics should link");
let package = assembler
.assemble_library("root_ns:root@1.0.0", sources.root, sources.support)
.expect("a table initialized from its callee's own module assembles");
let mut exports = package
.manifest
.exports()
.map(|export| export.path().as_ref().as_str().to_string())
.collect::<Vec<_>>();
exports.sort();
assert_eq!(
exports,
vec![
"::\"root_ns:root@1.0.0\"::init",
"::\"root_ns:root@1.0.0\"::wasm::__init_function_table",
"::\"root_ns:root@1.0.0\"::wasm::dispatch",
],
"the public surface is the author's `dispatch` plus the compiler's own initializers"
);
}
const WORLD_WITH_A_TABLE_CALLEE_IN_A_DECLARATION_ONLY_SIBLING: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @wasm {
builtin.function_table private @tbl : 1 {
builtin.function_table_entry 0 ::@external_dep::@sibling tag 1;
};
builtin.function public extern("C") @dispatch(%index: u32) {
hir.exec_indirect @tbl[%index] : extern("C") () -> () tag 1;
builtin.ret;
};
};
};
builtin.module public @external_dep {
builtin.function public extern("C") @sibling() {
};
};
};
"#;
#[test]
fn a_table_callee_in_an_unlowered_module_is_invalid_input() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_A_TABLE_CALLEE_IN_A_DECLARATION_ONLY_SIBLING);
let err = lower_world(world)
.err()
.expect("a table entry naming a callee with no definition must not lower");
let err = err.to_string();
assert!(
err.contains("sibling") && err.contains("external_dep"),
"the diagnostic must name the callee and the module it was expected in, got: {err}"
);
assert!(
err.contains("only declarations"),
"the diagnostic must say why there was no module to attach the code to, got: {err}"
);
}
const WORLD_WITH_TABLE_CALLEES_ACROSS_MODULES: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @a {
builtin.function private extern("C") @callee_a() {
builtin.ret;
};
builtin.function_table private @tbl : 3 {
builtin.function_table_entry 0 @callee_a tag 1;
builtin.function_table_entry 1 ::@"hir_ns:test@1.0.0"::@outer::@callee_b tag 1;
builtin.function_table_entry 2 ::@"hir_ns:test@1.0.0"::@outer::@inner::@callee_c tag 1;
};
builtin.function public extern("C") @dispatch(%index: u32) {
hir.exec_indirect @tbl[%index] : extern("C") () -> () tag 1;
builtin.ret;
};
};
builtin.module public @outer {
builtin.function private extern("C") @callee_b() {
builtin.ret;
};
builtin.module public @inner {
builtin.function private extern("C") @callee_c() {
builtin.ret;
};
};
};
};
};
"#;
const WORLD_WITH_AN_OVERWRITTEN_TABLE_SLOT: &str = r#"
builtin.world {
builtin.component private @"hir_ns:test@1.0.0" {
builtin.module public @a {
builtin.function private extern("C") @live() {
builtin.ret;
};
builtin.function_table private @tbl : 1 {
builtin.function_table_entry 0 ::@"hir_ns:test@1.0.0"::@z::@dead tag 1;
builtin.function_table_entry 0 @live tag 1;
};
builtin.function public extern("C") @dispatch(%index: u32) {
hir.exec_indirect @tbl[%index] : extern("C") () -> () tag 1;
builtin.ret;
};
};
builtin.module public @z {
builtin.function private extern("C") @dead(%v: u32) {
builtin.ret;
};
};
};
};
"#;
fn procrefs_in(body: &masm::Block) -> Vec<String> {
body.iter()
.filter_map(|op| match op {
masm::Op::Inst(inst) => match inst.inner() {
masm::Instruction::ProcRef(masm::InvocationTarget::Path(path)) => {
Some(path.inner().as_str().to_string())
}
_ => None,
},
_ => None,
})
.collect()
}
#[test]
fn a_dead_table_entry_is_not_written_to_its_slot() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_AN_OVERWRITTEN_TABLE_SLOT);
let lowered = lower_world(world).expect("a component with an overwritten table slot lowers");
let mut procrefs = Vec::new();
for module in lowered.modules.iter() {
for procedure in module.procedures() {
if procedure.name().as_str() == super::FUNCTION_TABLE_INIT_PROC {
procrefs.extend(procrefs_in(procedure.body()));
}
}
}
procrefs.sort();
assert_eq!(
procrefs,
vec!["::\"hir_ns:test@1.0.0\"::a::live"],
"only the entry that wins the slot may be written to it"
);
let owners = lowered
.modules
.iter()
.filter(|module| {
module
.procedures()
.any(|proc| proc.name().as_str() == super::FUNCTION_TABLE_INIT_PROC)
})
.map(|module| module.path().to_string())
.collect::<Vec<_>>();
assert_eq!(
owners,
vec!["::\"hir_ns:test@1.0.0\"::a"],
"a module whose only table entry is dead defines no initializer"
);
}
fn function_table_initializers_invoked(body: &masm::Block) -> Vec<String> {
let suffix = format!("::{}", super::FUNCTION_TABLE_INIT_PROC);
body.iter()
.filter_map(|op| match op {
masm::Op::Inst(inst) => match inst.inner() {
masm::Instruction::Exec(masm::InvocationTarget::Path(path)) => {
path.inner().as_str().strip_suffix(&suffix).map(String::from)
}
_ => None,
},
_ => None,
})
.collect()
}
#[test]
fn every_module_defining_a_table_callee_is_initialized_exactly_once() {
let context = Rc::new(Context::default());
let world = parse_world(&context, WORLD_WITH_TABLE_CALLEES_ACROSS_MODULES);
let lowered = lower_world(world).expect("a component with table callees across modules lowers");
let mut owners = lowered
.modules
.iter()
.filter(|module| {
module
.procedures()
.any(|proc| proc.name().as_str() == super::FUNCTION_TABLE_INIT_PROC)
})
.map(|module| module.path().to_string())
.collect::<Vec<_>>();
owners.sort();
assert_eq!(
owners,
vec![
"::\"hir_ns:test@1.0.0\"::a",
"::\"hir_ns:test@1.0.0\"::outer",
"::\"hir_ns:test@1.0.0\"::outer::inner",
],
"a module defines an initializer if and only if it defines a table callee"
);
let mut invoked = Vec::new();
for module in lowered.modules.iter() {
for procedure in module.procedures() {
if matches!(procedure.name().as_str(), "init" | super::FUNCTION_TABLE_INIT_PROC) {
invoked.extend(function_table_initializers_invoked(procedure.body()));
}
}
}
invoked.sort();
assert_eq!(invoked, owners, "every initializer must be reached exactly once");
let invoked_by = |module_path: &str, procedure_name: &str| -> Vec<String> {
let module = lowered
.modules
.iter()
.find(|module| module.path() == module_path)
.unwrap_or_else(|| panic!("no module lowered at '{module_path}'"));
let procedure = module
.procedures()
.find(|procedure| procedure.name().as_str() == procedure_name)
.unwrap_or_else(|| panic!("no '{procedure_name}' in '{module_path}'"));
let mut invoked = function_table_initializers_invoked(procedure.body());
invoked.sort();
invoked
};
assert_eq!(
invoked_by("::\"hir_ns:test@1.0.0\"", "init"),
vec!["::\"hir_ns:test@1.0.0\"::a", "::\"hir_ns:test@1.0.0\"::outer"],
"`init` reaches the outermost initializers, and only those"
);
assert_eq!(
invoked_by("::\"hir_ns:test@1.0.0\"::outer", super::FUNCTION_TABLE_INIT_PROC),
vec!["::\"hir_ns:test@1.0.0\"::outer::inner"],
"a module's initializer reaches the initializers of the modules nested within it"
);
for leaf in ["::\"hir_ns:test@1.0.0\"::a", "::\"hir_ns:test@1.0.0\"::outer::inner"] {
assert!(
invoked_by(leaf, super::FUNCTION_TABLE_INIT_PROC).is_empty(),
"an initializer with nothing nested within it reaches no other, but '{leaf}' did"
);
}
}
#[test]
fn frame_base_locals_use_the_locked_debugger_encoding() {
use miden_core::{Felt, serde::Serializable};
let expression = Expression::with_ops(vec![ExpressionOp::FrameBase {
base: FrameBase::Local(2),
byte_offset: 28,
}]);
let location = DebugVarLocation::Expression(expression.to_bytes());
let patched = patch_debug_var_location(&location, 8, None);
let DebugVarLocation::FrameBase {
global_index,
byte_offset,
} = patched
else {
panic!("local frame bases must use the debugger-supported location variant");
};
assert_eq!(
global_index,
FRAME_BASE_LOCAL_MARKER | u32::from(u16::from_le_bytes((-6i16).to_le_bytes()))
);
assert_eq!(byte_offset, 28);
let value = miden_debug::resolve_variable_value(
&DebugVarLocation::FrameBase {
global_index,
byte_offset,
},
&[],
|address| (address == 32).then_some(Felt::new(13).unwrap()),
|offset| (offset == -6).then_some(Felt::new(100).unwrap()),
);
assert_eq!(value, Some(Felt::new(13).unwrap()));
}
#[test]
fn unrepresentable_nested_frame_bases_kill_the_active_debugger_location() {
use miden_debug::processor::trace::RowIndex;
for stack_pointer_addr in [None, Some(1 << 31)] {
let kill = patch_nested_frame_base(stack_pointer_addr);
assert_eq!(kill.value_location(), &debug_var_kill_location());
let events = Rc::new(RefCell::new(BTreeMap::new()));
let mut tracker = miden_debug::DebugVarTracker::new(events);
tracker.record_events(
RowIndex::from(1),
vec![masm::DebugVarInfo::new(
"x",
DebugVarLocation::Const(miden_core::Felt::new(7).unwrap()),
)],
);
tracker.record_events(RowIndex::from(2), vec![kill]);
tracker.update_to_cycle(RowIndex::from(1));
assert!(tracker.get_variable("x").is_some());
tracker.update_to_cycle(RowIndex::from(2));
assert!(tracker.get_variable("x").is_none());
}
}
#[test]
fn out_of_range_fmp_offsets_become_debugger_kills() {
use miden_core::serde::Serializable;
let positive_frame_base = DebugVarLocation::Expression(
Expression::with_ops(vec![ExpressionOp::FrameBase {
base: FrameBase::Local(i16::MAX as u32 + 1),
byte_offset: 0,
}])
.to_bytes(),
);
let negative_frame_base = DebugVarLocation::Expression(
Expression::with_ops(vec![ExpressionOp::FrameBase {
base: FrameBase::Local(0),
byte_offset: 0,
}])
.to_bytes(),
);
for (location, aligned_num_locals) in [
(DebugVarLocation::Local(i16::MIN), 1),
(positive_frame_base, 0),
(negative_frame_base, i16::MAX as u16 + 2),
] {
assert_eq!(
patch_debug_var_location(&location, aligned_num_locals, None),
debug_var_kill_location()
);
}
}
#[test]
fn final_debugger_locations_and_opaque_expressions_are_preserved() {
use miden_core::serde::Serializable;
let opaque = DebugVarLocation::Expression(
Expression::with_ops(vec![ExpressionOp::Address { address: 7 }, ExpressionOp::Deref])
.to_bytes(),
);
let kill = debug_var_kill_location();
for location in [
DebugVarLocation::Stack(0),
DebugVarLocation::Memory(1),
DebugVarLocation::Const(miden_core::Felt::new(2).unwrap()),
opaque,
kill,
] {
assert_eq!(patch_debug_var_location(&location, 8, None), location);
}
}
fn patch_nested_frame_base(stack_pointer_addr: Option<u32>) -> masm::DebugVarInfo {
let span = SourceSpan::default();
let debug_var = masm::DebugVarInfo::new(
"x",
DebugVarLocation::FrameBase {
global_index: 0,
byte_offset: -4,
},
);
let nested = masm::Block::new(
span,
vec![masm::Op::Inst(Span::new(span, masm::Instruction::DebugVar(debug_var)))],
);
let mut block = masm::Block::new(span, vec![masm::Op::While { span, body: nested }]);
patch_debug_var_locals_in_block(&mut block, 0, stack_pointer_addr);
let masm::Op::While { body, .. } = block.iter().next().unwrap() else {
unreachable!();
};
let masm::Op::Inst(inst) = body.iter().next().unwrap() else {
unreachable!();
};
let masm::Instruction::DebugVar(info) = inst.inner() else {
unreachable!();
};
info.clone()
}