diff --git a/Cargo.lock b/Cargo.lock index 18474cd7..44e46622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "incan" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "blake2", "blake3", @@ -1527,14 +1527,14 @@ dependencies = [ [[package]] name = "incan_core" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "proc-macro2", "quote", @@ -1543,14 +1543,14 @@ dependencies = [ [[package]] name = "incan_semantics_core" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "incan_core", ] [[package]] name = "incan_semantics_stdlib" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "incan_core", "incan_semantics_core", @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "axum", "incan_core", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "incan_syntax" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "incan_core", "incan_semantics_core", @@ -1593,7 +1593,7 @@ dependencies = [ [[package]] name = "incan_web_macros" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "proc-macro2", "quote", @@ -3151,7 +3151,7 @@ dependencies = [ [[package]] name = "rust_inspect" -version = "0.3.0-rc19" +version = "0.3.0-rc20" dependencies = [ "hex", "incan_core", diff --git a/Cargo.toml b/Cargo.toml index 645780cf..c3b452e4 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-rc19" +version = "0.3.0-rc20" 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 7fe7b7ae..16efcea4 100644 --- a/src/backend/ir/codegen.rs +++ b/src/backend/ir/codegen.rs @@ -204,10 +204,11 @@ impl<'a> IrCodegen<'a> { fn canonical_registry_for_programs<'program>( programs: impl IntoIterator, ) -> FunctionRegistry { + let programs: Vec<_> = programs.into_iter().collect(); let mut registry = FunctionRegistry::new(); - for (module_path, program) in programs { + for (module_path, program) in &programs { for (name, signature) in program.function_registry.iter() { - let mut canonical_path = module_path.to_vec(); + let mut canonical_path = (*module_path).to_vec(); canonical_path.push(name.clone()); registry.register_canonical_path( &canonical_path, @@ -216,6 +217,39 @@ impl<'a> IrCodegen<'a> { ); } } + + let mut pending_reexports = Vec::new(); + for (module_path, program) in &programs { + for reexport in &program.function_reexports { + let mut alias_path = (*module_path).to_vec(); + alias_path.push(reexport.name.clone()); + pending_reexports.push((alias_path, reexport.target_path.clone())); + } + } + while !pending_reexports.is_empty() { + let mut unresolved = Vec::new(); + let mut made_progress = false; + for (alias_path, target_path) in pending_reexports { + if registry.get_canonical_path(&alias_path).is_some() { + made_progress = true; + continue; + } + if let Some(signature) = registry.get_canonical_path(&target_path).cloned() { + registry.register_canonical_path( + &alias_path, + signature.params.clone(), + signature.return_type.clone(), + ); + made_progress = true; + } else { + unresolved.push((alias_path, target_path)); + } + } + if !made_progress { + break; + } + pending_reexports = unresolved; + } registry } @@ -573,34 +607,42 @@ impl<'a> IrCodegen<'a> { 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(); for (dep_name, dep_ast, dep_path_segments) in &self.dependency_modules { - // For dependencies, use best-effort lowering without type info to - // preserve prior behavior and avoid redundant typechecking. - let mut dep_lowering = AstLowering::new(); + let dep_type_info = { + use crate::frontend::typechecker::TypeChecker; + let mut tc = TypeChecker::new(); + self.configure_typechecker(&mut tc); + match tc.check_with_imports_allow_private(dep_ast, &deps) { + Ok(()) => tc.type_info().clone(), + Err(errs) => return Err(GenerationError::TypeCheck(errs)), + } + }; + let mut dep_lowering = AstLowering::new_with_type_info(dep_type_info); dep_lowering.set_current_source_module_name( - dep_ast - .source_path - .as_deref() - .and_then(crate::frontend::module::logical_module_name_from_source_path), + dep_path_segments + .clone() + .map(|segments| segments.join(".")) + .or_else(|| { + dep_ast + .source_path + .as_deref() + .and_then(crate::frontend::module::logical_module_name_from_source_path) + }), ); + dep_lowering.seed_dependency_trait_decls(&self.dependency_modules); dep_lowering.seed_struct_field_aliases(global_aliases.clone()); let dep_ir = dep_lowering.lower_program(dep_ast)?; let module_path = dep_path_segments .clone() .unwrap_or_else(|| vec![(*dep_name).to_string()]); - for (name, signature) in dep_ir.function_registry.iter() { - let mut canonical_path = module_path.clone(); - canonical_path.push(name.clone()); - canonical_registry.register_canonical_path( - &canonical_path, - signature.params.clone(), - signature.return_type.clone(), - ); - } - dependency_ir_programs.push(dep_ir); + dependency_ir_programs.push((module_path, dep_ir)); } + let canonical_registry = Self::canonical_registry_for_programs( + dependency_ir_programs + .iter() + .map(|(module_path, dep_ir)| (module_path.as_slice(), dep_ir)), + ); // Emit IR to Rust code let use_emit_service = env::var("INCAN_EMIT_SERVICE").ok().as_deref() == Some("1"); @@ -625,7 +667,7 @@ impl<'a> IrCodegen<'a> { inner.set_callable_name_resolutions(callable_name_resolutions_for_emit); inner.set_callable_name_used_signature_keys(callable_name_used_signature_keys_for_emit); inner.set_callable_name_local_registry(ir_program.function_registry.clone()); - for dep_ir in &dependency_ir_programs { + for (_, dep_ir) in &dependency_ir_programs { inner.seed_dependency_nominal_metadata_from_program(dep_ir); } Ok(svc.emit_program(&ir_program)?) @@ -648,7 +690,7 @@ impl<'a> IrCodegen<'a> { emitter.set_callable_name_resolutions(callable_name_resolutions_for_emit); emitter.set_callable_name_used_signature_keys(callable_name_used_signature_keys_for_emit); emitter.set_callable_name_local_registry(ir_program.function_registry.clone()); - for dep_ir in &dependency_ir_programs { + for (_, dep_ir) in &dependency_ir_programs { emitter.seed_dependency_nominal_metadata_from_program(dep_ir); } Ok(emitter.emit_program(&ir_program)?) diff --git a/src/backend/ir/emit/expressions/calls.rs b/src/backend/ir/emit/expressions/calls.rs index 82726634..e499cc05 100644 --- a/src/backend/ir/emit/expressions/calls.rs +++ b/src/backend/ir/emit/expressions/calls.rs @@ -7,12 +7,12 @@ mod testing_asserts; use proc_macro2::TokenStream; use quote::quote; -use super::super::super::FunctionSignature; use super::super::super::conversions::{BinOpEmitKind, determine_binop_plan}; use super::super::super::decl::FunctionParam; use super::super::super::expr::{BinOp, IrCallArg, IrCallArgKind, IrExprKind, TypedExpr, VarRefKind}; use super::super::super::ownership::{ArgumentPassingPlan, ValueUseSite}; use super::super::super::types::IrType; +use super::super::super::{FunctionRegistry, FunctionSignature}; use super::super::{EmitError, IrEmitter}; use crate::frontend::ast::ParamKind; use incan_core::lang::stdlib; @@ -499,25 +499,21 @@ impl<'a> IrEmitter<'a> { _ => None, }; let callee_name = local_name.or(canonical_name); - let registry_signature = if let Some(path) = canonical_path { - self.canonical_function_registry().get_canonical_path(path) - } else { - local_name.and_then(|name| self.function_registry.get(name)) - }; - let result_specialized_signature = callable_signature.or(registry_signature).and_then(|signature| { + let merged_signature = FunctionRegistry::effective_call_signature_by( + self.function_registry, + self.canonical_function_registry(), + local_name, + canonical_path, + callable_signature, + Some(&func.ty), + |left, right| self.call_signature_type_matches(left, right), + ); + let result_specialized_signature = merged_signature.as_ref().and_then(|signature| { result_target_ty.and_then(|target_ty| Self::specialize_signature_by_result_target(signature, target_ty)) }); - let function_sig = associated_signature.as_ref().or_else(|| { - if canonical_path.is_some() { - result_specialized_signature - .as_ref() - .or(callable_signature.or(registry_signature)) - } else { - result_specialized_signature - .as_ref() - .or(registry_signature.or(callable_signature)) - } - }); + let function_sig = associated_signature + .as_ref() + .or_else(|| result_specialized_signature.as_ref().or(merged_signature.as_ref())); // The checked-newtype lowering path emits a compiler-internal panic marker call. This remains the narrow, // explicitly-tracked generated `panic!` exemption that issue #351 left to a separate follow-up. Render it as // the Rust `panic!` macro so generated code stays valid without colliding with user-defined functions that may diff --git a/src/backend/ir/emit/expressions/methods.rs b/src/backend/ir/emit/expressions/methods.rs index e1c5093f..c43d7cec 100644 --- a/src/backend/ir/emit/expressions/methods.rs +++ b/src/backend/ir/emit/expressions/methods.rs @@ -272,17 +272,11 @@ impl<'a> IrEmitter<'a> { .method_signature_for_receiver(&receiver.ty, method) .or(specialized_signature.as_ref()); let has_incan_receiver_signature = receiver_signature.is_some(); - let callable_signature = match (callable_signature, receiver_signature) { - (Some(call_sig), Some(method_sig)) - if call_sig.params.iter().all(|param| param.default.is_none()) - && method_sig.params.iter().any(|param| param.default.is_some()) => - { - Some(method_sig) - } - (Some(call_sig), _) => Some(call_sig), - (None, method_sig) => method_sig, - }; - if let Some(sig) = callable_signature + let callable_signature = + FunctionSignature::merge_default_source_by(callable_signature, receiver_signature, |left, right| { + self.call_signature_type_matches(left, right) + }); + if let Some(sig) = callable_signature.as_ref() && sig .params .iter() @@ -291,7 +285,7 @@ impl<'a> IrEmitter<'a> { return self.emit_rest_aware_call_args(receiver, args, sig); } - let ordered_args: Vec<(TypedExpr, bool)> = if let Some(sig) = callable_signature { + let ordered_args: Vec<(TypedExpr, bool)> = if let Some(sig) = callable_signature.as_ref() { if args.iter().any(|arg| arg.name.is_some()) { let mut positional: Vec = Vec::new(); let mut named: std::collections::HashMap<&str, TypedExpr> = std::collections::HashMap::new(); @@ -335,7 +329,7 @@ impl<'a> IrEmitter<'a> { .iter() .enumerate() .map(|(idx, (arg, from_default))| { - let param = callable_signature.and_then(|sig| sig.params.get(idx)); + let param = callable_signature.as_ref().and_then(|sig| sig.params.get(idx)); let external_method_shape = matches!( base_use_site, ValueUseSite::ExternalCallArg { .. } | ValueUseSite::MethodArg diff --git a/src/backend/ir/emit/mod.rs b/src/backend/ir/emit/mod.rs index a677203d..9d47cde1 100644 --- a/src/backend/ir/emit/mod.rs +++ b/src/backend/ir/emit/mod.rs @@ -567,6 +567,11 @@ impl<'a> IrEmitter<'a> { .unwrap_or(self.function_registry) } + /// Return whether two call-signature types describe the same emitted surface after transparent aliases expand. + pub(in crate::backend::ir::emit) fn call_signature_type_matches(&self, left: &IrType, right: &IrType) -> bool { + left == right || self.resolve_type_aliases_for_emit(left) == self.resolve_type_aliases_for_emit(right) + } + /// Resolve transparent type aliases before emission decisions that need structural type information. pub(in crate::backend::ir::emit) fn resolve_type_aliases_for_emit(&self, ty: &IrType) -> IrType { let mut visiting = HashSet::new(); diff --git a/src/backend/ir/emit/program.rs b/src/backend/ir/emit/program.rs index 9947d01f..9ba889be 100644 --- a/src/backend/ir/emit/program.rs +++ b/src/backend/ir/emit/program.rs @@ -887,37 +887,14 @@ impl<'program> GeneratedUseAnalyzer<'program> { IrExprKind::Var { name, .. } => Some(name.as_str()), _ => None, }; - let canonical_name = canonical_path.as_ref().and_then(|path| path.last()).map(String::as_str); - let registered_signature = if canonical_path.is_some() { - callable_signature.cloned().or_else(|| { - canonical_path - .as_ref() - .and_then(|path| self.function_registry.get_canonical_path(path).cloned()) - }) - } else { - local_name - .and_then(|name| self.function_registry.get(name).cloned()) - .or_else(|| canonical_name.and_then(|name| self.function_registry.get(name).cloned())) - .or_else(|| callable_signature.cloned()) - }; - registered_signature.or_else(|| match &func.ty { - IrType::Function { params, ret } => Some(FunctionSignature { - params: params - .iter() - .enumerate() - .map(|(idx, ty)| super::super::decl::FunctionParam { - name: format!("__incan_arg_{idx}"), - ty: ty.clone(), - mutability: super::super::types::Mutability::Immutable, - is_self: false, - kind: crate::frontend::ast::ParamKind::Normal, - default: None, - }) - .collect(), - return_type: ret.as_ref().clone(), - }), - _ => None, - }) + FunctionRegistry::effective_call_signature( + self.function_registry, + self.function_registry, + local_name, + canonical_path.as_deref(), + callable_signature, + Some(&func.ty), + ) } /// Record named function arguments that need private adapters for borrowed function-pointer parameters. diff --git a/src/backend/ir/lower/decl/methods.rs b/src/backend/ir/lower/decl/methods.rs index b40224e0..f6596075 100644 --- a/src/backend/ir/lower/decl/methods.rs +++ b/src/backend/ir/lower/decl/methods.rs @@ -354,7 +354,7 @@ impl AstLowering { type_param_names, )?; let adapter = self.decorated_method_original_adapter(owner, method)?; - let wrapper = self.lower_decorated_method_wrapper(owner, method)?; + let wrapper = self.lower_decorated_method_wrapper(owner, method, type_param_names)?; Ok(vec![original, adapter, wrapper]) } else { Ok(vec![self.lower_method_with_type_params(method, type_param_names)?]) @@ -366,6 +366,7 @@ impl AstLowering { &mut self, owner: &str, method: &ast::MethodDecl, + owner_type_param_names: Option<&HashSet<&str>>, ) -> Result { let Some(binding) = self.type_info.as_ref().and_then(|info| { info.declarations @@ -373,15 +374,23 @@ impl AstLowering { .get(&(owner.to_string(), method.name.clone())) .cloned() }) else { - return self.lower_method_with_type_params(method, None); + return self.lower_method_with_type_params(method, owner_type_param_names); }; let crate::frontend::symbols::ResolvedType::Function(params, ret) = binding.unbound_ty else { - return self.lower_method_with_type_params(method, None); + return self.lower_method_with_type_params(method, owner_type_param_names); }; let Some((receiver_param, surface_params)) = params.split_first() else { - return self.lower_method_with_type_params(method, None); + return self.lower_method_with_type_params(method, owner_type_param_names); }; let receiver_ty = self.lower_resolved_type(&receiver_param.ty); + let original_surface_params = match binding.original_unbound_ty { + crate::frontend::symbols::ResolvedType::Function(original_params, _) => { + original_params.into_iter().skip(1).collect::>() + } + _ => Vec::new(), + }; + let defaults = + self.decorated_param_defaults_for_surface(surface_params, &original_surface_params, &method.params); let mut wrapper_params = Vec::with_capacity(surface_params.len() + 1); let receiver = method.receiver.unwrap_or(ast::Receiver::Immutable); wrapper_params.push(FunctionParam { @@ -404,7 +413,7 @@ impl AstLowering { mutability: Mutability::Immutable, is_self: false, kind: param.kind, - default: None, + default: defaults.get(idx).cloned().flatten(), } })); let return_type = self.lower_resolved_type(&ret); diff --git a/src/backend/ir/lower/expr/calls.rs b/src/backend/ir/lower/expr/calls.rs index d5bbea00..32d10260 100644 --- a/src/backend/ir/lower/expr/calls.rs +++ b/src/backend/ir/lower/expr/calls.rs @@ -1206,25 +1206,6 @@ impl AstLowering { } } - /// Build a synthetic callable signature from an already-lowered function type. - fn function_signature_from_ir_type(params: &[IrType], ret: &IrType) -> FunctionSignature { - FunctionSignature { - params: params - .iter() - .enumerate() - .map(|(idx, ty)| FunctionParam { - name: format!("__incan_arg_{idx}"), - ty: ty.clone(), - mutability: super::super::super::types::Mutability::Immutable, - is_self: false, - kind: ast::ParamKind::Normal, - default: None, - }) - .collect(), - return_type: ret.clone(), - } - } - /// Return whether passing `arg` to a callable parameter should refine that parameter to a shared borrow. fn callable_arg_needs_implicit_borrow(arg: &TypedExpr, target_ty: &IrType) -> bool { if arg.ty.is_copy() || matches!(target_ty, IrType::Ref(_) | IrType::RefMut(_)) { @@ -1263,7 +1244,7 @@ impl AstLowering { return callable_signature; }; let mut signature = - callable_signature.unwrap_or_else(|| Self::function_signature_from_ir_type(params, ret.as_ref())); + callable_signature.unwrap_or_else(|| FunctionSignature::from_function_type(params, ret.as_ref())); let mut changed = false; for (idx, arg) in args.iter().enumerate() { diff --git a/src/backend/ir/lower/mod.rs b/src/backend/ir/lower/mod.rs index 53a1ef6f..698f67a4 100644 --- a/src/backend/ir/lower/mod.rs +++ b/src/backend/ir/lower/mod.rs @@ -39,10 +39,10 @@ use super::decl::{FunctionParam, IrDecl, IrDeclKind, IrImportOrigin, IrImportQua use super::expr::{IrCallArg, IrCallArgKind, IrExprKind, VarAccess, VarRefKind}; use super::stmt::{IrStmt, IrStmtKind}; use super::types::IrType; -use super::{FunctionSignature, IrProgram, Mutability}; +use super::{FunctionReexport, FunctionSignature, IrProgram, Mutability}; use crate::frontend::ast; use crate::frontend::decorator_resolution; -use crate::frontend::symbols::NewtypePrimitiveConstraint; +use crate::frontend::symbols::{CallableParam, NewtypePrimitiveConstraint}; use crate::frontend::typechecker::TypeCheckInfo; use crate::frontend::typechecker::stdlib_loader::StdlibAstCache; use incan_core::lang::conventions; @@ -258,6 +258,68 @@ impl AstLowering { self.current_source_module_name = name; } + /// Lower one typechecker-resolved callable surface into IR parameters, attaching an already-planned default + /// expression for each parameter when present. + fn function_params_from_callable_surface( + &mut self, + callable_params: &[CallableParam], + defaults: &[Option], + ) -> Vec { + callable_params + .iter() + .enumerate() + .map(|(idx, param)| { + let base_ty = self.lower_resolved_type(¶m.ty); + FunctionParam { + name: param.name.clone().unwrap_or_else(|| format!("__incan_arg_{idx}")), + ty: Self::lower_param_container_type(param.kind, base_ty), + mutability: Mutability::Immutable, + is_self: false, + kind: param.kind, + default: defaults.get(idx).cloned().flatten(), + } + }) + .collect() + } + + fn function_params_from_source_callable_surface( + &mut self, + callable_params: &[CallableParam], + source_params: &[ast::Spanned], + ) -> Vec { + callable_params + .iter() + .enumerate() + .map(|(idx, param)| { + let source_idx = param + .name + .as_deref() + .and_then(|name| source_params.iter().position(|source| source.node.name == name)) + .unwrap_or(idx); + let source_param = source_params.get(source_idx); + let default = if param.has_default { + source_param + .and_then(|source| source.node.default.as_ref()) + .and_then(|default_expr| self.lower_expr_spanned(default_expr).ok()) + } else { + None + }; + FunctionParam { + name: param.name.clone().unwrap_or_else(|| format!("__incan_arg_{idx}")), + ty: Self::lower_param_container_type(param.kind, self.lower_resolved_type(¶m.ty)), + mutability: if source_param.is_some_and(|source| source.node.is_mut) { + Mutability::Mutable + } else { + Mutability::Immutable + }, + is_self: false, + kind: param.kind, + default, + } + }) + .collect() + } + /// Return the logger name supplied to default `std.logging.get_logger()` calls. pub(super) fn current_default_logger_name(&self) -> String { self.current_source_module_name @@ -895,6 +957,50 @@ impl AstLowering { } } + fn collect_function_reexports(&self, program: &ast::Program) -> Vec { + let mut reexports = Vec::new(); + for decl in &program.declarations { + let ast::Declaration::Import(import) = &decl.node else { + continue; + }; + if !matches!(import.visibility, ast::Visibility::Public) { + continue; + } + let ast::ImportKind::From { module, items } = &import.kind else { + continue; + }; + + let module_path = self.canonical_source_import_module_segments(module); + for item in items { + let mut target_path = module_path.clone(); + target_path.push(item.name.clone()); + reexports.push(FunctionReexport { + name: item.alias.as_ref().unwrap_or(&item.name).clone(), + target_path, + }); + } + } + reexports + } + + fn canonical_source_import_module_segments(&self, module: &ast::ImportPath) -> Vec { + let segments = if module.parent_levels > 0 && !module.is_absolute { + let mut base = self + .current_source_module_name + .as_deref() + .map(|module_name| module_name.split('.').map(str::to_string).collect::>()) + .unwrap_or_default(); + for _ in 0..module.parent_levels { + base.pop(); + } + base.extend(module.segments.iter().cloned()); + base + } else { + module.segments.clone() + }; + crate::frontend::module::canonicalize_source_module_segments(&segments) + } + /// Lower a complete AST program to IR. /// /// This is the main entry point for the lowering pass. It performs: @@ -922,6 +1028,7 @@ impl AstLowering { let mut errors: Vec = Vec::new(); self.import_aliases = decorator_resolution::collect_import_aliases(program); self.rust_import_aliases = decorator_resolution::collect_rust_import_aliases(program); + ir_program.function_reexports = self.collect_function_reexports(program); self.imported_alias_targets = self.collect_imported_alias_targets(program); self.seed_imported_stdlib_trait_decls(program); self.alias_imported_dependency_trait_decls(); @@ -1061,55 +1168,78 @@ impl AstLowering { if let ast::Declaration::Function(ref f) = decl.node { let type_param_names: std::collections::HashSet<&str> = f.type_params.iter().map(|tp| tp.name.as_str()).collect(); - let params: Vec = f - .params - .iter() - .map(|p| { - let base_ty = self.lower_type_with_type_params(&p.node.ty.node, Some(&type_param_names)); - let param_ty = Self::lower_param_container_type(p.node.kind, base_ty); - FunctionParam { - name: p.node.name.clone(), - ty: param_ty, - mutability: if p.node.is_mut { - Mutability::Mutable - } else { - Mutability::Immutable - }, - is_self: false, - kind: p.node.kind, - default: match &p.node.default { - Some(default_expr) => self.lower_expr_spanned(default_expr).ok(), - None => None, - }, - } - }) - .collect(); - let return_type = self + let function_binding = self .type_info .as_ref() - .and_then(|info| info.declarations.decorated_function_bindings.get(&f.name)) - .and_then(|binding| match &binding.ty { - crate::frontend::symbols::ResolvedType::Function(_, ret) => Some(self.lower_resolved_type(ret)), - _ => None, - }) - .unwrap_or_else(|| self.lower_type_with_type_params(&f.return_type.node, Some(&type_param_names))); - ir_program - .function_registry - .register(f.name.clone(), params.clone(), return_type.clone()); - if let Some(signature) = ir_program.function_registry.get(&f.name).cloned() { - self.update_root_function_binding(&f.name, &signature.params, &signature.return_type); - } - if self + .and_then(|info| info.declarations.function_bindings.get(&f.name).cloned()); + let source_params: Vec = function_binding + .as_ref() + .map(|binding| self.function_params_from_source_callable_surface(&binding.params, &f.params)) + .unwrap_or_else(|| { + f.params + .iter() + .map(|p| { + let base_ty = + self.lower_type_with_type_params(&p.node.ty.node, Some(&type_param_names)); + let param_ty = Self::lower_param_container_type(p.node.kind, base_ty); + FunctionParam { + name: p.node.name.clone(), + ty: param_ty, + mutability: if p.node.is_mut { + Mutability::Mutable + } else { + Mutability::Immutable + }, + is_self: false, + kind: p.node.kind, + default: match &p.node.default { + Some(default_expr) => self.lower_expr_spanned(default_expr).ok(), + None => None, + }, + } + }) + .collect() + }); + if let Some(binding) = self .type_info .as_ref() - .is_some_and(|info| info.declarations.decorated_function_bindings.contains_key(&f.name)) + .and_then(|info| info.declarations.decorated_function_bindings.get(&f.name).cloned()) + && let crate::frontend::symbols::ResolvedType::Function(callable_params, callable_ret) = binding.ty { + let original_params = match &binding.original_ty { + crate::frontend::symbols::ResolvedType::Function(params, _) => params.as_slice(), + _ => &[], + }; + let defaults = + self.decorated_param_defaults_for_surface(&callable_params, original_params, &f.params); + let params = self.function_params_from_callable_surface(&callable_params, &defaults); + let return_type = self.lower_resolved_type(&callable_ret); + ir_program + .function_registry + .register(f.name.clone(), params.clone(), return_type.clone()); + self.update_root_function_binding(&f.name, ¶ms, &return_type); + let original_name = Self::decorator_original_function_name(&f.name); - let original_return_type = - self.lower_type_with_type_params(&f.return_type.node, Some(&type_param_names)); + let original_return_type = function_binding + .as_ref() + .map(|binding| self.lower_resolved_type(&binding.return_type)) + .unwrap_or_else(|| { + self.lower_type_with_type_params(&f.return_type.node, Some(&type_param_names)) + }); ir_program .function_registry - .register(original_name, params, original_return_type); + .register(original_name, source_params, original_return_type); + continue; + } + let return_type = function_binding + .as_ref() + .map(|binding| self.lower_resolved_type(&binding.return_type)) + .unwrap_or_else(|| self.lower_type_with_type_params(&f.return_type.node, Some(&type_param_names))); + ir_program + .function_registry + .register(f.name.clone(), source_params.clone(), return_type.clone()); + if let Some(signature) = ir_program.function_registry.get(&f.name).cloned() { + self.update_root_function_binding(&f.name, &signature.params, &signature.return_type); } } else if let ast::Declaration::Alias(ref alias) = decl.node && let [target] = alias.target.segments.as_slice() @@ -1599,6 +1729,10 @@ impl AstLowering { span: ast::Span::default().into(), }); }; + let original_params = match binding.original_ty { + crate::frontend::symbols::ResolvedType::Function(params, _) => params, + _ => Vec::new(), + }; let original_name = Self::decorator_original_function_name(&f.name); let original = self.lower_function_named(f, original_name.clone(), super::decl::Visibility::Private)?; @@ -1614,7 +1748,13 @@ impl AstLowering { let mut value = self.lower_expr_spanned(&decorator_expr)?; value.ty = decorated_ty.clone(); let static_name = Self::decorator_static_binding_name(&f.name); - let wrapper = self.decorated_function_wrapper(f, &static_name, &callable_params, callable_ret.as_ref()); + let wrapper = self.decorated_function_wrapper( + f, + &static_name, + &callable_params, + &original_params, + callable_ret.as_ref(), + ); Ok(vec![ IrDecl::new(IrDeclKind::Function(original)), @@ -1633,24 +1773,12 @@ impl AstLowering { &mut self, f: &ast::FunctionDecl, static_name: &str, - callable_params: &[crate::frontend::symbols::CallableParam], + callable_params: &[CallableParam], + original_params: &[CallableParam], callable_ret: &crate::frontend::symbols::ResolvedType, ) -> super::decl::IrFunction { - let params: Vec = callable_params - .iter() - .enumerate() - .map(|(idx, param)| { - let base_ty = self.lower_resolved_type(¶m.ty); - FunctionParam { - name: param.name.clone().unwrap_or_else(|| format!("__incan_arg_{idx}")), - ty: Self::lower_param_container_type(param.kind, base_ty), - mutability: Mutability::Immutable, - is_self: false, - kind: param.kind, - default: None, - } - }) - .collect(); + let defaults = self.decorated_param_defaults_for_surface(callable_params, original_params, &f.params); + let params = self.function_params_from_callable_surface(callable_params, &defaults); let return_type = self.lower_resolved_type(callable_ret); let static_func = TypedExpr::new( IrExprKind::StaticRead { @@ -1702,6 +1830,75 @@ impl AstLowering { } } + /// Lower source defaults for a decorated callable wrapper when the final callable surface still maps to the + /// original typechecker-resolved parameters. + /// + /// Function types can describe parameter types but not default expressions. User-defined decorators often return an + /// explicit function type such as `(int) -> int`, which erases the declaration's richer call-site defaults even + /// when the decorator keeps the same callable surface. This helper rebuilds one default plan from source parameter + /// metadata only after the final decorator surface still matches the original callable shape. The comparison uses + /// typechecker-resolved parameter types so transparent aliases like `type Expr = Union[...]` do not split lowering + /// behavior across import or alias boundaries. + pub(super) fn decorated_param_defaults_for_surface( + &mut self, + surface_params: &[CallableParam], + original_params: &[CallableParam], + source_params: &[ast::Spanned], + ) -> Vec> { + let positional_shapes_match = Self::decorated_positional_param_shapes_match(surface_params, original_params); + + surface_params + .iter() + .enumerate() + .map(|(idx, surface_param)| { + let default_expr = if let Some(name) = surface_param.name.as_deref() { + original_params + .iter() + .position(|original_param| { + original_param.name.as_deref() == Some(name) + && Self::decorated_param_shape_matches(surface_param, original_param) + }) + .and_then(|source_idx| { + original_params + .get(source_idx) + .is_some_and(|original_param| original_param.has_default) + .then(|| source_params.get(source_idx)) + .flatten() + }) + .and_then(|source_param| source_param.node.default.clone()) + } else if positional_shapes_match { + original_params + .get(idx) + .is_some_and(|original_param| original_param.has_default) + .then(|| source_params.get(idx)) + .flatten() + .and_then(|source_param| source_param.node.default.clone()) + } else { + None + }; + + default_expr.and_then(|expr| self.lower_expr_spanned(&expr).ok()) + }) + .collect() + } + + fn decorated_positional_param_shapes_match( + surface_params: &[CallableParam], + original_params: &[CallableParam], + ) -> bool { + surface_params.len() == original_params.len() + && surface_params + .iter() + .zip(original_params) + .all(|(surface_param, original_param)| { + Self::decorated_param_shape_matches(surface_param, original_param) + }) + } + + fn decorated_param_shape_matches(surface_param: &CallableParam, original_param: &CallableParam) -> bool { + surface_param.kind == original_param.kind && surface_param.ty == original_param.ty + } + /// Add alias-qualified dependency trait declarations so default methods can expand for imported derive aliases. fn alias_imported_dependency_trait_decls(&mut self) { let existing = self.trait_decls.clone(); diff --git a/src/backend/ir/mod.rs b/src/backend/ir/mod.rs index fe509511..fb5ce113 100644 --- a/src/backend/ir/mod.rs +++ b/src/backend/ir/mod.rs @@ -59,6 +59,84 @@ pub struct FunctionSignature { pub return_type: IrType, } +impl FunctionSignature { + /// Build a positional callable signature from a lowered function type. + pub fn from_function_type(params: &[IrType], ret: &IrType) -> Self { + Self { + params: params + .iter() + .enumerate() + .map(|(idx, ty)| FunctionParam { + name: format!("__incan_arg_{idx}"), + ty: ty.clone(), + mutability: Mutability::Immutable, + is_self: false, + kind: crate::frontend::ast::ParamKind::Normal, + default: None, + }) + .collect(), + return_type: ret.clone(), + } + } + + /// Return the effective call signature when one source carries precise callable type metadata and another carries + /// source defaults for the same callable surface. + pub fn merge_default_source( + primary: Option<&FunctionSignature>, + default_source: Option<&FunctionSignature>, + ) -> Option { + Self::merge_default_source_by(primary, default_source, |left, right| left == right) + } + + /// Return the effective call signature using a caller-supplied type equivalence rule for default inheritance. + pub fn merge_default_source_by( + primary: Option<&FunctionSignature>, + default_source: Option<&FunctionSignature>, + types_match: impl Fn(&IrType, &IrType) -> bool, + ) -> Option { + let Some(primary) = primary else { + return default_source.cloned(); + }; + let Some(default_source) = default_source else { + return Some(primary.clone()); + }; + let mut merged = primary.clone(); + if Self::params_match_for_default_inheritance(primary, default_source, &types_match) { + for (param, default_param) in merged.params.iter_mut().zip(&default_source.params) { + if param.default.is_none() { + param.default = default_param.default.clone(); + } + } + } + Some(merged) + } + + fn params_match_for_default_inheritance( + left: &FunctionSignature, + right: &FunctionSignature, + types_match: &impl Fn(&IrType, &IrType) -> bool, + ) -> bool { + left.params.len() == right.params.len() + && left + .params + .iter() + .zip(&right.params) + .all(|(left, right)| Self::param_matches_for_default_inheritance(left, right, types_match)) + } + + fn param_matches_for_default_inheritance( + left: &FunctionParam, + right: &FunctionParam, + types_match: &impl Fn(&IrType, &IrType) -> bool, + ) -> bool { + left.kind == right.kind + && types_match(&left.ty, &right.ty) + && (left.name == right.name + || left.name.starts_with("__incan_arg_") + || right.name.starts_with("__incan_arg_")) + } +} + /// Registry of all function signatures in the program #[derive(Debug, Clone, Default)] pub struct FunctionRegistry { @@ -113,6 +191,61 @@ impl FunctionRegistry { self.signatures.insert(name.clone(), sig.clone()); } } + + /// Resolve the effective function-call signature for one IR call site. + /// + /// This is the single merge point for callable metadata during emission. Typechecker/lowering metadata can carry a + /// precise callable surface, while the source registry can carry default expressions. Canonical paths resolve + /// through the cross-module registry, local names resolve through the module registry, and lowered function types + /// are only a final fallback. + pub fn effective_call_signature( + local_registry: &FunctionRegistry, + canonical_registry: &FunctionRegistry, + local_name: Option<&str>, + canonical_path: Option<&[String]>, + callable_signature: Option<&FunctionSignature>, + callee_ty: Option<&IrType>, + ) -> Option { + Self::effective_call_signature_by( + local_registry, + canonical_registry, + local_name, + canonical_path, + callable_signature, + callee_ty, + |left, right| left == right, + ) + } + + /// Resolve the effective function-call signature using a caller-supplied type equivalence rule. + pub fn effective_call_signature_by( + local_registry: &FunctionRegistry, + canonical_registry: &FunctionRegistry, + local_name: Option<&str>, + canonical_path: Option<&[String]>, + callable_signature: Option<&FunctionSignature>, + callee_ty: Option<&IrType>, + types_match: impl Fn(&IrType, &IrType) -> bool, + ) -> Option { + let registry_signature = if let Some(path) = canonical_path { + canonical_registry.get_canonical_path(path) + } else { + local_name.and_then(|name| local_registry.get(name)) + }; + FunctionSignature::merge_default_source_by(callable_signature, registry_signature, types_match).or_else(|| { + match callee_ty { + Some(IrType::Function { params, ret }) => Some(FunctionSignature::from_function_type(params, ret)), + _ => None, + } + }) + } +} + +/// Public source import re-export that should behave like the imported callable for metadata lookups. +#[derive(Debug, Clone)] +pub struct FunctionReexport { + pub name: String, + pub target_path: Vec, } /// A complete IR program @@ -126,6 +259,8 @@ pub struct IrProgram { pub entry_point: Option, /// Function signature registry for call-site type checking pub function_registry: FunctionRegistry, + /// Public source-function re-exports keyed by local exported name and canonical target path. + pub function_reexports: Vec, /// RFC 023: The `rust.module("path::to::module")` Rust backing path, if declared. /// /// When present, `@rust.extern` functions in this program emit delegation calls to this Rust module path instead @@ -146,6 +281,7 @@ impl IrProgram { source_module_name: None, entry_point: None, function_registry: FunctionRegistry::new(), + function_reexports: Vec::new(), rust_module_path: None, newtype_checked_ctor: std::collections::HashMap::new(), } diff --git a/src/backend/ir/trait_bound_inference.rs b/src/backend/ir/trait_bound_inference.rs index 0ac9dd38..a9238ef8 100644 --- a/src/backend/ir/trait_bound_inference.rs +++ b/src/backend/ir/trait_bound_inference.rs @@ -2782,6 +2782,7 @@ mod tests { source_module_name: None, entry_point: None, function_registry: FunctionRegistry::new(), + function_reexports: Vec::new(), rust_module_path: None, newtype_checked_ctor: Default::default(), } @@ -2829,6 +2830,7 @@ mod tests { source_module_name: None, entry_point: None, function_registry: FunctionRegistry::new(), + function_reexports: Vec::new(), rust_module_path: None, newtype_checked_ctor: Default::default(), }; diff --git a/src/frontend/typechecker/check_decl.rs b/src/frontend/typechecker/check_decl.rs index cede65bd..cfb38496 100644 --- a/src/frontend/typechecker/check_decl.rs +++ b/src/frontend/typechecker/check_decl.rs @@ -3778,7 +3778,7 @@ impl TypeChecker { return; }; - let mut binding_ty = original_ty; + let mut binding_ty = original_ty.clone(); for decorator in func.decorators.iter().rev() { if self.is_user_defined_decorator_candidate(&decorator.node) { binding_ty = self.apply_user_defined_decorator(decorator, binding_ty, &func.name); @@ -3790,7 +3790,10 @@ impl TypeChecker { { self.type_info.declarations.decorated_function_bindings.insert( func.name.clone(), - DecoratedFunctionBindingInfo { ty: binding_ty.clone() }, + DecoratedFunctionBindingInfo { + ty: binding_ty.clone(), + original_ty, + }, ); symbol.kind = SymbolKind::Variable(VariableInfo { ty: binding_ty, diff --git a/src/frontend/typechecker/collect.rs b/src/frontend/typechecker/collect.rs index ac71acdf..d2f3eb54 100644 --- a/src/frontend/typechecker/collect.rs +++ b/src/frontend/typechecker/collect.rs @@ -10,7 +10,7 @@ use crate::frontend::symbols::*; use crate::frontend::typechecker::helpers::freeze_const_type; use incan_core::lang::decorators::{self as core_decorators, DecoratorId}; -use super::TypeChecker; +use super::{FunctionBindingInfo, TypeChecker}; mod decl_helpers; pub(super) mod decorators; @@ -1255,6 +1255,13 @@ impl TypeChecker { }) .collect(); let return_type = self.resolve_type_checked(&func.return_type); + self.type_info.declarations.function_bindings.insert( + func.name.clone(), + FunctionBindingInfo { + params: params.clone(), + return_type: return_type.clone(), + }, + ); self.symbols.define(Symbol { name: func.name.clone(), diff --git a/src/frontend/typechecker/mod.rs b/src/frontend/typechecker/mod.rs index 13db2808..c0b1ab53 100644 --- a/src/frontend/typechecker/mod.rs +++ b/src/frontend/typechecker/mod.rs @@ -55,10 +55,11 @@ mod validate_rust_module; pub use const_eval::ConstValue; pub use type_info::{ - ComputedPropertyAccessInfo, DecoratedFunctionBindingInfo, DecoratedMethodBindingInfo, FixedUnpackPlan, IdentKind, - ProtocolIterationInfo, ResolvedMethodCall, ResolvedMethodDispatch, ResolvedOperatorCall, ResolvedOperatorKind, - RustArgCoercionInfo, RustArgCoercionKind, StaticBindingInfo, TestingFixtureInfo, TypeCheckInfo, - ValidatedNewtypeCoercionInfo, ValidatedNewtypeCoercionMode, ValidatedNewtypeCoercionStep, + ComputedPropertyAccessInfo, DecoratedFunctionBindingInfo, DecoratedMethodBindingInfo, FixedUnpackPlan, + FunctionBindingInfo, IdentKind, ProtocolIterationInfo, ResolvedMethodCall, ResolvedMethodDispatch, + ResolvedOperatorCall, ResolvedOperatorKind, RustArgCoercionInfo, RustArgCoercionKind, StaticBindingInfo, + TestingFixtureInfo, TypeCheckInfo, ValidatedNewtypeCoercionInfo, ValidatedNewtypeCoercionMode, + ValidatedNewtypeCoercionStep, }; #[cfg(test)] mod tests; diff --git a/src/frontend/typechecker/type_info.rs b/src/frontend/typechecker/type_info.rs index cfc48099..7dd05d86 100644 --- a/src/frontend/typechecker/type_info.rs +++ b/src/frontend/typechecker/type_info.rs @@ -177,6 +177,11 @@ pub struct RustInteropArtifacts { /// Declaration-level binding rewrites and visibility facts consumed by lowering. #[derive(Debug, Default, Clone)] pub struct DeclarationArtifacts { + /// Module-local function declarations keyed by source name after annotation resolution. + /// + /// Lowering consumes this instead of re-lowering raw AST annotations so aliases such as + /// `type Expr = Union[...]` do not produce a different callable surface from typechecked call sites. + pub function_bindings: HashMap, /// Module-visible static bindings keyed by local name for lowering/runtime emission. pub static_bindings: HashMap, /// Same-type method aliases keyed by nominal type name (`alias -> target_method`). @@ -427,11 +432,22 @@ pub struct StaticBindingInfo { pub is_imported: bool, } +/// Lowering metadata for one source function declaration. +#[derive(Debug, Clone, PartialEq)] +pub struct FunctionBindingInfo { + /// Typechecker-resolved source parameters, including default-presence markers. + pub params: Vec, + /// Typechecker-resolved source return type. + pub return_type: ResolvedType, +} + /// Lowering metadata for one RFC 036 decorated function binding. #[derive(Debug, Clone, PartialEq)] pub struct DecoratedFunctionBindingInfo { /// Final type of the module-visible binding after applying all user-defined decorators. pub ty: ResolvedType, + /// Original callable type before decorators are applied. + pub original_ty: ResolvedType, } /// Lowering metadata for one RFC 036 decorated method binding. diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index a8f51397..271743cd 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1914,18 +1914,40 @@ pub deterministic_spec = partial FunctionSpec(namespace="core", deterministic=tr @add(deterministic_spec(lifecycle="stable")) pub def normalize(value: int) -> int: return value +"#, + )?; + fs::write( + src_dir.join("registry_facade.incn"), + r#"pub from function_registry import add, deterministic_spec +"#, + )?; + fs::write( + src_dir.join("facade_helpers.incn"), + r#"from registry_facade import add, deterministic_spec + + +@add(deterministic_spec(lifecycle="stable")) +pub def facade_normalize(value: int) -> int: + return value "#, )?; fs::write( tests_dir.join("test_registry_intent.incn"), r#"from function_registry import registered_names, registered_namespaces from helpers import normalize +from facade_helpers import facade_normalize def test_decorator_can_infer_name_with_imported_partial_spec() -> None: assert normalize(7) == 7 assert registered_names[0] == "normalize" assert registered_namespaces[0] == "core" + + +def test_decorator_can_use_reexported_partial_spec() -> None: + assert facade_normalize(8) == 8 + assert registered_names[1] == "facade_normalize" + assert registered_namespaces[1] == "core" "#, )?; @@ -2013,6 +2035,310 @@ def test_partial_default_symbols_in_decorator() -> None: Ok(()) } +#[test] +fn test_decorated_functions_preserve_default_argument_calls_issue703() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "decorated_default_argument_calls", "")?; + let src_dir = main_path.parent().ok_or("main path had no parent")?; + fs::write( + src_dir.join("columns.incn"), + r#"pub model ColumnExpr: + pub value: str + + +pub model Ref: + pub name: str + + +pub model Literal: + pub value: int + + +pub type Expr = Union[Ref, Literal] + + +pub def col(value: str) -> ColumnExpr: + return ColumnExpr(value=value) + + +pub def union_col(name: str) -> Expr: + return Ref(name=name) +"#, + )?; + fs::write( + src_dir.join("defaults.incn"), + r#"pub model Ref: + pub name: str + + +pub model Literal: + pub value: int + + +pub type Expr = Union[Ref, Literal] + + +pub def col(name: str) -> Expr: + return Ref(name=name) + + +def identity(func: (Expr) -> int) -> (Expr) -> int: + return func + + +@identity +pub def decorated_default(expr: Expr = col("")) -> int: + return 1 +"#, + )?; + fs::write( + src_dir.join("test_consumer.incn"), + r#"from defaults import decorated_default + + +def test_imported_decorated_default_call() -> None: + assert decorated_default() == 1 +"#, + )?; + fs::write( + src_dir.join("facade.incn"), + r#"pub from defaults import decorated_default +"#, + )?; + fs::write( + src_dir.join("facade_chain.incn"), + r#"pub from facade import decorated_default +"#, + )?; + fs::write( + src_dir.join("facade_alias.incn"), + r#"pub from defaults import decorated_default as public_decorated_default +"#, + )?; + fs::write( + src_dir.join("test_facade_consumer.incn"), + r#"from facade import decorated_default + + +def test_reexported_decorated_default_call() -> None: + assert decorated_default() == 1 +"#, + )?; + fs::write( + src_dir.join("test_facade_chain_consumer.incn"), + r#"from facade_chain import decorated_default + + +def test_chained_reexported_decorated_default_call() -> None: + assert decorated_default() == 1 +"#, + )?; + fs::write( + src_dir.join("test_facade_alias_consumer.incn"), + r#"from facade_alias import public_decorated_default + + +def test_aliased_reexported_decorated_default_call() -> None: + assert public_decorated_default() == 1 +"#, + )?; + let functions_dir = src_dir.join("functions"); + let aggregates_dir = functions_dir.join("aggregates"); + fs::create_dir_all(&aggregates_dir)?; + fs::write( + aggregates_dir.join("count.incn"), + r#"from defaults import Expr, col + + +def identity(func: (Expr) -> int) -> (Expr) -> int: + return func + + +@identity +pub def count(expr: Expr = col("")) -> int: + return 1 +"#, + )?; + fs::write( + functions_dir.join("mod.incn"), + r#"pub from functions.aggregates.count import count +"#, + )?; + fs::write( + src_dir.join("test_nested_facade_consumer.incn"), + r#"from functions import count + + +def test_nested_reexported_decorated_default_call() -> None: + assert count() == 1 +"#, + )?; + let tests_dir = tmp.path().join("tests"); + fs::create_dir_all(&tests_dir)?; + fs::write( + tests_dir.join("test_decorated_default_probe.incn"), + r#"from columns import ColumnExpr, Expr, col, union_col + + +def identity(func: (int) -> int) -> ((int) -> int): + return func + + +class Box: + value: int + + @method_identity + def decorated_method_default(self, value: int = 11) -> int: + return value + + +def method_identity(func: (&Box, int) -> int) -> ((&Box, int) -> int): + return func + + +@identity +def decorated_default(value: int = 7) -> int: + return value + + +def count_identity(func: (ColumnExpr) -> int) -> ((ColumnExpr) -> int): + return func + + +@count_identity +def count(expr: ColumnExpr = col("")) -> int: + return 1 + + +def union_count_identity(func: (Expr) -> int) -> ((Expr) -> int): + return func + + +@union_count_identity +def union_count(expr: Expr = union_col("")) -> int: + return 1 + + +def adapted_impl(value: str) -> int: + return 7 + + +def string_adapter(func: (int) -> int) -> ((str) -> int): + return adapted_impl + + +@string_adapter +def surface_changed(value: int = 7) -> int: + return value + + +def plain_default(value: int = 7) -> int: + return value + + +def plain_union_default(expr: Expr = union_col("")) -> int: + return 1 + + +def test_decorated_default_probe() -> None: + assert plain_default() == 7 + assert plain_union_default() == 1 + assert plain_union_default(union_col("orders")) == 1 + assert decorated_default() == 7 + assert decorated_default(3) == 3 + box = Box(value=1) + assert box.decorated_method_default() == 11 + assert box.decorated_method_default(5) == 5 + assert count() == 1 + assert count(col("orders")) == 1 + assert union_count() == 1 + assert union_count(union_col("orders")) == 1 + assert surface_changed("changed") == 7 +"#, + )?; + + let test_path = tmp.path().join("tests/test_decorated_default_probe.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 decorated default arguments issue703"); + + let consumer_path = src_dir.join("test_consumer.incn"); + let consumer_output = run_incan( + tmp.path(), + &[ + "test", + consumer_path.to_str().ok_or("consumer path was not valid UTF-8")?, + ], + )?; + assert_success( + &consumer_output, + "incan test for imported decorated default arguments issue703", + ); + + let facade_consumer_path = src_dir.join("test_facade_consumer.incn"); + let facade_consumer_output = run_incan( + tmp.path(), + &[ + "test", + facade_consumer_path + .to_str() + .ok_or("facade consumer path was not valid UTF-8")?, + ], + )?; + assert_success( + &facade_consumer_output, + "incan test for re-exported decorated default arguments issue703", + ); + + let facade_chain_consumer_path = src_dir.join("test_facade_chain_consumer.incn"); + let facade_chain_consumer_output = run_incan( + tmp.path(), + &[ + "test", + facade_chain_consumer_path + .to_str() + .ok_or("facade chain consumer path was not valid UTF-8")?, + ], + )?; + assert_success( + &facade_chain_consumer_output, + "incan test for chained re-exported decorated default arguments issue703", + ); + + let facade_alias_consumer_path = src_dir.join("test_facade_alias_consumer.incn"); + let facade_alias_consumer_output = run_incan( + tmp.path(), + &[ + "test", + facade_alias_consumer_path + .to_str() + .ok_or("facade alias consumer path was not valid UTF-8")?, + ], + )?; + assert_success( + &facade_alias_consumer_output, + "incan test for aliased re-exported decorated default arguments issue703", + ); + + let nested_facade_consumer_path = src_dir.join("test_nested_facade_consumer.incn"); + let nested_facade_consumer_output = run_incan( + tmp.path(), + &[ + "test", + nested_facade_consumer_path + .to_str() + .ok_or("nested facade consumer path was not valid UTF-8")?, + ], + )?; + assert_success( + &nested_facade_consumer_output, + "incan test for nested re-exported decorated default arguments issue703", + ); + Ok(()) +} + #[test] fn test_decorator_callable_exposes_source_name_issue694() -> Result<(), Box> { let tmp = tempfile::tempdir()?; @@ -2038,11 +2364,17 @@ pub def capture(func: (int) -> int) -> ((int) -> int): pub def registered() -> (((int) -> int) -> ((int) -> int)): return capture +"#, + )?; + fs::write( + src_dir.join("registry_facade.incn"), + r#"pub from registry import names, registered "#, )?; fs::write( tests_dir.join("test_callable_name.incn"), r#"from registry import names, registered +from registry_facade import registered as facade_registered @registered() @@ -2050,9 +2382,16 @@ pub def sample(value: int) -> int: return value + 1 +@facade_registered() +pub def facade_sample(value: int) -> int: + return value + 2 + + def test_decorator_can_read_specific_callable_name() -> None: assert sample(1) == 2 assert names[0] == "sample" + assert facade_sample(1) == 3 + assert names[1] == "facade_sample" "#, )?; diff --git a/workspaces/docs-site/docs/release_notes/0_3.md b/workspaces/docs-site/docs/release_notes/0_3.md index cf27f9c4..4ad5e64c 100644 --- a/workspaces/docs-site/docs/release_notes/0_3.md +++ b/workspaces/docs-site/docs/release_notes/0_3.md @@ -39,7 +39,7 @@ Use this section as the map. The release note names each larger feature, says wh - **Value enums**: Keep enum type safety while exposing canonical `str` or `int` representations for external values. Read [Enums](../language/explanation/enums.md) and [Modeling with enums](../language/how-to/modeling_with_enums.md) ([RFC 032], #317). - **Enum methods and trait adoption**: Put enum-owned behavior on the enum and let enums adopt the same trait protocols as other source types. Read [Enums](../language/explanation/enums.md) and [Traits as language hooks](../language/explanation/traits_as_language_hooks.md) ([RFC 050], #334). - **Computed properties and protocol hooks**: Define property-like readers and dunder-backed operator/protocol behavior without pushing users into Rust-shaped wrappers. Read [Traits as language hooks](../language/explanation/traits_as_language_hooks.md) and [Derives and traits](../language/reference/derives_and_traits.md) ([RFC 046], [RFC 068], [RFC 028], #86, #162, #203). -- **Decorators**: Typecheck user-defined decorators for functions, async functions, and methods so later references see the decorated callable shape, and concrete decorated callable values expose `__name__` for registry-style decorators. Read [Decorators](../language/reference/language.md#decorators), [Functions](../language/reference/functions.md), and [Checked API metadata](../tooling/reference/checked_api_metadata.md) ([RFC 036], #170, #640, #694). +- **Decorators**: Typecheck user-defined decorators for functions, async functions, and methods so later references see the decorated callable shape, concrete decorated callable values expose `__name__` for registry-style decorators, and decorated wrappers preserve source default-argument calls when the callable surface is unchanged. Read [Decorators](../language/reference/language.md#decorators), [Functions](../language/reference/functions.md), and [Checked API metadata](../tooling/reference/checked_api_metadata.md) ([RFC 036], #170, #640, #694, #703). - **Symbol aliases**: Export an existing callable or type-like symbol under another name without pretending it is a hand-written wrapper. Read [Symbol aliases](../language/reference/symbol_aliases.md) ([RFC 083], #437). - **Callable presets with RHS `partial` declarations**: Write `pub get = partial route(method="GET")` when a new API name is really the same callable with named defaults, not a new function body. Read [Callable presets explained](../language/explanation/callable_presets.md), then [Callable presets](../language/reference/callable_presets.md) ([RFC 084], #453). - **Variadics and call unpacking**: Describe call shapes that accept or forward flexible argument lists without losing static checks. Read [Functions](../language/reference/functions.md) ([RFC 038], #83). @@ -109,6 +109,7 @@ This section is grouped by outcome rather than by every minimized repro. Issue n - **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, 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). +- **Decorated wrappers preserve defaults**: Decorated functions and methods keep source default-argument call behavior when the final decorated callable surface still matches the original declaration, including direct imports and public facade re-exports (#703). - **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