diff --git a/CHANGELOG.md b/CHANGELOG.md index 427dc07adb..264fc8b689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - Hardened MAST forest and package byte-slice deserialization against fuzzed length fields ([#3088](https://github.com/0xMiden/miden-vm/pull/3088)). - [BREAKING] Bounded the live advice map by total field elements during execution; advice-provider setup now returns an error when initial advice exceeds this limit ([#3085](https://github.com/0xMiden/miden-vm/pull/3085)). - Rejected empty kernel packages before linking so malformed dependency metadata returns a structured package error instead of reaching the linker's non-empty-kernel assertion ([#3082](https://github.com/0xMiden/miden-vm/pull/3082)). +- [BREAKING] Fixed project artifact reuse to ignore unrelated manifest fields, rejected private cross-module imports, and kept signature-only type imports live ([#3091](https://github.com/0xMiden/miden-vm/pull/3091)). #### Changes diff --git a/crates/assembly-syntax/src/ast/item/resolver/error.rs b/crates/assembly-syntax/src/ast/item/resolver/error.rs index 67cb999876..6c89632c61 100644 --- a/crates/assembly-syntax/src/ast/item/resolver/error.rs +++ b/crates/assembly-syntax/src/ast/item/resolver/error.rs @@ -54,6 +54,16 @@ pub enum SymbolResolutionError { #[related] actual: Option, }, + #[error("private symbol reference")] + #[diagnostic(help("only public items can be referenced from another module"))] + PrivateSymbol { + #[label("this symbol is private to another module")] + span: SourceSpan, + #[source_code] + source_file: Option>, + #[related] + defined: Option, + }, #[error("type expression nesting depth exceeded")] #[diagnostic(help("type expression nesting exceeded the maximum depth of {max_depth}"))] TypeExpressionDepthExceeded { @@ -158,6 +168,24 @@ impl SymbolResolutionError { } } + pub fn private_symbol( + span: SourceSpan, + defined: SourceSpan, + source_manager: &dyn SourceManager, + ) -> Self { + let defined_source_file = source_manager.get(defined.source_id()).ok(); + let source_file = source_manager.get(span.source_id()).ok(); + Self::PrivateSymbol { + span, + source_file, + defined: Some( + RelatedLabel::advice("the referenced item is private") + .with_labeled_span(defined, "the referenced item is private") + .with_source_file(defined_source_file), + ), + } + } + pub fn type_expression_depth_exceeded( span: SourceSpan, max_depth: usize, diff --git a/crates/assembly-syntax/src/ast/visit.rs b/crates/assembly-syntax/src/ast/visit.rs index 62f417ba66..f404258172 100644 --- a/crates/assembly-syntax/src/ast/visit.rs +++ b/crates/assembly-syntax/src/ast/visit.rs @@ -303,6 +303,11 @@ pub fn visit_procedure(visitor: &mut V, procedure: &Procedure) -> ControlF where V: ?Sized + Visit, { + if let Some(signature) = procedure.signature() { + for ty in signature.args.iter().chain(signature.results.iter()) { + visitor.visit_type_expr(ty)?; + } + } visitor.visit_block(procedure.body()) } @@ -894,6 +899,11 @@ pub fn visit_mut_procedure(visitor: &mut V, procedure: &mut Procedure) -> where V: ?Sized + VisitMut, { + if let Some(signature) = procedure.signature_mut() { + for ty in signature.args.iter_mut().chain(signature.results.iter_mut()) { + visitor.visit_mut_type_expr(ty)?; + } + } visitor.visit_mut_block(procedure.body_mut()) } diff --git a/crates/assembly/src/linker/resolver/symbol_resolver.rs b/crates/assembly/src/linker/resolver/symbol_resolver.rs index 8fa99af38c..76ea86ea52 100644 --- a/crates/assembly/src/linker/resolver/symbol_resolver.rs +++ b/crates/assembly/src/linker/resolver/symbol_resolver.rs @@ -300,6 +300,16 @@ impl<'a> SymbolResolver<'a> { context: &SymbolResolutionContext, path: Span<&Path>, ignored_imports: &mut BTreeSet>, + ) -> Result { + self.expand_path_from(context.module, context, path, ignored_imports) + } + + fn expand_path_from( + &self, + origin_module: ModuleIndex, + context: &SymbolResolutionContext, + path: Span<&Path>, + ignored_imports: &mut BTreeSet>, ) -> Result { let span = path.span(); let mut path = path.into_inner(); @@ -386,19 +396,25 @@ impl<'a> SymbolResolver<'a> { { SymbolResolution::Local(item) => { log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to local symbol: {}", context.module + item.into_inner()); + let gid = context.module + item.into_inner(); + self.ensure_item_visible(origin_module, gid, span)?; let path = self.module_path(context.module).join(&symbol); - Ok(SymbolResolution::Exact { - gid: context.module + item.into_inner(), - path: Span::new(span, path.into()), - }) + Ok(SymbolResolution::Exact { gid, path: Span::new(span, path.into()) }) }, SymbolResolution::External(path) => { log::trace!(target: "name-resolver::expand", "expanded '{symbol}' to unresolved external path '{path}'"); - self.expand_path(&context, path.as_deref(), ignored_imports) + self.expand_path_from( + origin_module, + &context, + path.as_deref(), + ignored_imports, + ) }, - resolved @ (SymbolResolution::MastRoot(_) | SymbolResolution::Exact { .. }) => { + resolved @ SymbolResolution::MastRoot(_) => Ok(resolved), + SymbolResolution::Exact { gid, path } => { log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to exact definition"); - Ok(resolved) + self.ensure_item_visible(origin_module, gid, span)?; + Ok(SymbolResolution::Exact { gid, path }) }, SymbolResolution::Module { id, path: module_path } => { log::trace!(target: "name-resolver::expand", "resolved '{symbol}' to module: id={id} path={module_path}"); @@ -437,7 +453,8 @@ impl<'a> SymbolResolver<'a> { if ignored_imports.contains(imported_symbol) { log::trace!(target: "name-resolver::expand", "skipping import expansion of '{imported_symbol}': already expanded, resolving as absolute path instead"); let path = path.to_absolute(); - break self.expand_path( + break self.expand_path_from( + origin_module, &context, Span::new(span, path.as_ref()), ignored_imports, @@ -500,7 +517,8 @@ impl<'a> SymbolResolver<'a> { let partially_expanded = external_path.join(subpath); log::trace!(target: "name-resolver::expand", "partially expanded '{path}' to '{partially_expanded}'"); ignored_imports.insert(imported_symbol.to_string().into_boxed_str().into()); - break self.expand_path( + break self.expand_path_from( + origin_module, &context, Span::new(span, partially_expanded.as_path()), ignored_imports, @@ -515,7 +533,8 @@ impl<'a> SymbolResolver<'a> { // Try to expand the path by treating it as an absolute path let absolute = path.to_absolute(); log::trace!(target: "name-resolver::expand", "no import found for '{imported_symbol}' in '{path}': attempting to resolve as absolute path instead"); - break self.expand_path( + break self.expand_path_from( + origin_module, &context, Span::new(span, absolute.as_ref()), ignored_imports, @@ -530,6 +549,29 @@ impl<'a> SymbolResolver<'a> { } } + fn ensure_item_visible( + &self, + origin_module: ModuleIndex, + item: GlobalItemIndex, + span: SourceSpan, + ) -> Result<(), LinkerError> { + if origin_module == item.module { + return Ok(()); + } + + let symbol = &self.graph[item.module][item.index]; + if symbol.visibility().is_public() { + return Ok(()); + } + + Err(SymbolResolutionError::private_symbol( + span, + symbol.name().span(), + self.source_manager(), + ) + .into()) + } + pub fn resolve_local( &self, context: &SymbolResolutionContext, diff --git a/crates/assembly/src/linker/rewrites/module.rs b/crates/assembly/src/linker/rewrites/module.rs index 593013202e..56c0d085b9 100644 --- a/crates/assembly/src/linker/rewrites/module.rs +++ b/crates/assembly/src/linker/rewrites/module.rs @@ -191,7 +191,6 @@ impl<'a, 'b: 'a> VisitMut for ModuleRewriter<'a, 'b> { fn visit_mut_alias(&mut self, alias: &mut Alias) -> ControlFlow { match alias.target() { AliasTarget::MastRoot(_) => return ControlFlow::Continue(()), - AliasTarget::Path(path) if path.is_absolute() => return ControlFlow::Continue(()), AliasTarget::Path(_) => (), } log::debug!(target: "linker", " * rewriting alias target {}", alias.target()); diff --git a/crates/assembly/src/project/build_provenance.rs b/crates/assembly/src/project/build_provenance.rs index 2aca0c6ab6..a93a32acbe 100644 --- a/crates/assembly/src/project/build_provenance.rs +++ b/crates/assembly/src/project/build_provenance.rs @@ -28,9 +28,9 @@ use super::PackageBuildSettings; /// /// The recorded fields differ by source origin: /// -/// - [`Self::Path`] tracks a hash of the effective manifest and the target's resolved source files, -/// along with a hash of the fully resolved dependency closure and the build-profile knobs that -/// affect the emitted package bytes. +/// - [`Self::Path`] tracks a hash of the selected target's build-provenance projection and resolved +/// source files, along with a hash of the fully resolved dependency closure and the build-profile +/// knobs that affect the emitted package bytes. /// - [`Self::Git`] records the repository identity and resolved revision instead of hashing the /// checked-out source tree directly, but still includes the dependency-closure hash and build /// settings for the same reuse decision. @@ -46,7 +46,7 @@ use super::PackageBuildSettings; pub(super) enum PackageBuildProvenance { /// Provenance for a package assembled from sources addressed by a local filesystem path. Path { - /// Hash of the effective manifest plus the root/support modules that define the target. + /// Hash of the build-provenance projection plus the root/support modules for the target. source_hash: Word, /// Hash of the resolved dependency closure, including linkage and exact selected /// artifacts. diff --git a/crates/assembly/src/project/dependency_graph.rs b/crates/assembly/src/project/dependency_graph.rs index 8458c9bc7c..0c6c1fc3fe 100644 --- a/crates/assembly/src/project/dependency_graph.rs +++ b/crates/assembly/src/project/dependency_graph.rs @@ -151,13 +151,13 @@ impl DependencyGraph { profile_name: &str, origin: &ProjectSourceOrigin, manifest_path: &FsPath, - workspace_root: Option<&FsPath>, + _workspace_root: Option<&FsPath>, visiting: &mut BTreeSet, ) -> Result { let dependency_hash = self.compute_dependency_closure_hash(package_id, profile_name, visiting)?; - let build_settings = - PackageBuildSettings::from_profile(project.resolve_profile(profile_name)?); + let profile = project.resolve_profile(profile_name)?; + let build_settings = PackageBuildSettings::from_profile(profile); match origin { ProjectSourceOrigin::Git { repo, resolved_revision, .. } => { @@ -172,8 +172,8 @@ impl DependencyGraph { Ok(PackageBuildProvenance::Path { source_hash: project.compute_path_source_hash( target, + profile, manifest_path, - workspace_root, )?, dependency_hash, build_settings, diff --git a/crates/assembly/src/project/package_ext.rs b/crates/assembly/src/project/package_ext.rs index 25e06f626e..66cf626f4a 100644 --- a/crates/assembly/src/project/package_ext.rs +++ b/crates/assembly/src/project/package_ext.rs @@ -17,7 +17,7 @@ use miden_assembly_syntax::{ }; use miden_core::{Word, utils::hash_string_to_word}; use miden_package_registry::PackageId; -use miden_project::{DependencyVersionScheme, Package as ProjectPackage, Profile, Target}; +use miden_project::{Package as ProjectPackage, Profile, Target}; use super::TargetSourcePaths; use crate::SourceManager; @@ -39,8 +39,8 @@ pub(super) trait ProjectPackageExt { fn compute_path_source_hash( &self, target: &Target, + profile: &Profile, manifest_path: &FsPath, - workspace_root: Option<&FsPath>, ) -> Result; fn excluded_target_roots( @@ -51,8 +51,6 @@ pub(super) trait ProjectPackageExt { fn resolve_target_source_paths(&self, target: &Target) -> Result; - fn effective_manifest_hash_input(&self) -> Result; - fn resolve_profile(&self, name: &str) -> Result<&Profile, Report>; } @@ -125,8 +123,8 @@ impl ProjectPackageExt for ProjectPackage { fn compute_path_source_hash( &self, target: &Target, + profile: &Profile, manifest_path: &FsPath, - workspace_root: Option<&FsPath>, ) -> Result { let source_paths = self.resolve_target_source_paths(target)?; let project_root = manifest_path.parent().ok_or_else(|| { @@ -152,24 +150,7 @@ impl ProjectPackageExt for ProjectPackage { } inputs.sort_by(|a, b| a.0.cmp(&b.0)); - let mut material = format!( - "target:{}\nkind:{}\nnamespace:{}\n", - target.name.inner(), - target.ty, - target.namespace.inner() - ); - if workspace_root.is_some() { - material.push_str("manifest:effective\n"); - material.push_str(&self.effective_manifest_hash_input()?); - material.push('\n'); - } else { - let manifest_label = manifest_path - .strip_prefix(project_root) - .unwrap_or(manifest_path) - .display() - .to_string(); - inputs.push((format!("manifest:{manifest_label}"), manifest_path.to_path_buf())); - } + let mut material = self.build_provenance_projection(target, profile); for (label, path) in inputs { let bytes = fs::read(&path).map_err(|error| { Report::msg(format!("failed to read source input '{}': {error}", path.display())) @@ -212,45 +193,6 @@ impl ProjectPackageExt for ProjectPackage { Ok(TargetSourcePaths { root: root_path, root_dir, support }) } - fn effective_manifest_hash_input(&self) -> Result { - let mut manifest = self.to_toml()?; - - let mut workspace_dependencies = self - .dependencies() - .iter() - .filter_map(|dependency| match dependency.scheme() { - DependencyVersionScheme::Workspace { member, version } => Some(( - dependency.name().to_string(), - member.path().to_string(), - version.as_ref().map(ToString::to_string), - dependency.linkage(), - )), - DependencyVersionScheme::WorkspacePath { path, version } => Some(( - dependency.name().to_string(), - path.path().to_string(), - version.as_ref().map(ToString::to_string), - dependency.linkage(), - )), - _ => None, - }) - .collect::>(); - workspace_dependencies.sort_by(|a, b| a.0.cmp(&b.0)); - - if !workspace_dependencies.is_empty() { - manifest.push_str("\n# resolved_workspace_dependencies\n"); - for (name, member_path, version, linkage) in workspace_dependencies { - match version { - Some(version) => { - manifest.push_str(&format!("{name}={member_path}@{version}:{linkage}\n")); - }, - None => manifest.push_str(&format!("{name}={member_path}:{linkage}\n")), - } - } - } - - Ok(manifest) - } - fn resolve_profile(&self, name: &str) -> Result<&Profile, Report> { self.get_profile(name).ok_or_else(|| { Report::msg(format!( diff --git a/crates/assembly/src/project/tests.rs b/crates/assembly/src/project/tests.rs index e591587574..b71c7d5aec 100644 --- a/crates/assembly/src/project/tests.rs +++ b/crates/assembly/src/project/tests.rs @@ -1820,6 +1820,93 @@ dep = { path = "dep" } ); } +#[test] +fn package_manifest_changes_without_build_effect_allow_source_dependency_reuse() { + let tempdir = TempDir::new().unwrap(); + let dep_dir = tempdir.path().join("dep"); + write_file( + &dep_dir.join("miden-project.toml"), + r#"[package] +name = "dep" +version = "1.0.0" + +[lib] +path = "lib.masm" +"#, + ); + write_file( + &dep_dir.join("lib.masm"), + r#"pub proc foo + push.1 +end +"#, + ); + + let root_dir = tempdir.path().join("root"); + let root_manifest = root_dir.join("miden-project.toml"); + write_file( + &root_manifest, + r#"[package] +name = "root" +version = "1.0.0" + +[lib] +path = "lib.masm" + +[dependencies] +dep = { path = "../dep" } +"#, + ); + write_file( + &root_dir.join("lib.masm"), + r#"pub proc entry + exec.::dep::foo +end +"#, + ); + + let mut context = TestContext::new(); + context + .assemble_library_package(&root_manifest, None) + .expect("initial build should succeed"); + let dep_record = context + .registry() + .get_by_semver(&PackageId::from("dep"), &"1.0.0".parse().unwrap()) + .expect("dependency should be registered"); + let dep_version = dep_record.version().clone(); + context.registry().clear_loaded_packages(); + + write_file( + &dep_dir.join("miden-project.toml"), + r#"# comments and formatting should not affect build provenance + +[package] +name = "dep" +version = "1.0.0" +description = "metadata-only update" + +[package.metadata.audit] +ticket = "ignored" + +[lib] +path = "src/lib.masm" + +[[bin]] +name = "unused" +path = "bin/unused.masm" + +[profile.unused] +debug = false +custom = "ignored" +"#, + ); + + context + .assemble_library_package(&root_manifest, None) + .expect("manifest-only changes outside build provenance should allow reuse"); + assert_eq!(context.registry().loaded_packages(), vec![format!("dep@{dep_version}")]); +} + #[test] fn git_dependency_reuses_canonical_revision_and_rejects_new_commit_without_semver_bump() { let tempdir = TempDir::new().unwrap(); diff --git a/crates/assembly/src/tests.rs b/crates/assembly/src/tests.rs index 1c5680de98..e8a5d33ce7 100644 --- a/crates/assembly/src/tests.rs +++ b/crates/assembly/src/tests.rs @@ -5615,6 +5615,123 @@ fn test_cross_module_constant_resolution_as_local_definition() -> TestResult { Ok(()) } +#[test] +fn importing_private_constant_from_another_module_is_rejected() -> TestResult { + let context = TestContext::default(); + + let module_a = context.parse_module_with_path( + "cycle::module_a", + source_file!( + &context, + r#" + const A_VAL = 10 + pub proc a_proc + push.A_VAL + end + "# + ), + )?; + + let module_b = context.parse_module_with_path( + "cycle::module_b", + source_file!( + &context, + r#" + use cycle::module_a::A_VAL + pub proc b_proc + push.A_VAL + end + "# + ), + )?; + + let err = Assembler::new(context.source_manager()) + .assemble_library([module_a, module_b]) + .expect_err("expected private constant import to be rejected"); + assert_diagnostic!(&err, "private symbol reference"); + assert_diagnostic!(&err, "only public items can be referenced from another module"); + + Ok(()) +} + +#[test] +fn importing_private_constant_from_another_module_by_absolute_path_is_rejected() -> TestResult { + let context = TestContext::default(); + + let module_a = context.parse_module_with_path( + "cycle::module_a", + source_file!( + &context, + r#" + const A_VAL = 10 + pub proc a_proc + push.A_VAL + end + "# + ), + )?; + + let module_b = context.parse_module_with_path( + "cycle::module_b", + source_file!( + &context, + r#" + use ::cycle::module_a::A_VAL + pub proc b_proc + push.A_VAL + end + "# + ), + )?; + + let err = Assembler::new(context.source_manager()) + .assemble_library([module_a, module_b]) + .expect_err("expected private absolute constant import to be rejected"); + assert_diagnostic!(&err, "private symbol reference"); + assert_diagnostic!(&err, "only public items can be referenced from another module"); + + Ok(()) +} + +#[test] +fn importing_private_type_from_another_module_is_rejected() -> TestResult { + let context = TestContext::default(); + + let module_a = context.parse_module_with_path( + "cycle::module_a", + source_file!( + &context, + r#" + type PrivateType = felt + pub proc a_proc + nop + end + "# + ), + )?; + + let module_b = context.parse_module_with_path( + "cycle::module_b", + source_file!( + &context, + r#" + use cycle::module_a::PrivateType + pub proc b_proc(value: PrivateType) + nop + end + "# + ), + )?; + + let err = Assembler::new(context.source_manager()) + .assemble_library([module_a, module_b]) + .expect_err("expected private type import to be rejected"); + assert_diagnostic!(&err, "private symbol reference"); + assert_diagnostic!(&err, "only public items can be referenced from another module"); + + Ok(()) +} + #[test] fn test_cross_module_constant_reexport_chain_in_procedure_scope() -> TestResult { let context = TestContext::new(); @@ -6055,6 +6172,45 @@ fn forward_declared_import_used_by_type_ref_is_not_reported_unused_when_warnings .expect("expected forward-declared import used by type ref to count as used"); } +#[test] +fn forward_declared_import_used_by_proc_signature_is_not_reported_unused_when_warnings_are_errors() +{ + use std::sync::Arc; + + use crate::{DefaultSourceManager, Parse, ParseOptions, ast::ModuleKind}; + + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let module = r#" + pub proc check(value: foo::Type) -> foo::Type + nop + end + use external::module -> foo + "#; + + let mut options = ParseOptions::new(ModuleKind::Library, "m"); + options.warnings_as_errors = true; + + <&str as Parse>::parse_with_options(module, source_manager, options) + .expect("expected forward-declared import used by signature type to count as used"); +} + +#[test] +fn kernel_import_used_by_proc_signature_is_not_reported_unused_when_warnings_are_errors() { + let context = TestContext::new(); + context + .parse_kernel(source_file!( + &context, + r#" + use external::module -> foo + + pub proc check(value: foo::Type) -> foo::Type + nop + end + "# + )) + .expect("expected kernel signature type import to count as used"); +} + #[test] fn forward_declared_import_used_by_constant_ref_is_not_reported_unused_when_warnings_are_errors() { use std::sync::Arc; diff --git a/crates/lib/core/asm/stark/deep_queries.masm b/crates/lib/core/asm/stark/deep_queries.masm index bf58ff1b05..0ae8743482 100644 --- a/crates/lib/core/asm/stark/deep_queries.masm +++ b/crates/lib/core/asm/stark/deep_queries.masm @@ -211,9 +211,11 @@ end #! Computes the DEEP composition polynomial FRI queries. #! -#! Input: [...] -#! Output: [...] -proc compute_deep_composition_polynomial_queries +#! Inputs: [...] +#! Outputs: [...] +#! +#! Invocation: exec +pub proc compute_deep_composition_polynomial_queries # Prepare the stack for the core procedure computing the queries exec.prepare_stack_deep_queries_computation # => [Y, query_ptr, query_end_ptr, W, query_ptr] diff --git a/crates/lib/core/asm/stark/public_inputs.masm b/crates/lib/core/asm/stark/public_inputs.masm index 26453d784e..c9b7ad1a13 100644 --- a/crates/lib/core/asm/stark/public_inputs.masm +++ b/crates/lib/core/asm/stark/public_inputs.masm @@ -47,9 +47,11 @@ use miden::core::stark::constants #! the benefit of making the reduction using the auxiliary randomness more efficient using #! `horner_eval_base`. #! -#! Input: [...] -#! Output: [...] -proc process_public_inputs +#! Inputs: [...] +#! Outputs: [...] +#! +#! Invocation: exec +pub proc process_public_inputs exec.constants::get_procedure_digest_process_public_inputs_ptr dynexec end @@ -79,9 +81,11 @@ end #! The var_len_ptr is the region that will temporarily store the variable-length public inputs #! and permanently store the results of reducing them by the auxiliary randomness. #! -#! Input: [num_var_len_pi_groups, num_fixed_len_pi, ...] -#! Output: [...] -proc compute_and_store_public_inputs_address +#! Inputs: [num_var_len_pi_groups, num_fixed_len_pi, ...] +#! Outputs: [...] +#! +#! Invocation: exec +pub proc compute_and_store_public_inputs_address # 1) Get a pointer to where OOD evaluations are stored exec.constants::ood_evaluations_ptr # => [ood_evals_ptr, num_var_len_pi_groups, num_fixed_len_pi, ...] @@ -117,12 +121,14 @@ end #! The procedure does NOT absorb the elements into the transcript -- the caller is responsible #! for calling `poseidon2::permute` after this procedure to complete the absorption. #! -#! Input: [Y, Y, C, ptr, ...] -#! Output: [A0, A1, C, ptr + 16, ..] +#! Inputs: [Y, Y, C, ptr, ...] +#! Outputs: [A0, A1, C, ptr + 16, ..] #! #! where A0 and A1 are the original base element words (from advice), positioned in the rate #! slots so that a subsequent `poseidon2::permute` absorbs them into the sponge state. -proc load_base_store_extension_double_word +#! +#! Invocation: exec +pub proc load_base_store_extension_double_word # 1) Load the first 4 base elements from the advice stack and save them temporarily adv_loadw exec.constants::tmp1 mem_storew_le diff --git a/crates/lib/core/asm/stark/utils.masm b/crates/lib/core/asm/stark/utils.masm index d3e2cfa50e..5083997d1f 100644 --- a/crates/lib/core/asm/stark/utils.masm +++ b/crates/lib/core/asm/stark/utils.masm @@ -295,11 +295,13 @@ proc sample_gamma swap end -#! Executes the constraints evalution check. +#! Executes the constraints evaluation check. #! -#! Input: [...] -#! Output: [...] -proc execute_constraint_evaluation_check +#! Inputs: [...] +#! Outputs: [...] +#! +#! Invocation: exec +pub proc execute_constraint_evaluation_check exec.constants::get_procedure_digest_execute_constraint_evaluation_check_ptr dynexec end @@ -308,9 +310,11 @@ end #! #! For AIRs without an auxiliary trace, the implementation should be a no-op. #! -#! Input: [...] -#! Output: [...] -proc observe_aux_trace +#! Inputs: [...] +#! Outputs: [...] +#! +#! Invocation: exec +pub proc observe_aux_trace exec.constants::get_procedure_digest_observe_aux_trace_ptr dynexec end diff --git a/crates/lib/core/docs/stark/deep_queries.md b/crates/lib/core/docs/stark/deep_queries.md index c77cb46839..2c2a43b065 100644 --- a/crates/lib/core/docs/stark/deep_queries.md +++ b/crates/lib/core/docs/stark/deep_queries.md @@ -4,3 +4,4 @@ | ----------- | ------------- | | prepare_stack_deep_queries_computation | Prepares the stack for the computation of the DEEP composition polynomial FRI queries.

It also performs some pre-computations that are common to all queries as an optimization.

Input: [...]
Output: [Y, query_ptr, query_end_ptr, W, query_ptr, ...]

where:

1. `Y` is a garbage word,
2. `query_ptr` is a pointer to the memory region from where the query indices will be fetched
and to where the computed FRI queries will be stored in a word-aligned manner,
3. `query_end_ptr` is a memory pointer used to indicate the end of the memory region used in
storing the computed FRI queries,
4. `W` is the word `[q_z_0, q_z_1, q_gz_0, q_gz_1]` where `q_z = (q_z_0, q_z_1)` and
`q_gz = (q_gz_0, q_gz_1)` represent the constant terms across all FRI queries computations.
| | compute_deep_query | Compute the DEEP composition polynomial FRI query at `index`.

Input: [Y, X, index, query_ptr, query_end_ptr, W, query_ptr, ...]
Output: [?, Y, query_ptr+1, query_end_ptr, ...]

where:
1. `Y` is a garbage word,
2. `X` is `[q_x_at_alpha_0, q_x_at_alpha_1, q_x_at_alpha_0, q_x_at_alpha_1]` where
`q_x = (q_x_0, q_x_1)` is the variable part of the query,
3. `index` is the query index in the FRI domain,
4. `query_ptr` is a pointer to the memory region from where the query indices will be fetched
and to where the computed FRI queries will be stored in a word-aligned manner,
5. `query_end_ptr` is a memory pointer used to indicate the end of the memory region used in
storing the computed FRI queries,
6. `W` is the word `[q_z_0, q_z_1, q_gz_0, q_gz_1]` where `q_z = (q_z_0, q_z_1)` and
`q_gz = (q_gz_0, q_gz_1)` represent the constant terms across all FRI queries computations.
7. `?` is a binary flag indicating if there are further queries to process.
| +| compute_deep_composition_polynomial_queries | Computes the DEEP composition polynomial FRI queries.

Inputs: [...]
Outputs: [...]

Invocation: exec
| diff --git a/crates/lib/core/docs/stark/public_inputs.md b/crates/lib/core/docs/stark/public_inputs.md index 2da4d00aca..6ad9b805d6 100644 --- a/crates/lib/core/docs/stark/public_inputs.md +++ b/crates/lib/core/docs/stark/public_inputs.md @@ -2,3 +2,6 @@ ## miden::core::stark::public_inputs | Procedure | Description | | ----------- | ------------- | +| process_public_inputs | Processes the public inputs.

This is a generic dispatch procedure that calls the instance-specific implementation
via `dynexec`. The instance-specific implementation loads fixed-length and variable-length
public inputs from the advice stack, reduces the variable-length inputs using auxiliary
randomness, and stores everything into the ACE READ section.

This involves:

1. Loading from the advice stack the fixed-length public inputs and storing them in memory
starting from the address pointed to by `public_inputs_address_ptr`.
2. Loading from the advice stack the variable-length public inputs, storing them temporarily
in memory, and then reducing them to an element in the challenge field using the auxiliary
randomness. This reduced value is then used to impose boundary conditions on the relevant
auxiliary columns.

Note that the fixed-length public inputs are stored as extension field elements while
the variable-length ones are stored as base field elements, because the ACE circuit operates
only on extension field elements and the latter inputs are not direct inputs to the circuit,
i.e., only their reduced values are.
Both fixed-length and variable-length public inputs are absorbed into the Fiat-Shamir
transcript via direct sponge absorption during loading.

It is worth noting that:

1. Only the fixed-length public inputs are stored for the lifetime of the verification
procedure. The variable-length public inputs are stored temporarily, as this simplifies
the task of reducing them using the auxiliary randomness. The resulting values from the
reductions are stored right after the fixed-length public inputs, in a word-aligned
manner and padded with zeros if needed.
2. The public inputs address is computed so that we end up with the following memory layout:

[..., a_0..a_{m-1}, b_0..b_{n-1}, beta0, beta1, alpha0, alpha1, OOD-start, ...]

where:

a) [a_0..a_{m-1}] are the fixed-length public inputs stored as extension field elements.
This section is double-word-aligned.
b) [b_0..b_{n-1}] are the results of reducing the variable-length public inputs using
auxiliary randomness. This section is word-aligned.
c) [beta0, beta1, alpha0, alpha1] is the auxiliary randomness.
d) OOD-start is the first field element of the section containing the OOD evaluations.
3. For each bus message in a group in the variable-length public inputs, each message is
expected to be padded to the next multiple of 8 and provided in reverse order. This has
the benefit of making the reduction using the auxiliary randomness more efficient using
`horner_eval_base`.

Inputs: [...]
Outputs: [...]

Invocation: exec
| +| compute_and_store_public_inputs_address | Computes the addresses where the public inputs are to be stored.

In order to call `eval_circuit`, we need to lay out the inputs to the constraint evaluation
circuit in a contiguous region of memory (called READ section in the ACE chiplet documentation)
right before the region storing the circuit description (called EVAL section).

As the number of public inputs is a per-instance parameter, while the sizes of the OOD
evaluation frames and the number of auxiliary random values are fixed, we lay out the public
inputs right before the auxiliary random values and OOD evaluations.
Hence the address where public inputs are stored is computed using a negative offset from the
address where the OOD evaluations are stored.

We compute two pointers:
var_len_ptr = OOD_EVALUATIONS_PTR - 4 - num_var_len_pi_groups * 4
pi_ptr = var_len_ptr - num_fixed_len_pi * 2

The `* 4` for VLPI groups provides word-alignment (each group uses 4 base felts = 2 EF
slots, even though the reduced value is only 1 EF). The `* 2` for FLPI converts the base
felt count to EF slots (each base felt is stored as [val, 0]).

The `-4` accounts for the auxiliary randomness word [beta0, beta1, alpha0, alpha1] at
AUX_RAND_ELEM_PTR (4 base felts between var_len_ptr and OOD_EVALUATIONS_PTR).

The var_len_ptr is the region that will temporarily store the variable-length public inputs
and permanently store the results of reducing them by the auxiliary randomness.

Inputs: [num_var_len_pi_groups, num_fixed_len_pi, ...]
Outputs: [...]

Invocation: exec
| +| load_base_store_extension_double_word | Loads 8 base field elements from the advice stack, stores them as extension field elements
in memory, and returns the original base element words in the rate positions for absorption
by the caller via `poseidon2::permute`.

The procedure does NOT absorb the elements into the transcript -- the caller is responsible
for calling `poseidon2::permute` after this procedure to complete the absorption.

Inputs: [Y, Y, C, ptr, ...]
Outputs: [A0, A1, C, ptr + 16, ..]

where A0 and A1 are the original base element words (from advice), positioned in the rate
slots so that a subsequent `poseidon2::permute` absorbs them into the sponge state.

Invocation: exec
| diff --git a/crates/lib/core/docs/stark/utils.md b/crates/lib/core/docs/stark/utils.md index cd42ad2513..9d560d7d37 100644 --- a/crates/lib/core/docs/stark/utils.md +++ b/crates/lib/core/docs/stark/utils.md @@ -6,4 +6,6 @@ | validate_inputs | Validates the inputs to the recursive verifier.

Checks log(trace_length) from the stack and security parameters from memory.
The security parameters must already be stored in memory before calling this
procedure (done by the specific verifier's load_security_params).

Input: [log(trace_length), ...]
Output: [log(trace_length), ...]
| | bit_reverse_len_parallel | Reverse the lowest `bits` bits of `index` using parallel bit-swap.
`pow2_shift` must equal 2^(32 - bits); the caller pre-computes it once.

The algorithm has two parts:

1) Left-shift: multiply index by pow2_shift = 2^(32-bits) to place the `bits`
meaningful bits into the top of a 32-bit word. Since index < 2^bits, the
product is always < 2^32 so u32wrapping_mul is exact.

2) Full 32-bit reversal via 5 parallel swap steps. Each step k (for k=1,2,4,8,16)
swaps every adjacent group of k bits. A mask isolates the even-positioned
groups; the odd-positioned groups are the complement. Shifting each half by
k positions and combining swaps all groups simultaneously. After all 5 steps,
bit position i has moved to position 31-i, which (because of the initial
left-shift) is exactly position (bits-1-i) of the original -- i.e., the
reversed index. No final shift is needed.

The two halves never overlap, so XOR = OR = ADD; we use XOR (1 cycle vs 3 for
u32wrapping_add).

Input: [index, pow2_shift, ...]
Output: [rev_index, ...]
Cycles: 78
| | set_up_auxiliary_inputs_ace | Sets up the stark-vars region of the ACE circuit input layout.

The stark-vars block is 10 EF slots (5 words) laid out as:

Word 0 (slots 0-1): alpha, z^N (EF: composition challenge, trace-length power)
Word 1 (slots 2-3): z_k, is_first (EF: periodic eval point, first-row selector)
Word 2 (slots 4-5): is_last, is_transition (EF: last-row selector, transition selector)
Word 3 (slots 6-7): gamma, weight0 (EF: batching challenge; base as EF: barycentric weight)
Word 4 (slots 8-9): f, s0 (base as EF: chunk shift ratio h^N, first shift)

Input: [max_cycle_len_log, ...]
Output: [...]
| +| execute_constraint_evaluation_check | Executes the constraints evaluation check.

Inputs: [...]
Outputs: [...]

Invocation: exec
| +| observe_aux_trace | Observes the auxiliary trace: draws aux randomness, reseeds with the aux trace commitment,
and absorbs aux trace boundary values into the transcript.

For AIRs without an auxiliary trace, the implementation should be a no-op.

Inputs: [...]
Outputs: [...]

Invocation: exec
| | store_dynamically_executed_procedures | Stores digests of dynamically executed procedures.

Input: [D0, D1, D2, D3, D4, ...]
Output: [...]
| diff --git a/crates/project/src/package.rs b/crates/project/src/package.rs index dd04c39a57..2922e0d41c 100644 --- a/crates/project/src/package.rs +++ b/crates/project/src/package.rs @@ -1,4 +1,7 @@ -use alloc::boxed::Box; +use alloc::{ + boxed::Box, + string::{String, ToString}, +}; #[cfg(feature = "std")] use std::path::Path; @@ -212,6 +215,35 @@ impl Package { pub fn manifest_path(&self) -> Option<&Path> { self.manifest_path.as_deref() } + + /// Return the package model projection that affects artifact reuse for `target` under + /// `profile`. + pub fn build_provenance_projection(&self, target: &Target, profile: &Profile) -> String { + let Self { + #[cfg(feature = "std")] + manifest_path: _, + name, + version, + description: _, + dependencies: _, + lints: _, + metadata: _, + lib: _, + bins: _, + profiles: _, + } = self; + + let mut projection = String::new(); + projection.push_str("package:name:"); + projection.push_str(name.inner().as_ref()); + projection.push('\n'); + projection.push_str("package:version:"); + projection.push_str(version.inner().to_string().as_str()); + projection.push('\n'); + target.append_build_provenance_projection(&mut projection); + profile.append_build_provenance_projection(&mut projection); + projection + } } /// Parsing @@ -327,7 +359,7 @@ impl Package { /// The output of this function is not guaranteed to be identical to the way the original /// manifest (if one exists) was written, i.e. it may emit keys that are optional or that /// contain default or inherited values. - pub fn to_toml(&self) -> Result { + pub fn to_toml(&self) -> Result { let manifest_ast = ast::ProjectFile { source_file: None, package: ast::PackageTable { diff --git a/crates/project/src/profile.rs b/crates/project/src/profile.rs index cee72495b1..abfe138209 100644 --- a/crates/project/src/profile.rs +++ b/crates/project/src/profile.rs @@ -1,4 +1,4 @@ -use alloc::string::ToString; +use alloc::string::{String, ToString}; use core::borrow::Borrow; use miden_assembly_syntax::debuginfo::Spanned; @@ -196,6 +196,18 @@ impl Profile { { self.metadata.get(key) } + + /// Append the profile settings that affect package artifact reuse to `out`. + pub fn append_build_provenance_projection(&self, out: &mut String) { + let Self { name: _, debug, trim_paths, metadata: _ } = self; + + out.push_str("profile:debug:"); + out.push_str(if *debug { "true" } else { "false" }); + out.push('\n'); + out.push_str("profile:trim_paths:"); + out.push_str(if *trim_paths { "true" } else { "false" }); + out.push('\n'); + } } impl Extend<(Span>, Span)> for Profile { diff --git a/crates/project/src/target.rs b/crates/project/src/target.rs index 787ebaccb8..3a3ccca948 100644 --- a/crates/project/src/target.rs +++ b/crates/project/src/target.rs @@ -1,3 +1,5 @@ +use alloc::string::{String, ToString}; + use miden_assembly_syntax::Path; use crate::*; @@ -69,4 +71,25 @@ impl Target { pub const fn is_kernel(&self) -> bool { matches!(self.ty, TargetType::Kernel) } + + /// Append the selected target fields that affect package artifact reuse to `out`. + pub fn append_build_provenance_projection(&self, out: &mut String) { + let Self { ty, name, namespace, path } = self; + + out.push_str("target:kind:"); + out.push_str(ty.to_string().as_str()); + out.push('\n'); + out.push_str("target:name:"); + out.push_str(name.inner().as_ref()); + out.push('\n'); + out.push_str("target:namespace:"); + out.push_str(namespace.inner().as_str()); + out.push('\n'); + out.push_str("target:path:"); + match path.as_ref() { + Some(path) => out.push_str(path.inner().path()), + None => out.push_str(""), + } + out.push('\n'); + } } diff --git a/miden-vm/tests/integration/cli/cli_test.rs b/miden-vm/tests/integration/cli/cli_test.rs index 9b1dd8f786..7f1a153a7c 100644 --- a/miden-vm/tests/integration/cli/cli_test.rs +++ b/miden-vm/tests/integration/cli/cli_test.rs @@ -112,7 +112,7 @@ fn cli_bundle_kernel_noexports() { cmd.arg("bundle") .arg("./tests/integration/cli/data/lib_noexports") .arg("--kernel") - .arg("./tests/integration/cli/data/kernel_main.masm") + .arg("./tests/integration/cli/data/kernel_noexports.masm") .arg("--output") .arg(output_file.as_path()); cmd.assert().success(); diff --git a/miden-vm/tests/integration/cli/data/kernel_noexports.masm b/miden-vm/tests/integration/cli/data/kernel_noexports.masm new file mode 100644 index 0000000000..72596bc8bc --- /dev/null +++ b/miden-vm/tests/integration/cli/data/kernel_noexports.masm @@ -0,0 +1,8 @@ +proc internal_proc + caller + drop +end + +pub proc kernel_proc + exec.internal_proc +end