Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Implemented project assembly ([#2877](https://github.com/0xMiden/miden-vm/pull/2877)).
- Added `FastProcessor::into_parts()` to extract advice provider, memory, and precompile transcript after step-based execution ([#2901](https://github.com/0xMiden/miden-vm/pull/2901)).
- Fixed assembler panics caused by mixing modules parsed with one source manager into an assembler created with another source manager; these cases now return a structured linker error instead ([#2987](https://github.com/0xMiden/miden-vm/pull/2987)).

## 0.22.0 (2026-03-18)

Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions crates/assembly-syntax/src/ast/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ pub struct Module {
pub(crate) items: Vec<Export>,
/// AdviceMap that this module expects to be loaded in the host before executing.
pub(crate) advice_map: AdviceMap,
/// The source file from which this module was parsed, if any.
///
/// Programmatically-constructed modules do not have a backing source file.
source_file: Option<Arc<SourceFile>>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two issues with this:

  1. Programmatically-constructed modules can absolutely have a backing source file - it just means that the source language was not Miden Assembly (or that the AST was literally constructed by hand for a test, but that's not the important case).
  2. Source spans within a Module do not have to come from the same source file, and in fact that is the common case when compiling from Rust source code currently.

}

/// Constants
Expand Down Expand Up @@ -153,6 +157,7 @@ impl Module {
kind,
items: Default::default(),
advice_map: Default::default(),
source_file: None,
}
}

Expand All @@ -173,6 +178,12 @@ impl Module {
self
}

/// Associates the source file from which this module was parsed.
pub(crate) fn with_source_file(mut self, source_file: Arc<SourceFile>) -> Self {
self.source_file = Some(source_file);
self
}

/// Sets the [Path] for this module
pub fn set_path(&mut self, path: impl AsRef<Path>) {
self.path = path.as_ref().to_path_buf();
Expand Down Expand Up @@ -440,6 +451,11 @@ impl Module {
&self.advice_map
}

/// Returns the source file from which this module was parsed, if any.
pub fn source_file(&self) -> Option<&Arc<SourceFile>> {
self.source_file.as_ref()
}

/// Get an iterator over the constants defined in this module.
pub fn constants(&self) -> impl Iterator<Item = &Constant> + '_ {
self.items.iter().filter_map(|item| match item {
Expand Down
6 changes: 5 additions & 1 deletion crates/assembly-syntax/src/sema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ pub fn analyze(
let mut analyzer = AnalysisContext::new(source.clone(), source_manager);
analyzer.set_warnings_as_errors(warnings_as_errors);

let mut module = Box::new(Module::new(kind, path).with_span(source.source_span()));
let mut module = Box::new(
Module::new(kind, path)
.with_source_file(source.clone())
.with_span(source.source_span()),
);

let mut forms = VecDeque::from(forms);
let mut enums = SmallVec::<[EnumType; 1]>::new_const();
Expand Down
7 changes: 7 additions & 0 deletions crates/assembly/src/linker/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ pub enum LinkerError {
prev_values: Vec<Felt>,
new_values: Vec<Felt>,
},
#[error(
"source manager mismatch: module '{path}' was parsed with a different source manager than the one used by the assembler"
)]
#[diagnostic(help(
"ensure the module is parsed with the same source manager that is passed to the assembler"
))]
SourceManagerMismatch { path: Arc<Path> },
#[error("undefined type alias")]
#[diagnostic()]
UndefinedType {
Expand Down
15 changes: 14 additions & 1 deletion crates/assembly/src/linker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use miden_assembly_syntax::{
self, Alias, AttributeSet, GlobalItemIndex, InvocationTarget, InvokeKind, ItemIndex,
Module, ModuleIndex, Path, SymbolResolution, Visibility, types,
},
debuginfo::{SourceManager, SourceSpan, Span, Spanned},
debuginfo::{SourceFile, SourceManager, SourceSpan, Span, Spanned},
library::{ItemInfo, ModuleInfo},
};
use miden_core::{Word, advice::AdviceMap, program::Kernel};
Expand All @@ -74,6 +74,13 @@ use self::{
resolver::*,
};

fn source_manager_owns_file(source_manager: &dyn SourceManager, file: &SourceFile) -> bool {
match source_manager.get(file.id()) {
Ok(found) => core::ptr::addr_eq(Arc::as_ptr(&found), file),
Err(_) => false,
}
}

/// Represents the current status of a symbol in the state of the [Linker]
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum LinkStatus {
Expand Down Expand Up @@ -277,6 +284,12 @@ impl Linker {
pub fn link_module(&mut self, module: &mut Module) -> Result<ModuleIndex, LinkerError> {
log::debug!(target: "linker", "adding unprocessed module {}", module.path());

if let Some(source_file) = module.source_file()
&& !source_manager_owns_file(self.source_manager.as_ref(), source_file)
{
return Err(LinkerError::SourceManagerMismatch { path: module.path().into() });
}

let is_duplicate = self.find_module_index(module.path()).is_some();
if is_duplicate {
return Err(LinkerError::DuplicateModule { path: module.path().into() });
Expand Down
91 changes: 86 additions & 5 deletions crates/assembly/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ use std::{
};

use miden_assembly_syntax::{
MAX_REPEAT_COUNT, ast::Path, diagnostics::WrapErr, library::LibraryExport,
MAX_REPEAT_COUNT,
ast::{Block, Instruction, Op, Path, Procedure, Visibility},
debuginfo::{DefaultSourceManager, SourceManager, Span},
diagnostics::WrapErr,
library::LibraryExport,
};
use miden_core::{
Felt, Word, assert_matches,
Expand Down Expand Up @@ -4115,7 +4119,7 @@ fn vendoring() -> TestResult {
let mod1 = mod_parser
.parse(PathBuf::new("test::mod1").unwrap(), source, context.source_manager())
.unwrap();
Assembler::default().assemble_library([mod1]).unwrap()
Assembler::new(context.source_manager()).assemble_library([mod1]).unwrap()
};

let lib = {
Expand All @@ -4124,7 +4128,7 @@ fn vendoring() -> TestResult {
.parse(PathBuf::new("test::mod2").unwrap(), source, context.source_manager())
.unwrap();

let mut assembler = Assembler::default();
let mut assembler = Assembler::new(context.source_manager());
assembler.link_static_library(vendor_lib)?;
assembler.assemble_library([mod2]).unwrap()
};
Expand All @@ -4140,7 +4144,7 @@ fn vendoring() -> TestResult {
let expected_lib = {
let source = source_file!(&context, "pub proc foo push.1 end");
let mod2 = mod_parser.parse("test::expected", source, context.source_manager()).unwrap();
Assembler::default().assemble_library([mod2]).unwrap()
Assembler::new(context.source_manager()).assemble_library([mod2]).unwrap()
};

// 3. Verify that the expected library (which has push.1) has AssemblyOps
Expand Down Expand Up @@ -5097,7 +5101,7 @@ fn test_assembler_debug_info_present() {
let module = parse_module!(&context, "test::foo", source);

// Test: With debug mode always enabled (issue #1821), debug info should always be present
let assembler = Assembler::default();
let assembler = Assembler::new(context.source_manager());
let library = assembler.assemble_library([module]).unwrap();
let mast_forest = library.mast_forest();

Expand Down Expand Up @@ -5789,3 +5793,80 @@ fn test_linking_recursive_expansion_via_renamed_aliases() -> TestResult {

Ok(())
}

#[test]
fn mismatched_source_manager_caught_before_lowering() {
let sm_a: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let _dummy = Module::parser(ModuleKind::Library)
.parse_str("lib::dummy", "pub proc dummy\n nop\nend", sm_a.clone())
.unwrap();

let assembler = Assembler::new(sm_a);

let sm_b: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
let module = Module::parser(ModuleKind::Library)
.parse_str("lib::external", "@locals(4) pub proc bar\n loc_loadw_be.2\nend", sm_b)
.unwrap();

let result = assembler.assemble_library([module]);
let err = result.expect_err("should fail with source manager mismatch");
let msg = err.to_string();
assert!(msg.contains("source manager mismatch"), "unexpected error: {msg}");
}

#[test]
fn issue_2778_parser_api_source_manager_mismatch_is_reported_without_panic() {
use std::panic::{AssertUnwindSafe, catch_unwind};

let assembler_source_manager: Arc<dyn SourceManager> =
Arc::new(DefaultSourceManager::default());
let parser_source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());

let source = r#"
use miden::protocol::output_note

@locals(1)
pub proc create_withdraw_return_note(tag: felt, note_type: felt, recipient: word, amount_0_out: felt, amount_1_out: felt)
# loc.0: note_id
# => [tag, aux, note_type, execution_hint, PAYBACK_NOTE_RECIPIENT]
exec.output_note::create_note
# => [note_id]
loc_store.0
# => []
end
"#;
let module = Module::parser(ModuleKind::Library)
.parse_str("test::lib", source, parser_source_manager)
.unwrap();

let assembled = catch_unwind(AssertUnwindSafe(|| {
Assembler::new(assembler_source_manager).assemble_library([module])
}));

assert!(
assembled.is_ok(),
"assembler panicked while reporting a source manager mismatch"
);
let err = assembled.unwrap().expect_err("should fail with source manager mismatch");
let rendered = err.to_string();
assert!(rendered.contains("source manager mismatch"), "unexpected error: {rendered}");
}

#[test]
fn programmatic_module_without_source_file_assembles_ok() -> TestResult {
let context = TestContext::default();

let mut module = Module::new(ModuleKind::Library, "lib::programmatic");
let nop_body = Block::new(Default::default(), vec![Op::Inst(Span::unknown(Instruction::Nop))]);
let procedure = Procedure::new(
Default::default(),
Visibility::Public,
"nop_proc".parse().unwrap(),
0,
nop_body,
);
module.define_procedure(procedure, context.source_manager()).unwrap();

context.assemble_library([alloc::boxed::Box::new(module)])?;
Ok(())
}
12 changes: 10 additions & 2 deletions crates/debug-types/src/source_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,20 @@ impl SourceFile {
Some(SourceSpan::at(self.id, offset.0))
}

/// Get a [FileLineCol] equivalent to the start of the given [SourceSpan]
pub(crate) fn try_location(&self, span: SourceSpan) -> Option<FileLineCol> {
if span.source_id() != self.id {
return None;
}

self.content.location(ByteIndex(span.into_range().start))
}

/// Get a [FileLineCol] equivalent to the start of the given [SourceSpan]
pub fn location(&self, span: SourceSpan) -> FileLineCol {
assert_eq!(span.source_id(), self.id, "mismatched source ids");

self.content
.location(ByteIndex(span.into_range().start))
self.try_location(span)
.expect("invalid source span: starting byte is out of bounds")
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/debug-types/src/source_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ impl DefaultSourceManagerImpl {
self.files
.get(span.source_id())
.ok_or(SourceManagerError::InvalidSourceId)
.map(|file| file.location(span))
.and_then(|file| file.try_location(span).ok_or(SourceManagerError::InvalidBounds))
}

fn location_to_span(&self, loc: Location) -> Option<SourceSpan> {
Expand Down
Loading