diff --git a/Cargo.lock b/Cargo.lock index 51627a40..18474cd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "incan" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "blake2", "blake3", @@ -1527,14 +1527,14 @@ dependencies = [ [[package]] name = "incan_core" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "proc-macro2", "quote", @@ -1543,14 +1543,14 @@ dependencies = [ [[package]] name = "incan_semantics_core" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "incan_core", ] [[package]] name = "incan_semantics_stdlib" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "incan_core", "incan_semantics_core", @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "axum", "incan_core", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "incan_syntax" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "incan_core", "incan_semantics_core", @@ -1593,7 +1593,7 @@ dependencies = [ [[package]] name = "incan_web_macros" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "proc-macro2", "quote", @@ -3151,7 +3151,7 @@ dependencies = [ [[package]] name = "rust_inspect" -version = "0.3.0-rc18" +version = "0.3.0-rc19" dependencies = [ "hex", "incan_core", diff --git a/Cargo.toml b/Cargo.toml index b193ff9d..645780cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ resolver = "2" ra_ap_proc_macro_api = { path = "crates/third_party/ra_ap_proc_macro_api" } [workspace.package] -version = "0.3.0-rc18" +version = "0.3.0-rc19" description = "The Incan programming language compiler" edition = "2024" rust-version = "1.92" diff --git a/src/backend/ir/codegen.rs b/src/backend/ir/codegen.rs index 899bd3af..7fe7b7ae 100644 --- a/src/backend/ir/codegen.rs +++ b/src/backend/ir/codegen.rs @@ -49,8 +49,8 @@ mod ordinal_bridge; mod serde_activation; use dependency_metadata::{ - collect_dependency_type_metadata, collect_externally_reachable_items_by_module, collect_model_field_aliases, - should_preserve_dependency_public_items, + DependencySymbolMetadata, collect_dependency_symbol_metadata, collect_externally_reachable_items_by_module, + collect_model_field_aliases, should_preserve_dependency_public_items, }; use ordinal_bridge::{OrdinalBridgeConfig, compilation_imports_std_ordinal_contract, imports_std_ordinal_contract}; use serde_activation::{add_serde_to_newtypes, collect_serde_derives}; @@ -219,6 +219,16 @@ impl<'a> IrCodegen<'a> { registry } + fn apply_dependency_symbol_metadata(emitter: &mut IrEmitter<'_>, metadata: &DependencySymbolMetadata) { + emitter.set_type_module_paths(metadata.module_paths.clone(), metadata.ambiguous_type_names.clone()); + emitter.set_value_module_paths( + metadata.value_module_paths.clone(), + metadata.ambiguous_value_names.clone(), + ); + emitter.set_dependency_enum_types(metadata.enum_type_names.clone()); + emitter.set_external_error_trait_types(metadata.error_trait_type_names.clone()); + } + /// Enable strict generated Rust lint validation for `--emit-rust --strict`. pub fn set_strict_generated_lints(&mut self, enabled: bool) { self.strict_generated_lints = enabled; @@ -501,7 +511,7 @@ impl<'a> IrCodegen<'a> { // RFC 021: Make alias-aware lowering work across module boundaries by seeding alias maps // for models declared in dependency modules as well. let global_aliases = collect_model_field_aliases(program, &deps); - let dependency_type_metadata = collect_dependency_type_metadata(&self.dependency_modules); + let dependency_symbol_metadata = collect_dependency_symbol_metadata(&self.dependency_modules); let uses_std_ordinal_contract = compilation_imports_std_ordinal_contract(program, &self.dependency_modules); let ordinal_bridge = self.ordinal_bridge_config(uses_std_ordinal_contract); let (needs_serialize, needs_deserialize) = collect_serde_derives(program, &deps); @@ -536,13 +546,17 @@ impl<'a> IrCodegen<'a> { // RFC 023: Infer trait bounds for generic functions. super::trait_bound_inference::infer_trait_bounds(&mut ir_program); + let callable_name_use_facts = IrEmitter::callable_name_use_facts_for_program( + &ir_program, + &self.externally_reachable_items, + true, + &dependency_symbol_metadata.error_trait_type_names, + ); if let Some(used_keys) = callable_name_used_signature_keys.as_deref_mut() { - used_keys.extend(IrEmitter::callable_name_signature_keys_for_program( - &ir_program, - &self.externally_reachable_items, - true, - &dependency_type_metadata.error_trait_type_names, - )); + used_keys.extend(callable_name_use_facts.signature_keys.iter().cloned()); + if callable_name_use_facts.generic_trait_used { + used_keys.extend(callable_name_use_facts.function_arg_signature_keys.iter().cloned()); + } } if let Some(resolutions) = callable_name_resolutions.as_deref_mut() { IrEmitter::add_callable_name_resolutions_for_program(resolutions, Vec::new(), &ir_program); @@ -551,10 +565,13 @@ impl<'a> IrCodegen<'a> { .as_ref() .map(|resolutions| (**resolutions).clone()) .unwrap_or_default(); - let callable_name_used_signature_keys_for_emit = callable_name_used_signature_keys + let mut callable_name_used_signature_keys_for_emit = callable_name_used_signature_keys .as_ref() .map(|used_keys| (**used_keys).clone()) .unwrap_or_default(); + if callable_name_use_facts.generic_trait_used { + callable_name_used_signature_keys_for_emit.extend(callable_name_use_facts.function_arg_signature_keys); + } let mut canonical_registry = FunctionRegistry::new(); let mut dependency_ir_programs = Vec::new(); @@ -595,12 +612,7 @@ impl<'a> IrCodegen<'a> { if self.emit_zen_in_main { inner.set_emit_zen(true); } - inner.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - inner.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - inner.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(inner, &dependency_symbol_metadata); inner.set_needs_serde(self.needs_serde); inner.set_external_rust_functions(self.external_rust_functions.clone()); inner.set_strict_generated_lints(self.strict_generated_lints); @@ -623,12 +635,7 @@ impl<'a> IrCodegen<'a> { if self.emit_zen_in_main { emitter.set_emit_zen(true); } - emitter.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - emitter.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - emitter.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(&mut emitter, &dependency_symbol_metadata); emitter.set_needs_serde(self.needs_serde); emitter.set_external_rust_functions(self.external_rust_functions.clone()); emitter.set_strict_generated_lints(self.strict_generated_lints); @@ -766,7 +773,7 @@ impl<'a> IrCodegen<'a> { .map(|(name, ast, _)| (*name, *ast)) .collect(); let global_aliases = collect_model_field_aliases(program, &deps); - let dependency_type_metadata = collect_dependency_type_metadata(&self.dependency_modules); + let dependency_symbol_metadata = collect_dependency_symbol_metadata(&self.dependency_modules); let uses_std_ordinal_contract = compilation_imports_std_ordinal_contract(program, &self.dependency_modules); let ordinal_bridge = OrdinalBridgeConfig::for_internal_module(uses_std_ordinal_contract); let dependency_reachable_items = @@ -832,6 +839,7 @@ impl<'a> IrCodegen<'a> { // Generate main file after dependency lowering so it can own shared crate-root union wrappers. let mut callable_name_resolutions = HashMap::new(); let mut callable_name_used_signature_keys = HashSet::new(); + let mut callable_name_function_arg_signature_keys = HashSet::new(); let mut generic_callable_name_trait_used = false; for (_, module_path, ir) in &lowered_modules { IrEmitter::add_callable_name_resolutions_for_program( @@ -845,21 +853,18 @@ impl<'a> IrCodegen<'a> { } let preserve_public_items = should_preserve_dependency_public_items(module_path, self.preserve_dependency_public_items); - callable_name_used_signature_keys.extend(IrEmitter::callable_name_signature_keys_for_program( - ir, - &reachable_items, - preserve_public_items, - &dependency_type_metadata.error_trait_type_names, - )); - generic_callable_name_trait_used |= IrEmitter::generic_callable_name_trait_used_for_program( + let callable_name_use_facts = IrEmitter::callable_name_use_facts_for_program( ir, &reachable_items, preserve_public_items, - &dependency_type_metadata.error_trait_type_names, + &dependency_symbol_metadata.error_trait_type_names, ); + callable_name_used_signature_keys.extend(callable_name_use_facts.signature_keys); + callable_name_function_arg_signature_keys.extend(callable_name_use_facts.function_arg_signature_keys); + generic_callable_name_trait_used |= callable_name_use_facts.generic_trait_used; } if generic_callable_name_trait_used { - callable_name_used_signature_keys.extend(callable_name_resolutions.keys().cloned()); + callable_name_used_signature_keys.extend(callable_name_function_arg_signature_keys); } let main_code = self.try_generate_via_ir_with_union_config( @@ -886,12 +891,7 @@ impl<'a> IrCodegen<'a> { inner.set_internal_module_roots(internal_roots.clone()); inner.set_preserve_public_items(preserve_public_items); inner.set_externally_reachable_items(reachable_items.clone()); - inner.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - inner.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - inner.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(inner, &dependency_symbol_metadata); inner.set_external_rust_functions(self.external_rust_functions.clone()); inner.set_qualify_union_types_from_crate(true); inner.set_emit_generated_union_definitions(false); @@ -909,12 +909,7 @@ impl<'a> IrCodegen<'a> { emitter.set_internal_module_roots(internal_roots.clone()); emitter.set_preserve_public_items(preserve_public_items); emitter.set_externally_reachable_items(reachable_items); - emitter.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - emitter.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - emitter.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(&mut emitter, &dependency_symbol_metadata); emitter.set_external_rust_functions(self.external_rust_functions.clone()); emitter.set_qualify_union_types_from_crate(true); emitter.set_emit_generated_union_definitions(false); @@ -1008,7 +1003,7 @@ impl<'a> IrCodegen<'a> { .map(|(name, ast, _)| (*name, *ast)) .collect(); let global_aliases = collect_model_field_aliases(program, &deps); - let dependency_type_metadata = collect_dependency_type_metadata(&self.dependency_modules); + let dependency_symbol_metadata = collect_dependency_symbol_metadata(&self.dependency_modules); let uses_std_ordinal_contract = compilation_imports_std_ordinal_contract(program, &self.dependency_modules); let ordinal_bridge = OrdinalBridgeConfig::for_internal_module(uses_std_ordinal_contract); let dependency_reachable_items = @@ -1071,6 +1066,7 @@ impl<'a> IrCodegen<'a> { // Generate main file after dependency lowering so it can own shared crate-root union wrappers. let mut callable_name_resolutions = HashMap::new(); let mut callable_name_used_signature_keys = HashSet::new(); + let mut callable_name_function_arg_signature_keys = HashSet::new(); let mut generic_callable_name_trait_used = false; for (path, ir) in &lowered_modules { IrEmitter::add_callable_name_resolutions_for_program(&mut callable_name_resolutions, path.clone(), ir); @@ -1080,21 +1076,18 @@ impl<'a> IrCodegen<'a> { } let preserve_public_items = should_preserve_dependency_public_items(path, self.preserve_dependency_public_items); - callable_name_used_signature_keys.extend(IrEmitter::callable_name_signature_keys_for_program( + let callable_name_use_facts = IrEmitter::callable_name_use_facts_for_program( ir, &reachable_items, preserve_public_items, - &dependency_type_metadata.error_trait_type_names, - )); - generic_callable_name_trait_used |= IrEmitter::generic_callable_name_trait_used_for_program( - ir, - &reachable_items, - preserve_public_items, - &dependency_type_metadata.error_trait_type_names, + &dependency_symbol_metadata.error_trait_type_names, ); + callable_name_used_signature_keys.extend(callable_name_use_facts.signature_keys); + callable_name_function_arg_signature_keys.extend(callable_name_use_facts.function_arg_signature_keys); + generic_callable_name_trait_used |= callable_name_use_facts.generic_trait_used; } if generic_callable_name_trait_used { - callable_name_used_signature_keys.extend(callable_name_resolutions.keys().cloned()); + callable_name_used_signature_keys.extend(callable_name_function_arg_signature_keys); } let main_code = self.try_generate_via_ir_with_union_config( @@ -1121,12 +1114,7 @@ impl<'a> IrCodegen<'a> { inner.set_internal_module_roots(internal_roots.clone()); inner.set_preserve_public_items(preserve_public_items); inner.set_externally_reachable_items(reachable_items.clone()); - inner.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - inner.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - inner.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(inner, &dependency_symbol_metadata); inner.set_external_rust_functions(self.external_rust_functions.clone()); inner.set_qualify_union_types_from_crate(true); inner.set_emit_generated_union_definitions(false); @@ -1144,12 +1132,7 @@ impl<'a> IrCodegen<'a> { emitter.set_internal_module_roots(internal_roots.clone()); emitter.set_preserve_public_items(preserve_public_items); emitter.set_externally_reachable_items(reachable_items); - emitter.set_type_module_paths( - dependency_type_metadata.module_paths.clone(), - dependency_type_metadata.ambiguous_type_names.clone(), - ); - emitter.set_dependency_enum_types(dependency_type_metadata.enum_type_names.clone()); - emitter.set_external_error_trait_types(dependency_type_metadata.error_trait_type_names.clone()); + Self::apply_dependency_symbol_metadata(&mut emitter, &dependency_symbol_metadata); emitter.set_external_rust_functions(self.external_rust_functions.clone()); emitter.set_qualify_union_types_from_crate(true); emitter.set_emit_generated_union_definitions(false); diff --git a/src/backend/ir/codegen/dependency_metadata.rs b/src/backend/ir/codegen/dependency_metadata.rs index 541e4cb1..f0519d14 100644 --- a/src/backend/ir/codegen/dependency_metadata.rs +++ b/src/backend/ir/codegen/dependency_metadata.rs @@ -209,21 +209,25 @@ pub(super) fn collect_externally_reachable_items_by_module( reachable } -/// Dependency type facts gathered during codegen setup and reused by module emission. +/// Dependency symbol facts gathered during codegen setup and reused by module emission. #[derive(Debug, Clone, Default)] -pub(super) struct DependencyTypeMetadata { +pub(super) struct DependencySymbolMetadata { pub(super) module_paths: HashMap>, pub(super) ambiguous_type_names: HashSet, + pub(super) value_module_paths: HashMap>, + pub(super) ambiguous_value_names: HashSet, pub(super) enum_type_names: HashSet, pub(super) error_trait_type_names: HashSet, } -/// Collect dependency type metadata needed by IR emission for cross-module nominal types. -pub(super) fn collect_dependency_type_metadata( +/// Collect dependency symbol metadata needed by IR emission for cross-module nominal types and values. +pub(super) fn collect_dependency_symbol_metadata( deps: &[(&str, &Program, Option>)], -) -> DependencyTypeMetadata { +) -> DependencySymbolMetadata { let mut paths: HashMap> = HashMap::new(); let mut ambiguous: HashSet = HashSet::new(); + let mut value_paths: HashMap> = HashMap::new(); + let mut ambiguous_values: HashSet = HashSet::new(); let mut enum_type_names: HashSet = HashSet::new(); let mut non_enum_type_names: HashSet = HashSet::new(); let mut error_trait_type_names: HashSet = HashSet::new(); @@ -231,6 +235,33 @@ pub(super) fn collect_dependency_type_metadata( for (_name, program, path_segments) in deps { for decl in &program.declarations { + if let Some(segs) = path_segments.as_ref() + && let Some(name) = match &decl.node { + Declaration::Const(c) => Some(&c.name), + Declaration::Static(s) => Some(&s.name), + Declaration::Function(f) => Some(&f.name), + Declaration::Partial(p) => Some(&p.name), + Declaration::Alias(a) => Some(&a.name), + Declaration::Import(_) + | Declaration::Model(_) + | Declaration::Class(_) + | Declaration::Trait(_) + | Declaration::TypeAlias(_) + | Declaration::Newtype(_) + | Declaration::Enum(_) + | Declaration::TestModule(_) + | Declaration::Docstring(_) => None, + } + { + if let Some(existing) = value_paths.get(name) { + if existing != segs { + ambiguous_values.insert(name.clone()); + } + } else { + value_paths.insert(name.clone(), segs.clone()); + } + } + let type_name = match &decl.node { Declaration::Model(m) => { if m.traits.iter().any(|bound| bound.node.name == error_trait_name) { @@ -276,11 +307,16 @@ pub(super) fn collect_dependency_type_metadata( for name in &ambiguous { paths.remove(name); } + for name in &ambiguous_values { + value_paths.remove(name); + } enum_type_names.retain(|name| !ambiguous.contains(name) && !non_enum_type_names.contains(name)); - DependencyTypeMetadata { + DependencySymbolMetadata { module_paths: paths, ambiguous_type_names: ambiguous, + value_module_paths: value_paths, + ambiguous_value_names: ambiguous_values, enum_type_names, error_trait_type_names, } diff --git a/src/backend/ir/emit/expressions/indexing.rs b/src/backend/ir/emit/expressions/indexing.rs index 81d53850..d7725d80 100644 --- a/src/backend/ir/emit/expressions/indexing.rs +++ b/src/backend/ir/emit/expressions/indexing.rs @@ -51,9 +51,7 @@ impl<'a> IrEmitter<'a> { return Ok(quote! { "".to_string() }); }; let callable = self.emit_expr(object)?; - let param_tokens = params.iter().map(|param| self.emit_type(param)).collect::>(); - let ret_tokens = self.emit_type(ret); - let fn_ty = quote! { fn(#(#param_tokens),*) -> #ret_tokens }; + let fn_ty = self.emit_callable_fn_type(params, ret); let helper = Self::callable_name_helper_ident(&signature_key); let mut helper_calls = Vec::new(); @@ -112,29 +110,6 @@ impl<'a> IrEmitter<'a> { iter.fold(first, |acc, segment| quote! { #acc :: #segment }) } - /// Build the fully-qualified generated-module path for a type imported from another emitted module. - /// - /// Default argument expressions can be expanded at a call site outside the module that declared the default. When - /// the default names an enum variant from that declaring module, the generated Rust must qualify the enum type - /// through the dependency module path instead of assuming the type name is locally imported. - fn emit_dependency_type_path(&self, name: &str) -> Option { - if name.contains("::") || self.ambiguous_type_names.contains(name) { - return None; - } - let module_path = self.type_module_paths.get(name)?; - let mut segments = vec![quote! { crate }]; - for segment in module_path { - let ident = Self::rust_ident(segment); - segments.push(quote! { #ident }); - } - let name_ident = Self::rust_ident(name); - segments.push(quote! { #name_ident }); - - let mut iter = segments.into_iter(); - let first = iter.next()?; - Some(iter.fold(first, |acc, segment| quote! { #acc :: #segment })) - } - /// Emit an index expression. /// /// Handles `list[i]` and `dict[k]` access with: diff --git a/src/backend/ir/emit/expressions/mod.rs b/src/backend/ir/emit/expressions/mod.rs index 3f929b96..4e1c3097 100644 --- a/src/backend/ir/emit/expressions/mod.rs +++ b/src/backend/ir/emit/expressions/mod.rs @@ -688,6 +688,11 @@ impl<'a> IrEmitter<'a> { Ok(quote! { #n.get() }) } IrExprKind::Var { name, access: _, .. } => { + if *self.qualify_internal_canonical_paths.borrow() + && let Some(path) = self.emit_dependency_value_path(name) + { + return Ok(path); + } let n = Self::rust_ident(name); Ok(quote! { #n }) } diff --git a/src/backend/ir/emit/mod.rs b/src/backend/ir/emit/mod.rs index 0ce789a0..a677203d 100644 --- a/src/backend/ir/emit/mod.rs +++ b/src/backend/ir/emit/mod.rs @@ -75,6 +75,14 @@ pub(crate) struct CallableNameResolution { pub(super) module_paths: Vec>, } +/// Callable-name usage facts collected from one lowered program. +#[derive(Debug, Clone, Default)] +pub(crate) struct CallableNameUseFacts { + pub(crate) signature_keys: HashSet, + pub(crate) function_arg_signature_keys: HashSet, + pub(crate) generic_trait_used: bool, +} + /// Usage facts collected before Rust emission. /// /// This analysis is intentionally about generated Rust lints, not source-language reachability diagnostics. It records @@ -104,6 +112,8 @@ pub(super) struct GeneratedUseAnalysis { pub(super) borrowed_function_adapters: HashSet<(String, Vec)>, /// Concrete function-pointer signatures whose values read `__name__`. pub(super) callable_name_signature_keys: HashSet, + /// Concrete top-level function signatures passed through reachable calls. + pub(super) callable_name_function_arg_signature_keys: HashSet, /// Whether a generic callable parameter reads `__name__` through the generated callable-name trait. pub(super) uses_generic_callable_name_trait: bool, } @@ -269,6 +279,10 @@ pub struct IrEmitter<'a> { type_module_paths: HashMap>, /// Type names that are declared in multiple modules (ambiguous). ambiguous_type_names: HashSet, + /// Map of value name -> module path segments for dependency modules. + value_module_paths: HashMap>, + /// Value names that are declared in multiple modules (ambiguous). + ambiguous_value_names: HashSet, /// Imported enum type names discovered from dependency modules. /// /// Imported enums usually lower to `IrType::Struct(name)` in consumer modules, so for-loop emission needs this @@ -384,6 +398,8 @@ impl<'a> IrEmitter<'a> { const_string_literals: std::collections::HashMap::new(), type_module_paths: HashMap::new(), ambiguous_type_names: HashSet::new(), + value_module_paths: HashMap::new(), + ambiguous_value_names: HashSet::new(), dependency_enum_types: HashSet::new(), external_error_trait_types: HashSet::new(), internal_module_roots: HashSet::new(), @@ -907,6 +923,47 @@ impl<'a> IrEmitter<'a> { self.ambiguous_type_names = ambiguous; } + /// Set value-to-module path mappings for dependency expressions that must be emitted outside their defining + /// module. + pub fn set_value_module_paths(&mut self, paths: HashMap>, ambiguous: HashSet) { + self.value_module_paths = paths; + self.ambiguous_value_names = ambiguous; + } + + pub(in crate::backend::ir::emit) fn emit_dependency_item_path( + &self, + module_path: &[String], + name: &str, + ) -> Option { + let mut segments = vec![quote! { crate }]; + for segment in module_path { + let ident = Self::rust_ident(segment); + segments.push(quote! { #ident }); + } + let ident = Self::rust_ident(name); + segments.push(quote! { #ident }); + + let mut iter = segments.into_iter(); + let first = iter.next()?; + Some(iter.fold(first, |acc, segment| quote! { #acc :: #segment })) + } + + pub(in crate::backend::ir::emit) fn emit_dependency_type_path(&self, name: &str) -> Option { + if name.contains("::") || self.ambiguous_type_names.contains(name) { + return None; + } + let module_path = self.type_module_paths.get(name)?; + self.emit_dependency_item_path(module_path, name) + } + + pub(in crate::backend::ir::emit) fn emit_dependency_value_path(&self, name: &str) -> Option { + if name.contains("::") || self.ambiguous_value_names.contains(name) { + return None; + } + let module_path = self.value_module_paths.get(name)?; + self.emit_dependency_item_path(module_path, name) + } + /// Set imported enum type names discovered during codegen setup. pub fn set_dependency_enum_types(&mut self, enum_type_names: HashSet) { self.dependency_enum_types = enum_type_names; diff --git a/src/backend/ir/emit/program.rs b/src/backend/ir/emit/program.rs index d7219c5e..9947d01f 100644 --- a/src/backend/ir/emit/program.rs +++ b/src/backend/ir/emit/program.rs @@ -20,7 +20,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use incan_core::lang::surface::result_methods::ResultMethodId; use incan_core::lang::traits::{self as core_traits, TraitId}; @@ -36,7 +36,7 @@ use super::super::expr::{ use super::super::stmt::AssignTarget; use super::super::types::{IR_UNION_TYPE_NAME, IrType}; use super::super::{FunctionRegistry, FunctionSignature, IrDecl, IrProgram, IrStmt, IrStmtKind, TypedExpr}; -use super::{EmitError, GeneratedUseAnalysis, IrEmitter}; +use super::{CallableNameUseFacts, EmitError, GeneratedUseAnalysis, IrEmitter}; struct OrdinalValueEnumBridgeSpec { type_path: TokenStream, @@ -571,6 +571,9 @@ impl<'program> GeneratedUseAnalyzer<'program> { self.scan_type(ty); } for arg in args { + for key in self.callable_name_function_arg_signature_keys(&arg.expr) { + self.analysis.callable_name_function_arg_signature_keys.insert(key); + } self.scan_expr(&arg.expr); } } @@ -791,6 +794,52 @@ impl<'program> GeneratedUseAnalyzer<'program> { } } + fn callable_name_function_arg_signature_keys(&self, expr: &TypedExpr) -> Vec { + match &expr.kind { + IrExprKind::Var { name, .. } => { + let mut keys = HashSet::new(); + if let IrType::Function { params, ret } = &expr.ty + && let Some(key) = IrEmitter::callable_name_signature_key(params, ret) + { + keys.insert(key); + } + if let Some(signature) = self.function_registry.get(name) { + let params = signature + .params + .iter() + .map(|param| param.ty.clone()) + .collect::>(); + if let Some(key) = IrEmitter::callable_name_signature_key(¶ms, &signature.return_type) { + keys.insert(key); + } + } + let Some(IrDecl { + kind: IrDeclKind::Function(func), + .. + }) = self.declarations_by_name.get(name).copied() + else { + let mut keys = keys.into_iter().collect::>(); + keys.sort(); + return keys; + }; + if func.is_async || !func.type_params.is_empty() { + return Vec::new(); + } + let params = func.params.iter().map(|param| param.ty.clone()).collect::>(); + if let Some(key) = IrEmitter::callable_name_signature_key(¶ms, &func.return_type) { + keys.insert(key); + } + let mut keys = keys.into_iter().collect::>(); + keys.sort(); + keys + } + IrExprKind::InteropCoerce { expr, .. } + | IrExprKind::NumericResize { expr, .. } + | IrExprKind::Cast { expr, .. } => self.callable_name_function_arg_signature_keys(expr), + _ => Vec::new(), + } + } + /// Record non-Copy observer callbacks that need generated borrowed helper items. fn record_result_observer_callback( &mut self, @@ -2070,13 +2119,13 @@ impl<'a> IrEmitter<'a> { fn emit_generated_union_member_type(&self, ty: &IrType) -> TokenStream { match ty { IrType::Struct(name) | IrType::Enum(name) | IrType::Trait(name) => self - .emit_dependency_nominal_type_path(name) + .emit_dependency_type_path(name) .unwrap_or_else(|| self.emit_type(ty)), IrType::NamedGeneric(name, args) if name == super::super::types::IR_UNION_TYPE_NAME => { self.emit_union_type_path(ty) } IrType::NamedGeneric(name, args) => { - let base = self.emit_dependency_nominal_type_path(name).unwrap_or_else(|| { + let base = self.emit_dependency_type_path(name).unwrap_or_else(|| { let ident = Self::rust_ident(name); quote! { #ident } }); @@ -2151,25 +2200,6 @@ impl<'a> IrEmitter<'a> { } } - /// Emit a crate-qualified path for an unambiguous nominal type declared in a dependency module. - fn emit_dependency_nominal_type_path(&self, name: &str) -> Option { - if name.contains("::") || self.ambiguous_type_names.contains(name) { - return None; - } - let module_path = self.type_module_paths.get(name)?; - let mut segments = vec![quote! { crate }]; - for segment in module_path { - let ident = Self::rust_ident(segment); - segments.push(quote! { #ident }); - } - let name_ident = Self::rust_ident(name); - segments.push(quote! { #name_ident }); - - let mut iter = segments.into_iter(); - let first = iter.next()?; - Some(iter.fold(first, |acc, segment| quote! { #acc :: #segment })) - } - /// Emit a complete IR program to formatted Rust code. #[tracing::instrument(skip_all, fields(decl_count = program.declarations.len()))] pub fn emit_program(&mut self, program: &IrProgram) -> Result { @@ -2278,34 +2308,23 @@ impl<'a> IrEmitter<'a> { Ok(format!("{}{}", header, with_marker)) } - pub(crate) fn callable_name_signature_keys_for_program( + pub(crate) fn callable_name_use_facts_for_program( program: &IrProgram, externally_reachable_items: &HashSet, preserve_public_items: bool, external_error_trait_types: &HashSet, - ) -> HashSet { - GeneratedUseAnalyzer::analyze( - program, - externally_reachable_items, - preserve_public_items, - external_error_trait_types, - ) - .callable_name_signature_keys - } - - pub(crate) fn generic_callable_name_trait_used_for_program( - program: &IrProgram, - externally_reachable_items: &HashSet, - preserve_public_items: bool, - external_error_trait_types: &HashSet, - ) -> bool { - GeneratedUseAnalyzer::analyze( + ) -> CallableNameUseFacts { + let analysis = GeneratedUseAnalyzer::analyze( program, externally_reachable_items, preserve_public_items, external_error_trait_types, - ) - .uses_generic_callable_name_trait + ); + CallableNameUseFacts { + signature_keys: analysis.callable_name_signature_keys, + function_arg_signature_keys: analysis.callable_name_function_arg_signature_keys, + generic_trait_used: analysis.uses_generic_callable_name_trait, + } } fn callable_name_signature_for_key(&self, key: &str) -> Option<(Vec, IrType)> { @@ -2330,29 +2349,15 @@ impl<'a> IrEmitter<'a> { fn callable_name_helper_keys( &self, local_callable_name_signature_keys: &HashSet, - include_all_callable_signatures: bool, + include_generic_callable_signatures: bool, ) -> Vec { let mut keys = local_callable_name_signature_keys.clone(); - if include_all_callable_signatures { - for (_, signature) in self.callable_name_local_registry().iter() { - let params = signature - .params - .iter() - .map(|param| param.ty.clone()) - .collect::>(); - if let Some(key) = Self::callable_name_signature_key(¶ms, &signature.return_type) { - keys.insert(key); - } - } - keys.extend( - self.callable_name_resolutions - .iter() - .filter(|(_, resolution)| { - self.callable_name_current_module_path.is_empty() - || resolution.module_paths.iter().any(|path| !path.is_empty()) - }) - .map(|(key, _)| key.clone()), - ); + if include_generic_callable_signatures { + keys.extend(self.callable_name_used_signature_keys.iter().filter_map(|key| { + self.callable_name_signature_for_key(key) + .is_some() + .then_some(key.clone()) + })); } for (key, resolution) in &self.callable_name_resolutions { if self.callable_name_used_signature_keys.contains(key) @@ -2368,7 +2373,12 @@ impl<'a> IrEmitter<'a> { keys } - fn callable_name_resolution_expr(&self, key: &str, callable_tokens: TokenStream) -> TokenStream { + fn callable_name_resolution_expr_with_fallback( + &self, + key: &str, + callable_tokens: TokenStream, + fallback: TokenStream, + ) -> TokenStream { let helper = Self::callable_name_helper_ident(key); let mut helper_calls = Vec::new(); helper_calls.push(quote! { #helper(#callable_tokens) }); @@ -2384,8 +2394,7 @@ impl<'a> IrEmitter<'a> { helper_calls.push(quote! { #helper_path(#callable_tokens) }); } } - let fallback = proc_macro2::Literal::string(""); - let mut resolved = quote! { #fallback.to_string() }; + let mut resolved = fallback; for helper_call in helper_calls.into_iter().rev() { resolved = quote! { if let Some(__incan_name) = #helper_call { @@ -2403,14 +2412,35 @@ impl<'a> IrEmitter<'a> { return None; } let trait_ident = Self::rust_ident("__IncanCallableName"); - let impls = keys - .iter() - .filter_map(|key| { - let (params, ret) = self.callable_name_signature_for_key(key)?; - let param_tokens = params.iter().map(|param| self.emit_type(param)).collect::>(); - let ret_tokens = self.emit_type(&ret); - let fn_ty = quote! { fn(#(#param_tokens),*) -> #ret_tokens }; - let resolved = self.callable_name_resolution_expr(key, quote! { __incan_callable }); + let mut grouped_keys: BTreeMap> = BTreeMap::new(); + for key in keys { + let Some((params, ret)) = self.callable_name_signature_for_key(key) else { + continue; + }; + let resolved_params = params + .iter() + .map(|param| self.resolve_type_aliases_for_emit(param)) + .collect::>(); + let resolved_ret = self.resolve_type_aliases_for_emit(&ret); + let Some(resolved_key) = Self::callable_name_signature_key(&resolved_params, &resolved_ret) else { + continue; + }; + grouped_keys.entry(resolved_key).or_default().push(key.clone()); + } + + let impls = grouped_keys + .values_mut() + .filter_map(|keys| { + keys.sort(); + let primary_key = keys.first()?; + let (params, ret) = self.callable_name_signature_for_key(primary_key)?; + let fn_ty = self.emit_callable_fn_type(¶ms, &ret); + let fallback = proc_macro2::Literal::string(""); + let mut resolved = quote! { #fallback.to_string() }; + for key in keys.iter().rev() { + resolved = + self.callable_name_resolution_expr_with_fallback(key, quote! { __incan_callable }, resolved); + } Some(quote! { impl #trait_ident for #fn_ty { fn __incan_callable_name(&self) -> String { @@ -2442,9 +2472,7 @@ impl<'a> IrEmitter<'a> { .filter_map(|key| { let (params, ret) = self.callable_name_signature_for_key(key)?; let helper = Self::callable_name_helper_ident(key); - let param_tokens = params.iter().map(|param| self.emit_type(param)).collect::>(); - let ret_tokens = self.emit_type(&ret); - let fn_ty = quote! { fn(#(#param_tokens),*) -> #ret_tokens }; + let fn_ty = self.emit_callable_fn_type(¶ms, &ret); let mut candidates = self .callable_name_local_registry() .iter() diff --git a/src/backend/ir/emit/types.rs b/src/backend/ir/emit/types.rs index 66876efa..6604a640 100644 --- a/src/backend/ir/emit/types.rs +++ b/src/backend/ir/emit/types.rs @@ -122,6 +122,11 @@ impl<'a> IrEmitter<'a> { if name == surface_types::as_str(SurfaceTypeId::ValidationError) { return quote! { incan_stdlib::validation::ValidationError }; } + if *self.qualify_internal_canonical_paths.borrow() + && let Some(path) = self.emit_dependency_type_path(name) + { + return path; + } Self::emit_path_ident(name) } IrType::NamedGeneric(name, _) if name == super::super::types::IR_UNION_TYPE_NAME => { @@ -135,11 +140,15 @@ impl<'a> IrEmitter<'a> { Some(CollectionTypeId::Generator) => Some(quote! { incan_stdlib::iter::Generator }), _ => None, }; - let n = Self::emit_path_ident(name); let ts: Vec<_> = args.iter().map(|t| self.emit_type(t)).collect(); if let Some(n) = frozen_name { quote! { #n < #(#ts),* > } + } else if *self.qualify_internal_canonical_paths.borrow() + && let Some(n) = self.emit_dependency_type_path(name) + { + quote! { #n < #(#ts),* > } } else { + let n = Self::emit_path_ident(name); quote! { #n < #(#ts),* > } } } @@ -171,6 +180,14 @@ impl<'a> IrEmitter<'a> { } } + pub(in crate::backend::ir::emit) fn emit_callable_fn_type(&self, params: &[IrType], ret: &IrType) -> TokenStream { + let previous = self.qualify_internal_canonical_paths.replace(true); + let param_tokens = params.iter().map(|param| self.emit_type(param)).collect::>(); + let ret_tokens = self.emit_type(ret); + self.qualify_internal_canonical_paths.replace(previous); + quote! { fn(#(#param_tokens),*) -> #ret_tokens } + } + // ======================================================================== // RFC 023: Type parameter emission with trait bounds // ======================================================================== diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 2c65436c..a8f51397 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1941,6 +1941,78 @@ def test_decorator_can_infer_name_with_imported_partial_spec() -> None: Ok(()) } +#[test] +fn test_imported_partial_default_symbols_survive_decorator_argument_issue701() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "imported_partial_default_symbols_decorator", "")?; + let src_dir = main_path.parent().ok_or("main path had no parent")?; + let tests_dir = tmp.path().join("tests"); + fs::create_dir_all(&tests_dir)?; + fs::write( + src_dir.join("registry.incn"), + r#"pub const DEFAULT_NAMESPACE: str = "core" + + +pub enum Policy(str): + Portable = "portable" + + +pub model Spec: + pub namespace: str + pub policy: Policy + pub lifecycle: str + + +pub static namespaces: list[str] = [] +pub static names: list[str] = [] + + +pub spec = partial Spec(namespace=DEFAULT_NAMESPACE, policy=Policy.Portable) + + +pub def capture(func: (int) -> int) -> ((int) -> int): + names.append(func.__name__) + return func + + +pub def add(spec_value: Spec) -> (((int) -> int) -> ((int) -> int)): + namespaces.append(spec_value.namespace) + return capture +"#, + )?; + fs::write( + src_dir.join("helpers.incn"), + r#"from registry import add, spec + + +@add(spec(lifecycle="v1")) +pub def sample(value: int) -> int: + return value + 1 +"#, + )?; + fs::write( + tests_dir.join("test_partial_default_symbols.incn"), + r#"from helpers import sample +from registry import names, namespaces + + +def test_partial_default_symbols_in_decorator() -> None: + assert sample(1) == 2 + assert names[0] == "sample" + assert namespaces[0] == "core" +"#, + )?; + + let test_path = tests_dir.join("test_partial_default_symbols.incn"); + let test_output = run_incan( + tmp.path(), + &["test", test_path.to_str().ok_or("test path was not valid UTF-8")?], + )?; + assert_success(&test_output, "incan test for imported partial default symbols issue701"); + Ok(()) +} + #[test] fn test_decorator_callable_exposes_source_name_issue694() -> Result<(), Box> { let tmp = tempfile::tempdir()?; @@ -2045,6 +2117,143 @@ def test_generic_decorator_can_read_callable_name() -> None: Ok(()) } +#[test] +fn test_generic_decorator_callable_name_accepts_imported_alias_union_issue701() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "generic_callable_name_imported_alias_union", "")?; + let src_dir = main_path.parent().ok_or("main path had no parent")?; + let tests_dir = tmp.path().join("tests"); + fs::create_dir_all(&tests_dir)?; + fs::write( + src_dir.join("types.incn"), + r#"pub model A: + pub value: int + + +pub model B: + pub value: int + + +pub type Expr = Union[A, B] +"#, + )?; + fs::write( + src_dir.join("registry.incn"), + r#"pub static names: list[str] = [] + + +pub def capture[F](func: F) -> F: + names.append(func.__name__) + return func + + +pub def register[F]() -> ((F) -> F): + return (func) => capture[F](func) +"#, + )?; + fs::write( + src_dir.join("helpers.incn"), + r#"from registry import register +from types import Expr + + +@register[(Expr) -> Expr]() +pub def identity_expr(value: Expr) -> Expr: + return value +"#, + )?; + fs::write( + tests_dir.join("test_alias_union_callable_name.incn"), + r#"from helpers import identity_expr +from registry import names +from types import A + + +def test_alias_union_callable_name() -> None: + identity_expr(A(value=1)) + assert names[0] == "identity_expr" +"#, + )?; + + let test_path = tests_dir.join("test_alias_union_callable_name.incn"); + let test_output = run_incan( + tmp.path(), + &["test", test_path.to_str().ok_or("test path was not valid UTF-8")?], + )?; + assert_success( + &test_output, + "incan test for alias/union generic callable name issue701", + ); + Ok(()) +} + +#[test] +fn test_generic_callable_name_planning_ignores_unrelated_async_signatures_issue701() +-> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "generic_callable_name_with_async_noise", "")?; + let src_dir = main_path.parent().ok_or("main path had no parent")?; + let tests_dir = tmp.path().join("tests"); + fs::create_dir_all(&tests_dir)?; + fs::write( + src_dir.join("registry.incn"), + r#"pub static names: list[str] = [] + + +pub def capture[F](func: F) -> F: + names.append(func.__name__) + return func + + +pub def register[F]() -> ((F) -> F): + return (func) => capture[F](func) +"#, + )?; + fs::write( + src_dir.join("helpers.incn"), + r#"from registry import register + + +@register[(int) -> int]() +pub def sample(value: int) -> int: + return value + 1 +"#, + )?; + fs::write( + src_dir.join("noise.incn"), + r#"pub async def unrelated_async(delay: float) -> None: + return + + +pub def unrelated_generic[T](value: T) -> T: + return value +"#, + )?; + fs::write( + tests_dir.join("test_scoped_callable_name_planning.incn"), + r#"from helpers import sample +from registry import names + + +def test_generic_callable_name_ignores_unrelated_signatures() -> None: + assert sample(1) == 2 + assert names[0] == "sample" +"#, + )?; + + let test_path = tests_dir.join("test_scoped_callable_name_planning.incn"); + let test_output = run_incan( + tmp.path(), + &["test", test_path.to_str().ok_or("test path was not valid UTF-8")?], + )?; + assert_success( + &test_output, + "incan test for scoped generic callable-name planning issue701", + ); + Ok(()) +} + #[test] fn build_frozen_uses_existing_lockfile_without_network() -> Result<(), Box> { let tmp = tempfile::tempdir()?; diff --git a/workspaces/docs-site/docs/release_notes/0_3.md b/workspaces/docs-site/docs/release_notes/0_3.md index 489145d0..cf27f9c4 100644 --- a/workspaces/docs-site/docs/release_notes/0_3.md +++ b/workspaces/docs-site/docs/release_notes/0_3.md @@ -106,9 +106,9 @@ This section is grouped by outcome rather than by every minimized repro. Issue n - **Cross-module codegen is more predictable**: Imported defaults qualify correctly, same-shaped union wrappers are shared, wide union narrowing lowers fully, keyword-named modules escape consistently, and public submodule reexports work under `src/` (#395, #457, #461, #458, #122, #287). - **Web registration keeps private internals private**: Private route handlers and models are retained for web registration without making them user-visible public API (#117). - **Package exports match ordinary builds**: Public aliases, public partial presets, package-boundary alias consumption, lowercase exported statics, imported static decorator strings, and keyword-named public symbols follow the same rules across build modes (#617, #631, #633, #658, #659, #698). -- **Partial presets keep their defaults in decorators**: Imported public partials now retain their projected default arguments when used inside decorator factory arguments, matching ordinary runtime calls (#698). +- **Partial presets keep their defaults in decorators**: Imported public partials now retain their projected default arguments and module-owned default symbols when used inside decorator factory arguments, matching ordinary runtime calls (#698, #701). - **Decorator metadata crosses package boundaries**: Source signatures, imported/decorator `const str` arguments, generic decorator factories, method-call decorator factories, and reexport-only facade projections are represented in checked metadata more reliably (#636, #638, #640, #669, #694, #695). -- **Decorator helpers can inspect generic callables**: `func.__name__` works in generic `(F) -> F` decorator helpers, so registry decorators can infer the decorated helper name instead of repeating it as a string (#694). +- **Decorator helpers can inspect generic callables**: `func.__name__` works in generic `(F) -> F` decorator helpers, including imported alias and union callable signatures, so registry decorators can infer the decorated helper name instead of repeating it as a string (#694, #701). - **Script and test manifests are scoped**: Generated Cargo manifests include only reachable dependencies instead of blindly inheriting package-level heavy dependencies (#665). ### Formatter And Test Runner