From 5ded4caa63094fdc949411c844099129136af95f Mon Sep 17 00:00:00 2001 From: Danny Meijer Date: Tue, 26 May 2026 14:08:28 +0200 Subject: [PATCH] bugfix - preserve decorator callable identity and partial presets (#694, #698) --- Cargo.lock | 18 +- Cargo.toml | 2 +- .../src/bin/generate_lang_reference.rs | 15 + crates/incan_core/src/lang/features.rs | 3 +- src/backend/ir/codegen.rs | 180 ++++++++++- src/backend/ir/emit/expressions/calls.rs | 8 +- src/backend/ir/emit/expressions/indexing.rs | 80 +++++ src/backend/ir/emit/mod.rs | 174 +++++++++- src/backend/ir/emit/program.rs | 304 ++++++++++++++++-- src/backend/ir/lower/decl/functions.rs | 280 ++++++++++++++++ src/backend/ir/mod.rs | 26 ++ src/backend/ir/trait_bound_inference.rs | 4 + src/frontend/module.rs | 27 +- src/frontend/typechecker/check_expr/access.rs | 7 + src/frontend/typechecker/tests.rs | 48 +++ tests/cli_integration.rs | 216 +++++++++++++ .../semantic_string_audit.json | 22 +- .../language/reference/feature_inventory.md | 5 +- .../docs/language/reference/language.md | 15 + .../docs-site/docs/release_notes/0_3.md | 6 +- 20 files changed, 1382 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb0a5332e..51627a401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "incan" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "blake2", "blake3", @@ -1527,14 +1527,14 @@ dependencies = [ [[package]] name = "incan_core" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "proc-macro2", "quote", @@ -1543,14 +1543,14 @@ dependencies = [ [[package]] name = "incan_semantics_core" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "incan_core", ] [[package]] name = "incan_semantics_stdlib" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "incan_core", "incan_semantics_core", @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "axum", "incan_core", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "incan_syntax" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "incan_core", "incan_semantics_core", @@ -1593,7 +1593,7 @@ dependencies = [ [[package]] name = "incan_web_macros" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "proc-macro2", "quote", @@ -3151,7 +3151,7 @@ dependencies = [ [[package]] name = "rust_inspect" -version = "0.3.0-rc17" +version = "0.3.0-rc18" dependencies = [ "hex", "incan_core", diff --git a/Cargo.toml b/Cargo.toml index fa3192dce..b193ff9dc 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-rc17" +version = "0.3.0-rc18" description = "The Incan programming language compiler" edition = "2024" rust-version = "1.92" diff --git a/crates/incan_core/src/bin/generate_lang_reference.rs b/crates/incan_core/src/bin/generate_lang_reference.rs index 57fb45e59..1935a304c 100644 --- a/crates/incan_core/src/bin/generate_lang_reference.rs +++ b/crates/incan_core/src/bin/generate_lang_reference.rs @@ -524,6 +524,21 @@ pub def col(name: str) -> ColumnExpr: The post-decoration binding keeps the concrete callable signature of the decorated function unless the decorator deliberately returns a different callable shape. Checked API metadata and imports observe that concrete signature, not the generic helper's `F`. +The callable value passed into a decorator exposes `__name__` as the source callable name. Registry and catalog decorators can use this from concrete decorator helpers and from generic `(F) -> F` helpers, so a decorator can record `func.__name__` without requiring the decorated declaration to repeat its own public name in a string argument. + +```incan +def capture[F](func: F) -> F: + registry_names.append(func.__name__) + return func + +def registered[F]() -> ((F) -> F): + return (func) => capture[F](func) + +@registered() +pub def sample(value: int) -> int: + return value + 1 +``` + Method decorators receive an unbound callable shape with the receiver first. A decorator on `def label(self, value: int) -> str` sees `(&Box, int) -> str`; a decorator on `def bump(mut self, value: int) -> int` sees `(&mut Box, int) -> int`. The wrapper passes the actual receiver borrow through to the decorated callable, so method decorators do not require cloning the receiver. Class, model, trait, enum, newtype, field, alias, and module decorators remain limited to compiler-owned decorators. Compiler-owned decorators such as `@derive`, `@route`, `@rust.extern`, `@rust.allow`, `@staticmethod`, `@classmethod`, and `@requires` keep their existing special behavior. diff --git a/crates/incan_core/src/lang/features.rs b/crates/incan_core/src/lang/features.rs index f53d83377..efe27205a 100644 --- a/crates/incan_core/src/lang/features.rs +++ b/crates/incan_core/src/lang/features.rs @@ -500,10 +500,11 @@ pub const FEATURES: &[FeatureDescriptor] = &[ introduced_in_rfc: RFC::_036, stability: Stability::Stable, activation: "None for user-defined decorators; compiler-owned decorators keep their documented imports.", - summary: "Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type.", + summary: "Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type and decorator helpers that expose `func.__name__`.", canonical_forms: &[ "@logged", "@registered(\"catalog.ref\")", + "func.__name__", "@registered[(str) -> ColumnExpr](\"catalog.ref\")", ], prefer_over: "Boilerplate wrapper declarations around every function that needs the same callable transform.", diff --git a/src/backend/ir/codegen.rs b/src/backend/ir/codegen.rs index 6fa92cd9d..899bd3afd 100644 --- a/src/backend/ir/codegen.rs +++ b/src/backend/ir/codegen.rs @@ -37,11 +37,12 @@ use crate::frontend::ast::Program; use crate::frontend::diagnostics::CompileError; use crate::frontend::library_manifest_index::LibraryManifestIndex; +use super::emit::CallableNameResolution; use super::scanners::{ check_for_this_import as scan_check_for_this_import, collect_rust_crates as scan_collect_rust_crates, detect_serde_usage, }; -use super::{AstLowering, EmitError, EmitService, IrEmitter, LoweringErrors}; +use super::{AstLowering, EmitError, EmitService, FunctionRegistry, IrEmitter, IrProgram, LoweringErrors}; mod dependency_metadata; mod ordinal_bridge; @@ -199,6 +200,25 @@ impl<'a> IrCodegen<'a> { } } + /// Build a registry for explicit canonical cross-module calls. + fn canonical_registry_for_programs<'program>( + programs: impl IntoIterator, + ) -> FunctionRegistry { + let mut registry = FunctionRegistry::new(); + for (module_path, program) in programs { + for (name, signature) in program.function_registry.iter() { + let mut canonical_path = module_path.to_vec(); + canonical_path.push(name.clone()); + registry.register_canonical_path( + &canonical_path, + signature.params.clone(), + signature.return_type.clone(), + ); + } + } + registry + } + /// 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; @@ -459,7 +479,7 @@ impl<'a> IrCodegen<'a> { program: &Program, internal_module_roots: &HashSet, ) -> Result { - self.try_generate_via_ir_with_union_config(program, internal_module_roots, HashMap::new(), false) + self.try_generate_via_ir_with_union_config(program, internal_module_roots, HashMap::new(), false, None, None) } /// Generate code via the IR pipeline with optional crate-root union sharing for multi-file source modules. @@ -469,6 +489,8 @@ impl<'a> IrCodegen<'a> { internal_module_roots: &HashSet, generated_union_types: HashMap, qualify_union_types_from_crate: bool, + mut callable_name_resolutions: Option<&mut HashMap>, + mut callable_name_used_signature_keys: Option<&mut HashSet>, ) -> Result { let deps: Vec<(&str, &Program)> = self .dependency_modules @@ -514,11 +536,29 @@ impl<'a> IrCodegen<'a> { // RFC 023: Infer trait bounds for generic functions. super::trait_bound_inference::infer_trait_bounds(&mut ir_program); - - // Build unified function registry including imported module functions - let mut unified_registry = ir_program.function_registry.clone(); + 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, + )); + } + if let Some(resolutions) = callable_name_resolutions.as_deref_mut() { + IrEmitter::add_callable_name_resolutions_for_program(resolutions, Vec::new(), &ir_program); + } + let callable_name_resolutions_for_emit = callable_name_resolutions + .as_ref() + .map(|resolutions| (**resolutions).clone()) + .unwrap_or_default(); + let callable_name_used_signature_keys_for_emit = callable_name_used_signature_keys + .as_ref() + .map(|used_keys| (**used_keys).clone()) + .unwrap_or_default(); + + let mut canonical_registry = FunctionRegistry::new(); let mut dependency_ir_programs = Vec::new(); - for (_, dep_ast, _) in &self.dependency_modules { + 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(); @@ -530,7 +570,18 @@ impl<'a> IrCodegen<'a> { ); dep_lowering.seed_struct_field_aliases(global_aliases.clone()); let dep_ir = dep_lowering.lower_program(dep_ast)?; - unified_registry.merge(&dep_ir.function_registry); + 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); } @@ -557,12 +608,17 @@ impl<'a> IrCodegen<'a> { self.apply_ordinal_bridge_config(inner, &ordinal_bridge); inner.set_qualify_union_types_from_crate(qualify_union_types_from_crate); inner.set_generated_union_types(generated_union_types); + inner.set_canonical_function_registry(canonical_registry.clone()); + inner.set_callable_name_current_module_path(Vec::new()); + 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 { inner.seed_dependency_nominal_metadata_from_program(dep_ir); } Ok(svc.emit_program(&ir_program)?) } else { - let mut emitter = IrEmitter::new(&unified_registry); + let mut emitter = IrEmitter::new(&ir_program.function_registry); emitter.set_internal_module_roots(internal_module_roots.clone()); if self.emit_zen_in_main { emitter.set_emit_zen(true); @@ -580,6 +636,11 @@ impl<'a> IrCodegen<'a> { self.apply_ordinal_bridge_config(&mut emitter, &ordinal_bridge); emitter.set_qualify_union_types_from_crate(qualify_union_types_from_crate); emitter.set_generated_union_types(generated_union_types); + emitter.set_canonical_function_registry(canonical_registry.clone()); + emitter.set_callable_name_current_module_path(Vec::new()); + 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 { emitter.seed_dependency_nominal_metadata_from_program(dep_ir); } @@ -758,14 +819,57 @@ impl<'a> IrCodegen<'a> { .collect(); super::trait_bound_inference::propagate_trait_bounds_from_programs(current_ir, &external_programs); } + let all_module_canonical_registry = Self::canonical_registry_for_programs( + lowered_modules + .iter() + .map(|(_, module_path, ir)| (module_path.as_slice(), ir)), + ); let mut shared_union_types = HashMap::new(); for (_, _, ir) in &lowered_modules { shared_union_types.extend(IrEmitter::collect_union_types_from_program(ir)); } // Generate main file after dependency lowering so it can own shared crate-root union wrappers. - let main_code = - self.try_generate_via_ir_with_union_config(program, &internal_roots, shared_union_types, true)?; + let mut callable_name_resolutions = HashMap::new(); + let mut callable_name_used_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( + &mut callable_name_resolutions, + module_path.clone(), + ir, + ); + let mut reachable_items = dependency_reachable_items.get(module_path).cloned().unwrap_or_default(); + if let Some(injected_items) = self.externally_reachable_items_by_module.get(module_path) { + reachable_items.extend(injected_items.iter().cloned()); + } + 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( + ir, + &reachable_items, + preserve_public_items, + &dependency_type_metadata.error_trait_type_names, + ); + } + if generic_callable_name_trait_used { + callable_name_used_signature_keys.extend(callable_name_resolutions.keys().cloned()); + } + + let main_code = self.try_generate_via_ir_with_union_config( + program, + &internal_roots, + shared_union_types, + true, + Some(&mut callable_name_resolutions), + Some(&mut callable_name_used_signature_keys), + )?; let mut modules = HashMap::new(); for (name, module_path, ir) in &lowered_modules { @@ -791,6 +895,10 @@ impl<'a> IrCodegen<'a> { 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); + inner.set_canonical_function_registry(all_module_canonical_registry.clone()); + inner.set_callable_name_current_module_path(module_path.clone()); + inner.set_callable_name_resolutions(callable_name_resolutions.clone()); + inner.set_callable_name_used_signature_keys(callable_name_used_signature_keys.clone()); self.apply_ordinal_bridge_config(inner, &ordinal_bridge); for (_, _, dep_ir) in &lowered_modules { inner.seed_dependency_nominal_metadata_from_program(dep_ir); @@ -810,6 +918,10 @@ impl<'a> IrCodegen<'a> { 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); + emitter.set_canonical_function_registry(all_module_canonical_registry.clone()); + emitter.set_callable_name_current_module_path(module_path.clone()); + emitter.set_callable_name_resolutions(callable_name_resolutions.clone()); + emitter.set_callable_name_used_signature_keys(callable_name_used_signature_keys.clone()); self.apply_ordinal_bridge_config(&mut emitter, &ordinal_bridge); for (_, _, dep_ir) in &lowered_modules { emitter.seed_dependency_nominal_metadata_from_program(dep_ir); @@ -949,14 +1061,50 @@ impl<'a> IrCodegen<'a> { .collect(); super::trait_bound_inference::propagate_trait_bounds_from_programs(current_ir, &external_programs); } + let all_module_canonical_registry = + Self::canonical_registry_for_programs(lowered_modules.iter().map(|(path, ir)| (path.as_slice(), ir))); let mut shared_union_types = HashMap::new(); for (_, ir) in &lowered_modules { shared_union_types.extend(IrEmitter::collect_union_types_from_program(ir)); } // Generate main file after dependency lowering so it can own shared crate-root union wrappers. - let main_code = - self.try_generate_via_ir_with_union_config(program, &internal_roots, shared_union_types, true)?; + let mut callable_name_resolutions = HashMap::new(); + let mut callable_name_used_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); + let mut reachable_items = dependency_reachable_items.get(path).cloned().unwrap_or_default(); + if let Some(injected_items) = self.externally_reachable_items_by_module.get(path) { + reachable_items.extend(injected_items.iter().cloned()); + } + 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( + 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, + ); + } + if generic_callable_name_trait_used { + callable_name_used_signature_keys.extend(callable_name_resolutions.keys().cloned()); + } + + let main_code = self.try_generate_via_ir_with_union_config( + program, + &internal_roots, + shared_union_types, + true, + Some(&mut callable_name_resolutions), + Some(&mut callable_name_used_signature_keys), + )?; let mut modules = HashMap::new(); for (path, ir) in &lowered_modules { @@ -982,6 +1130,10 @@ impl<'a> IrCodegen<'a> { 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); + inner.set_canonical_function_registry(all_module_canonical_registry.clone()); + inner.set_callable_name_current_module_path(path.clone()); + inner.set_callable_name_resolutions(callable_name_resolutions.clone()); + inner.set_callable_name_used_signature_keys(callable_name_used_signature_keys.clone()); self.apply_ordinal_bridge_config(inner, &ordinal_bridge); for (_, dep_ir) in &lowered_modules { inner.seed_dependency_nominal_metadata_from_program(dep_ir); @@ -1001,6 +1153,10 @@ impl<'a> IrCodegen<'a> { 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); + emitter.set_canonical_function_registry(all_module_canonical_registry.clone()); + emitter.set_callable_name_current_module_path(path.clone()); + emitter.set_callable_name_resolutions(callable_name_resolutions.clone()); + emitter.set_callable_name_used_signature_keys(callable_name_used_signature_keys.clone()); self.apply_ordinal_bridge_config(&mut emitter, &ordinal_bridge); for (_, dep_ir) in &lowered_modules { emitter.seed_dependency_nominal_metadata_from_program(dep_ir); diff --git a/src/backend/ir/emit/expressions/calls.rs b/src/backend/ir/emit/expressions/calls.rs index bb7c82c2e..827266346 100644 --- a/src/backend/ir/emit/expressions/calls.rs +++ b/src/backend/ir/emit/expressions/calls.rs @@ -499,12 +499,10 @@ impl<'a> IrEmitter<'a> { _ => None, }; let callee_name = local_name.or(canonical_name); - let registry_signature = if canonical_path.is_some() { - canonical_name.and_then(|name| self.function_registry.get(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)) - .or_else(|| canonical_name.and_then(|name| self.function_registry.get(name))) + local_name.and_then(|name| self.function_registry.get(name)) }; let result_specialized_signature = callable_signature.or(registry_signature).and_then(|signature| { result_target_ty.and_then(|target_ty| Self::specialize_signature_by_result_target(signature, target_ty)) diff --git a/src/backend/ir/emit/expressions/indexing.rs b/src/backend/ir/emit/expressions/indexing.rs index f06cadf5b..81d538500 100644 --- a/src/backend/ir/emit/expressions/indexing.rs +++ b/src/backend/ir/emit/expressions/indexing.rs @@ -40,6 +40,78 @@ fn emit_dict_lookup_index_key(object: &TypedExpr, index: &TypedExpr, emitted: To } impl<'a> IrEmitter<'a> { + /// Emit the stable source name for a function-typed value when the value points at a registered generated + /// function. Decorator lowering passes undecorated originals such as `__incan_original_sample`, but source-facing + /// metadata should still report `sample`. + fn emit_callable_name_expr(&self, object: &TypedExpr) -> Result { + let IrType::Function { params, ret } = &object.ty else { + return Ok(quote! { "".to_string() }); + }; + let Some(signature_key) = Self::callable_name_signature_key(params, ret) else { + 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 helper = Self::callable_name_helper_ident(&signature_key); + let mut helper_calls = Vec::new(); + if self.local_callable_name_signature_keys().contains(&signature_key) { + helper_calls.push(quote! { #helper(__incan_callable) }); + } + if let Some(resolution) = self.callable_name_resolutions.get(&signature_key) { + for module_path in &resolution.module_paths { + if module_path == &self.callable_name_current_module_path { + continue; + } + let helper_path = self.emit_callable_name_helper_path(module_path, &signature_key); + helper_calls.push(quote! { #helper_path(__incan_callable) }); + } + } + let fallback = proc_macro2::Literal::string(""); + let mut resolved = quote! { #fallback.to_string() }; + for helper_call in helper_calls.into_iter().rev() { + resolved = quote! { + if let Some(__incan_name) = #helper_call { + __incan_name.to_string() + } else { + #resolved + } + }; + } + + Ok(quote! {{ + let __incan_callable: #fn_ty = #callable; + #resolved + }}) + } + + fn emit_generic_callable_name_expr(&self, object: &TypedExpr) -> Result { + let object = self.emit_expr(object)?; + Ok(quote! { __IncanCallableName::__incan_callable_name(&#object) }) + } + + pub(in crate::backend::ir::emit) fn emit_callable_name_helper_path( + &self, + module_path: &[String], + signature_key: &str, + ) -> TokenStream { + let helper = Self::callable_name_helper_ident(signature_key); + if module_path.is_empty() { + return quote! { crate::#helper }; + } + let mut segments = vec![quote! { crate }]; + for segment in module_path { + let ident = Self::rust_ident(segment); + segments.push(quote! { #ident }); + } + segments.push(quote! { #helper }); + let mut iter = segments.into_iter(); + let first = iter.next().unwrap_or_else(|| quote! { crate }); + 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 @@ -218,6 +290,14 @@ impl<'a> IrEmitter<'a> { /// - Tuple field access (`tuple.0` → `tuple.0`) /// - Regular struct field access (`obj.field` → `obj.field`) pub(in super::super) fn emit_field_expr(&self, object: &TypedExpr, field: &str) -> Result { + if field == "__name__" { + return match object.ty { + IrType::Function { .. } => self.emit_callable_name_expr(object), + IrType::Generic(_) => self.emit_generic_callable_name_expr(object), + _ => Ok(quote! { "".to_string() }), + }; + } + if Self::expr_is_storage_rooted(object) { let rewritten = Self::rewrite_storage_root_expr( &TypedExpr::new( diff --git a/src/backend/ir/emit/mod.rs b/src/backend/ir/emit/mod.rs index c1f77dd2d..0ce789a0b 100644 --- a/src/backend/ir/emit/mod.rs +++ b/src/backend/ir/emit/mod.rs @@ -31,7 +31,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use super::decl::{IrDeclKind, IrEnumValue, IrEnumValueType, IrStruct, VariantFields, Visibility}; use super::expr::TypedExpr; @@ -67,6 +67,14 @@ pub(crate) struct ExternalOrdinalCustomKey { pub has_ordinal_bytes_equal: bool, } +/// Cross-module callable-name resolver metadata keyed by a concrete function-pointer signature. +#[derive(Debug, Clone)] +pub(crate) struct CallableNameResolution { + pub(super) params: Vec, + pub(super) ret: IrType, + pub(super) module_paths: Vec>, +} + /// Usage facts collected before Rust emission. /// /// This analysis is intentionally about generated Rust lints, not source-language reachability diagnostics. It records @@ -94,6 +102,10 @@ pub(super) struct GeneratedUseAnalysis { pub(super) result_observer_callable_types: HashSet, /// Top-level function values adapted to a borrowed function-pointer parameter. pub(super) borrowed_function_adapters: HashSet<(String, Vec)>, + /// Concrete function-pointer signatures whose values read `__name__`. + pub(super) callable_name_signature_keys: HashSet, + /// Whether a generic callable parameter reads `__name__` through the generated callable-name trait. + pub(super) uses_generic_callable_name_trait: bool, } impl GeneratedUseAnalysis { @@ -213,8 +225,10 @@ pub struct IrEmitter<'a> { emit_zen_in_main: bool, /// Whether serde is needed for emitted Rust derives or helpers. needs_serde: RefCell, - /// Function registry for call-site type checking + /// Function registry for module-local call-site default argument filling and type-aware argument conversion. function_registry: &'a FunctionRegistry, + /// Cross-module registry used only for IR calls that carry an explicit canonical callee path. + canonical_function_registry: Option, /// Track struct derives for generating serde methods in impl blocks struct_derives: std::collections::HashMap>, /// Current function's return type (for applying conversions in return statements) @@ -322,6 +336,15 @@ pub struct IrEmitter<'a> { emitted_result_observer_callable_helpers: RefCell>, /// Top-level function values adapted to a borrowed function-pointer parameter. borrowed_function_adapters: RefCell)>>, + /// Current generated Rust module path. The crate root uses an empty path. + callable_name_current_module_path: Vec, + /// Concrete callable-name helper modules available to this compilation unit. + callable_name_resolutions: HashMap, + /// Concrete callable-name signatures used somewhere in this compilation unit. + callable_name_used_signature_keys: HashSet, + /// Local callable registry used for module-local callable-name helpers when the main emitter has a unified + /// cross-module call registry. + callable_name_local_registry: Option, } impl<'a> IrEmitter<'a> { @@ -340,6 +363,7 @@ impl<'a> IrEmitter<'a> { emit_zen_in_main: false, needs_serde: RefCell::new(false), function_registry, + canonical_function_registry: None, struct_derives: std::collections::HashMap::new(), current_function_return_type: RefCell::new(None), external_rust_functions: std::collections::HashSet::new(), @@ -378,9 +402,155 @@ impl<'a> IrEmitter<'a> { result_observer_callable_types: RefCell::new(HashSet::new()), emitted_result_observer_callable_helpers: RefCell::new(HashSet::new()), borrowed_function_adapters: RefCell::new(HashSet::new()), + callable_name_current_module_path: Vec::new(), + callable_name_resolutions: HashMap::new(), + callable_name_used_signature_keys: HashSet::new(), + callable_name_local_registry: None, + } + } + + /// Configure the generated Rust module path for callable-name helper routing. + pub(crate) fn set_callable_name_current_module_path(&mut self, path: Vec) { + self.callable_name_current_module_path = path; + } + + /// Configure the canonical callable registry for explicit cross-module call paths. + pub(crate) fn set_canonical_function_registry(&mut self, registry: FunctionRegistry) { + self.canonical_function_registry = Some(registry); + } + + pub(super) fn canonical_function_registry(&self) -> &FunctionRegistry { + self.canonical_function_registry + .as_ref() + .unwrap_or(self.function_registry) + } + + /// Configure the concrete callable-name helper modules available to this emitter. + pub(crate) fn set_callable_name_resolutions(&mut self, resolutions: HashMap) { + self.callable_name_resolutions = resolutions; + } + + /// Configure the callable-name signatures that are used anywhere in this generated crate. + pub(crate) fn set_callable_name_used_signature_keys(&mut self, keys: HashSet) { + self.callable_name_used_signature_keys = keys; + } + + /// Configure the local callable registry used by generated callable-name helpers. + pub(crate) fn set_callable_name_local_registry(&mut self, registry: FunctionRegistry) { + self.callable_name_local_registry = Some(registry); + } + + /// Add every concrete function-pointer signature from one lowered program to the cross-module resolver map. + pub(crate) fn add_callable_name_resolutions_for_program( + out: &mut HashMap, + module_path: Vec, + program: &IrProgram, + ) { + for (_, signature) in program.function_registry.iter() { + let params = signature + .params + .iter() + .map(|param| param.ty.clone()) + .collect::>(); + let ret = signature.return_type.clone(); + let Some(key) = Self::callable_name_signature_key(¶ms, &ret) else { + continue; + }; + let resolution = out.entry(key).or_insert_with(|| CallableNameResolution { + params, + ret, + module_paths: Vec::new(), + }); + if !resolution.module_paths.contains(&module_path) { + resolution.module_paths.push(module_path.clone()); + } + } + for resolution in out.values_mut() { + resolution.module_paths.sort(); } } + /// Return the deterministic helper identifier for a concrete callable signature key. + pub(super) fn callable_name_helper_ident(key: &str) -> proc_macro2::Ident { + format_ident!( + "__incan_callable_name_{:016x}", + Self::stable_callable_name_hash(key.as_bytes()) + ) + } + + /// Return a stable signature key for callable-name helpers when the function-pointer type is concrete. + pub(super) fn callable_name_signature_key(params: &[IrType], ret: &IrType) -> Option { + if !params.iter().all(Self::callable_name_type_supported) || !Self::callable_name_type_supported(ret) { + return None; + } + let params = params.iter().map(IrType::rust_name).collect::>().join(", "); + Some(format!("fn({params}) -> {}", ret.rust_name())) + } + + fn callable_name_signature_key_from_signature(signature: &FunctionSignature) -> Option { + let params = signature + .params + .iter() + .map(|param| param.ty.clone()) + .collect::>(); + Self::callable_name_signature_key(¶ms, &signature.return_type) + } + + fn callable_name_type_supported(ty: &IrType) -> bool { + match ty { + IrType::Unknown | IrType::Generic(_) | IrType::ImplTrait(_) | IrType::SelfType => false, + IrType::List(inner) + | IrType::Set(inner) + | IrType::Option(inner) + | IrType::Ref(inner) + | IrType::RefMut(inner) => Self::callable_name_type_supported(inner), + IrType::Dict(key, value) | IrType::Result(key, value) => { + Self::callable_name_type_supported(key) && Self::callable_name_type_supported(value) + } + IrType::Tuple(items) => items.iter().all(Self::callable_name_type_supported), + IrType::NamedGeneric(_, args) => args.iter().all(Self::callable_name_type_supported), + IrType::Function { params, ret } => Self::callable_name_signature_key(params, ret).is_some(), + IrType::Unit + | IrType::Bool + | IrType::Int + | IrType::Float + | IrType::Decimal { .. } + | IrType::String + | IrType::StrRef + | IrType::StaticStr + | IrType::FrozenStr + | IrType::Bytes + | IrType::StaticBytes + | IrType::FrozenBytes + | IrType::Numeric(_) + | IrType::Struct(_) + | IrType::Enum(_) + | IrType::Trait(_) => true, + } + } + + fn stable_callable_name_hash(bytes: &[u8]) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash + } + + pub(super) fn local_callable_name_signature_keys(&self) -> HashSet { + self.callable_name_local_registry() + .iter() + .filter_map(|(_, signature)| Self::callable_name_signature_key_from_signature(signature)) + .collect() + } + + pub(super) fn callable_name_local_registry(&self) -> &FunctionRegistry { + self.callable_name_local_registry + .as_ref() + .unwrap_or(self.function_registry) + } + /// 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 40ea06cc0..d7219c5e3 100644 --- a/src/backend/ir/emit/program.rs +++ b/src/backend/ir/emit/program.rs @@ -620,6 +620,15 @@ impl<'program> GeneratedUseAnalyzer<'program> { } IrExprKind::Field { object, field } => { self.scan_expr(object); + if field == "__name__" + && let IrType::Function { params, ret } = &object.ty + && let Some(key) = IrEmitter::callable_name_signature_key(params, ret) + { + self.analysis.callable_name_signature_keys.insert(key); + } + if field == "__name__" && matches!(object.ty, IrType::Generic(_)) { + self.analysis.uses_generic_callable_name_trait = true; + } if let Some(type_name) = self.object_nominal_type_name(object) { let field = self .struct_field_aliases @@ -830,28 +839,36 @@ impl<'program> GeneratedUseAnalyzer<'program> { _ => None, }; let canonical_name = canonical_path.as_ref().and_then(|path| path.last()).map(String::as_str); - 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()) - .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, + 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, + }) } /// Record named function arguments that need private adapters for borrowed function-pointer parameters. @@ -2261,6 +2278,232 @@ impl<'a> IrEmitter<'a> { Ok(format!("{}{}", header, with_marker)) } + pub(crate) fn callable_name_signature_keys_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( + program, + externally_reachable_items, + preserve_public_items, + external_error_trait_types, + ) + .uses_generic_callable_name_trait + } + + fn callable_name_signature_for_key(&self, key: &str) -> Option<(Vec, IrType)> { + self.callable_name_local_registry() + .iter() + .find_map(|(_, signature)| { + let params = signature + .params + .iter() + .map(|param| param.ty.clone()) + .collect::>(); + (Self::callable_name_signature_key(¶ms, &signature.return_type).as_deref() == Some(key)) + .then(|| (params, signature.return_type.clone())) + }) + .or_else(|| { + self.callable_name_resolutions + .get(key) + .map(|resolution| (resolution.params.clone(), resolution.ret.clone())) + }) + } + + fn callable_name_helper_keys( + &self, + local_callable_name_signature_keys: &HashSet, + include_all_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()), + ); + } + for (key, resolution) in &self.callable_name_resolutions { + if self.callable_name_used_signature_keys.contains(key) + && resolution + .module_paths + .contains(&self.callable_name_current_module_path) + { + keys.insert(key.clone()); + } + } + let mut keys = keys.into_iter().collect::>(); + keys.sort(); + keys + } + + fn callable_name_resolution_expr(&self, key: &str, callable_tokens: TokenStream) -> TokenStream { + let helper = Self::callable_name_helper_ident(key); + let mut helper_calls = Vec::new(); + helper_calls.push(quote! { #helper(#callable_tokens) }); + if let Some(resolution) = self.callable_name_resolutions.get(key) { + for module_path in &resolution.module_paths { + if module_path == &self.callable_name_current_module_path { + continue; + } + if module_path.is_empty() && !self.callable_name_current_module_path.is_empty() { + continue; + } + let helper_path = self.emit_callable_name_helper_path(module_path, key); + helper_calls.push(quote! { #helper_path(#callable_tokens) }); + } + } + let fallback = proc_macro2::Literal::string(""); + let mut resolved = quote! { #fallback.to_string() }; + for helper_call in helper_calls.into_iter().rev() { + resolved = quote! { + if let Some(__incan_name) = #helper_call { + __incan_name.to_string() + } else { + #resolved + } + }; + } + resolved + } + + fn emit_generic_callable_name_trait(&self, keys: &[String]) -> Option { + if keys.is_empty() { + 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 }); + Some(quote! { + impl #trait_ident for #fn_ty { + fn __incan_callable_name(&self) -> String { + let __incan_callable: #fn_ty = *self; + #resolved + } + } + }) + }) + .collect::>(); + if impls.is_empty() { + return None; + } + Some(quote! { + pub trait #trait_ident { + fn __incan_callable_name(&self) -> String; + } + + #(#impls)* + }) + } + + fn emit_callable_name_helpers( + &self, + emitted_callable_names: &HashSet, + keys: &[String], + ) -> Vec { + keys.iter() + .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 mut candidates = self + .callable_name_local_registry() + .iter() + .filter(|(name, signature)| { + emitted_callable_names.contains(*name) + && signature.params.len() == params.len() + && signature.params.iter().map(|param| ¶m.ty).eq(params.iter()) + && signature.return_type == ret + }) + .map(|(name, _)| { + let source_name = name.strip_prefix("__incan_original_").unwrap_or(name); + (name.clone(), source_name.to_string()) + }) + .collect::>(); + candidates.sort_by(|left, right| left.0.cmp(&right.0)); + let has_candidates = !candidates.is_empty(); + + let mut body = quote! { None }; + for (candidate, source_name) in candidates.into_iter().rev() { + let candidate_ident = Self::rust_ident(&candidate); + let source_literal = proc_macro2::Literal::string(&source_name); + body = quote! { + if std::ptr::fn_addr_eq(callable, #candidate_ident as #fn_ty) { + Some(#source_literal) + } else { + #body + } + }; + } + let callable_param = if has_candidates { + Self::rust_ident("callable") + } else { + Self::rust_ident("_callable") + }; + + let visibility = if self.callable_name_resolutions.get(key).is_some_and(|resolution| { + self.callable_name_used_signature_keys.contains(key) + && resolution + .module_paths + .contains(&self.callable_name_current_module_path) + }) { + quote! { pub(crate) } + } else { + quote! {} + }; + let private_interfaces_allow = (!visibility.is_empty()).then(|| { + quote! { #[allow(private_interfaces)] } + }); + + Some(quote! { + #private_interfaces_allow + #visibility fn #helper(#callable_param: #fn_ty) -> Option<&'static str> { + #body + } + }) + }) + .collect() + } + /// Emit a program to TokenStream (without formatting). pub fn emit_program_tokens(&self, program: &IrProgram) -> Result { let mut items = Vec::new(); @@ -2273,9 +2516,13 @@ impl<'a> IrEmitter<'a> { let uses_stdlib_error_trait = analysis.uses_stdlib_error_trait; let result_observer_callable_types = analysis.result_observer_callable_types.clone(); let borrowed_function_adapters = analysis.borrowed_function_adapters.clone(); + let local_callable_name_signature_keys = analysis.callable_name_signature_keys.clone(); + let uses_generic_callable_name_trait = analysis.uses_generic_callable_name_trait; self.set_result_observer_callable_types(result_observer_callable_types); self.set_borrowed_function_adapters(borrowed_function_adapters); self.set_generated_use_analysis(analysis); + let callable_name_helper_keys = + self.callable_name_helper_keys(&local_callable_name_signature_keys, uses_generic_callable_name_trait); let emitted_declarations: Vec<&IrDecl> = program .declarations @@ -2422,6 +2669,21 @@ impl<'a> IrEmitter<'a> { }); } + let emitted_callable_names: HashSet = emitted_declarations + .iter() + .filter_map(|decl| match &decl.kind { + IrDeclKind::Function(func) => Some(func.name.clone()), + IrDeclKind::SymbolAlias { name, .. } => Some(name.clone()), + _ => None, + }) + .collect(); + items.extend(self.emit_callable_name_helpers(&emitted_callable_names, &callable_name_helper_keys)); + if uses_generic_callable_name_trait + && let Some(trait_item) = self.emit_generic_callable_name_trait(&callable_name_helper_keys) + { + items.push(trait_item); + } + // Emit all declarations. let defines_ordinal_key_trait = Self::emitted_declarations_define_capability_trait( program, diff --git a/src/backend/ir/lower/decl/functions.rs b/src/backend/ir/lower/decl/functions.rs index 85e5adf9b..716f9362f 100644 --- a/src/backend/ir/lower/decl/functions.rs +++ b/src/backend/ir/lower/decl/functions.rs @@ -2,6 +2,8 @@ use super::super::super::Mutability; use super::super::super::decl::{FunctionParam, IrFunction, IrTraitBound, IrTraitBoundOrigin}; +use super::super::super::expr::{IrDictEntry, IrExprKind, IrGeneratorClause, IrListEntry}; +use super::super::super::stmt::{AssignTarget, IrStmt, IrStmtKind}; use super::super::super::types::IrType; use super::super::AstLowering; use super::super::errors::LoweringError; @@ -31,6 +33,269 @@ fn body_contains_yield(body: &[ast::Spanned]) -> bool { }) } +fn collect_generic_callable_name_type_params_from_expr(expr: &super::super::super::IrExpr, out: &mut Vec) { + match &expr.kind { + IrExprKind::Field { object, field } => { + if field == "__name__" + && let IrType::Generic(name) = &object.ty + && !out.contains(name) + { + out.push(name.clone()); + } + collect_generic_callable_name_type_params_from_expr(object, out); + } + IrExprKind::BinOp { left, right, .. } => { + collect_generic_callable_name_type_params_from_expr(left, out); + collect_generic_callable_name_type_params_from_expr(right, out); + } + IrExprKind::UnaryOp { operand, .. } + | IrExprKind::Await(operand) + | IrExprKind::Try(operand) + | IrExprKind::NumericResize { expr: operand, .. } + | IrExprKind::Cast { expr: operand, .. } + | IrExprKind::InteropCoerce { expr: operand, .. } => { + collect_generic_callable_name_type_params_from_expr(operand, out); + } + IrExprKind::Call { func, args, .. } => { + collect_generic_callable_name_type_params_from_expr(func, out); + for arg in args { + collect_generic_callable_name_type_params_from_expr(&arg.expr, out); + } + } + IrExprKind::BuiltinCall { args, .. } => { + for arg in args { + collect_generic_callable_name_type_params_from_expr(arg, out); + } + } + IrExprKind::KnownMethodCall { args, .. } => { + for arg in args { + collect_generic_callable_name_type_params_from_expr(&arg.expr, out); + } + } + IrExprKind::MethodCall { receiver, args, .. } => { + collect_generic_callable_name_type_params_from_expr(receiver, out); + for arg in args { + collect_generic_callable_name_type_params_from_expr(&arg.expr, out); + } + } + IrExprKind::Index { object, index } => { + collect_generic_callable_name_type_params_from_expr(object, out); + collect_generic_callable_name_type_params_from_expr(index, out); + } + IrExprKind::Slice { + target, + start, + end, + step, + } => { + collect_generic_callable_name_type_params_from_expr(target, out); + for expr in [start, end, step].into_iter().flatten() { + collect_generic_callable_name_type_params_from_expr(expr, out); + } + } + IrExprKind::ListComp { + element, + iterable, + filter, + .. + } => { + collect_generic_callable_name_type_params_from_expr(element, out); + collect_generic_callable_name_type_params_from_expr(iterable, out); + if let Some(filter) = filter { + collect_generic_callable_name_type_params_from_expr(filter, out); + } + } + IrExprKind::DictComp { + key, + value, + iterable, + filter, + .. + } => { + collect_generic_callable_name_type_params_from_expr(key, out); + collect_generic_callable_name_type_params_from_expr(value, out); + collect_generic_callable_name_type_params_from_expr(iterable, out); + if let Some(filter) = filter { + collect_generic_callable_name_type_params_from_expr(filter, out); + } + } + IrExprKind::Generator { element, clauses } => { + collect_generic_callable_name_type_params_from_expr(element, out); + for clause in clauses { + match clause { + IrGeneratorClause::For { iterable, .. } => { + collect_generic_callable_name_type_params_from_expr(iterable, out); + } + IrGeneratorClause::If(condition) => { + collect_generic_callable_name_type_params_from_expr(condition, out); + } + } + } + } + IrExprKind::List(items) => { + for item in items { + match item { + IrListEntry::Element(value) | IrListEntry::Spread(value) => { + collect_generic_callable_name_type_params_from_expr(value, out); + } + } + } + } + IrExprKind::Dict(items) => { + for item in items { + match item { + IrDictEntry::Pair(key, value) => { + collect_generic_callable_name_type_params_from_expr(key, out); + collect_generic_callable_name_type_params_from_expr(value, out); + } + IrDictEntry::Spread(value) => { + collect_generic_callable_name_type_params_from_expr(value, out); + } + } + } + } + IrExprKind::Set(items) | IrExprKind::Tuple(items) => { + for item in items { + collect_generic_callable_name_type_params_from_expr(item, out); + } + } + IrExprKind::Struct { fields, .. } => { + for (_, value) in fields { + collect_generic_callable_name_type_params_from_expr(value, out); + } + } + IrExprKind::If { + condition, + then_branch, + else_branch, + } => { + collect_generic_callable_name_type_params_from_expr(condition, out); + collect_generic_callable_name_type_params_from_expr(then_branch, out); + if let Some(else_branch) = else_branch { + collect_generic_callable_name_type_params_from_expr(else_branch, out); + } + } + IrExprKind::Match { scrutinee, arms } => { + collect_generic_callable_name_type_params_from_expr(scrutinee, out); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_generic_callable_name_type_params_from_expr(guard, out); + } + collect_generic_callable_name_type_params_from_expr(&arm.body, out); + } + } + IrExprKind::Closure { body, .. } => { + collect_generic_callable_name_type_params_from_expr(body, out); + } + IrExprKind::Block { stmts, value } => { + collect_generic_callable_name_type_params_from_stmts(stmts, out); + if let Some(value) = value { + collect_generic_callable_name_type_params_from_expr(value, out); + } + } + IrExprKind::Loop { body } => collect_generic_callable_name_type_params_from_stmts(body, out), + IrExprKind::Race { arms, .. } => { + for arm in arms { + collect_generic_callable_name_type_params_from_expr(&arm.awaitable, out); + collect_generic_callable_name_type_params_from_expr(&arm.body, out); + } + } + IrExprKind::Range { start, end, .. } => { + for expr in [start, end].into_iter().flatten() { + collect_generic_callable_name_type_params_from_expr(expr, out); + } + } + IrExprKind::Format { parts } => { + for part in parts { + if let super::super::super::expr::FormatPart::Expr { expr, .. } = part { + collect_generic_callable_name_type_params_from_expr(expr, out); + } + } + } + IrExprKind::Var { .. } + | IrExprKind::StaticRead { .. } + | IrExprKind::StaticBinding { .. } + | IrExprKind::AssociatedFunction { .. } + | IrExprKind::Unit + | IrExprKind::None + | IrExprKind::Bool(_) + | IrExprKind::Int(_) + | IrExprKind::IntLiteral(_) + | IrExprKind::Float(_) + | IrExprKind::Decimal(_) + | IrExprKind::String(_) + | IrExprKind::Bytes(_) + | IrExprKind::Literal(_) + | IrExprKind::FieldsList(_) + | IrExprKind::SerdeToJson + | IrExprKind::SerdeFromJson(_) => {} + } +} + +fn collect_generic_callable_name_type_params_from_stmts(stmts: &[IrStmt], out: &mut Vec) { + for stmt in stmts { + match &stmt.kind { + IrStmtKind::Expr(expr) + | IrStmtKind::Yield(expr) + | IrStmtKind::Let { value: expr, .. } + | IrStmtKind::CompoundAssign { value: expr, .. } => { + collect_generic_callable_name_type_params_from_expr(expr, out); + } + IrStmtKind::Assign { target, value } => { + collect_generic_callable_name_type_params_from_assign_target(target, out); + collect_generic_callable_name_type_params_from_expr(value, out); + } + IrStmtKind::Return(Some(expr)) => collect_generic_callable_name_type_params_from_expr(expr, out), + IrStmtKind::Break { value: Some(expr), .. } => { + collect_generic_callable_name_type_params_from_expr(expr, out); + } + IrStmtKind::While { condition, body, .. } => { + collect_generic_callable_name_type_params_from_expr(condition, out); + collect_generic_callable_name_type_params_from_stmts(body, out); + } + IrStmtKind::For { iterable, body, .. } => { + collect_generic_callable_name_type_params_from_expr(iterable, out); + collect_generic_callable_name_type_params_from_stmts(body, out); + } + IrStmtKind::Loop { body, .. } | IrStmtKind::Block(body) => { + collect_generic_callable_name_type_params_from_stmts(body, out); + } + IrStmtKind::If { + condition, + then_branch, + else_branch, + } => { + collect_generic_callable_name_type_params_from_expr(condition, out); + collect_generic_callable_name_type_params_from_stmts(then_branch, out); + if let Some(else_branch) = else_branch { + collect_generic_callable_name_type_params_from_stmts(else_branch, out); + } + } + IrStmtKind::Match { scrutinee, arms } => { + collect_generic_callable_name_type_params_from_expr(scrutinee, out); + for arm in arms { + if let Some(guard) = &arm.guard { + collect_generic_callable_name_type_params_from_expr(guard, out); + } + collect_generic_callable_name_type_params_from_expr(&arm.body, out); + } + } + IrStmtKind::Return(None) | IrStmtKind::Break { value: None, .. } | IrStmtKind::Continue(_) => {} + } + } +} + +fn collect_generic_callable_name_type_params_from_assign_target(target: &AssignTarget, out: &mut Vec) { + match target { + AssignTarget::Field { object, .. } => collect_generic_callable_name_type_params_from_expr(object, out), + AssignTarget::Index { object, index } => { + collect_generic_callable_name_type_params_from_expr(object, out); + collect_generic_callable_name_type_params_from_expr(index, out); + } + AssignTarget::Var(_) | AssignTarget::StaticBinding(_) | AssignTarget::Static(_) => {} + } +} + impl AstLowering { /// Lower a function declaration. /// @@ -133,6 +398,21 @@ impl AstLowering { let mut all_type_params = Self::lower_type_params(&f.type_params); all_type_params.extend(hidden_type_params); + let mut callable_name_type_params = Vec::new(); + collect_generic_callable_name_type_params_from_stmts(&body, &mut callable_name_type_params); + for type_param_name in callable_name_type_params { + if let Some(type_param) = all_type_params + .iter_mut() + .find(|type_param| type_param.name == type_param_name) + && !type_param.bounds.iter().any(|bound| { + bound.trait_path == "__IncanCallableName" + && bound.type_args.is_empty() + && bound.assoc_types.is_empty() + }) + { + type_param.bounds.push(IrTraitBound::simple("__IncanCallableName")); + } + } if is_generator { for type_param in &mut all_type_params { for trait_path in ["Send", "Static"] { diff --git a/src/backend/ir/mod.rs b/src/backend/ir/mod.rs index 0fa6d1f16..fe5095110 100644 --- a/src/backend/ir/mod.rs +++ b/src/backend/ir/mod.rs @@ -71,16 +71,42 @@ impl FunctionRegistry { Self::default() } + /// Build the registry key used for a canonical module path such as `helpers.normalize`. + pub fn canonical_key(path: &[String]) -> Option { + if path.len() < 2 { + return None; + } + Some(path.join("::")) + } + /// Register a function signature pub fn register(&mut self, name: String, params: Vec, return_type: IrType) { self.signatures.insert(name, FunctionSignature { params, return_type }); } + /// Register a function signature under its canonical module path. + pub fn register_canonical_path(&mut self, path: &[String], params: Vec, return_type: IrType) { + if let Some(key) = Self::canonical_key(path) { + self.register(key, params, return_type); + } + } + /// Look up a function signature by name pub fn get(&self, name: &str) -> Option<&FunctionSignature> { self.signatures.get(name) } + /// Look up a function signature by canonical module path. + pub fn get_canonical_path(&self, path: &[String]) -> Option<&FunctionSignature> { + let key = Self::canonical_key(path)?; + self.signatures.get(&key) + } + + /// Iterate over registered function signatures. + pub fn iter(&self) -> impl Iterator { + self.signatures.iter() + } + /// Merge another registry into this one pub fn merge(&mut self, other: &FunctionRegistry) { for (name, sig) in &other.signatures { diff --git a/src/backend/ir/trait_bound_inference.rs b/src/backend/ir/trait_bound_inference.rs index d55adb91b..0ac9dd38d 100644 --- a/src/backend/ir/trait_bound_inference.rs +++ b/src/backend/ir/trait_bound_inference.rs @@ -2178,6 +2178,7 @@ fn expr_type_param_name( fn type_param_name_from_ir_type(ty: &IrType, type_params: &HashSet<&str>) -> Option { match ty { IrType::Generic(name) if type_params.contains(name.as_str()) => Some(name.clone()), + IrType::Struct(name) if type_params.contains(name.as_str()) => Some(name.clone()), _ => None, } } @@ -2703,6 +2704,9 @@ fn collect_calls_in_expr( recurse_stmt(stmt, result); } } + IrExprKind::Closure { body, .. } => { + recurse_expr(body, result); + } IrExprKind::Race { arms, .. } => { for arm in arms { recurse_expr(&arm.awaitable, result); diff --git a/src/frontend/module.rs b/src/frontend/module.rs index 1130b9104..978c05fd6 100644 --- a/src/frontend/module.rs +++ b/src/frontend/module.rs @@ -455,6 +455,11 @@ pub fn exported_symbols(ast: &Program) -> Vec { exports.push(ExportedSymbol::Function(f.name.clone())); } } + Declaration::Partial(p) => { + if matches!(p.visibility, Visibility::Public) { + exports.push(ExportedSymbol::Function(p.name.clone())); + } + } Declaration::Import(import) => { // Both `from module import X` and `from rust::crate import X` are treated as re-exports. This lets // stdlib files like `response.incn` expose axum types (`from rust::axum import Json`) to importers @@ -472,7 +477,7 @@ pub fn exported_symbols(ast: &Program) -> Vec { } } } - Declaration::Partial(_) | Declaration::Docstring(_) | Declaration::TestModule(_) => {} + Declaration::Docstring(_) | Declaration::TestModule(_) => {} } } @@ -1091,6 +1096,26 @@ source-root = "library" } } + #[test] + fn test_exported_symbols_partial() -> Result<(), Vec> { + let source = r#" +pub def route(method: str, path: str) -> str: + return path + +pub get = partial route(method="GET") +"#; + let tokens = lexer::lex(source).map_err(|e| e.iter().map(|x| x.message.clone()).collect::>())?; + let ast = parser::parse(&tokens).map_err(|e| e.iter().map(|x| x.message.clone()).collect::>())?; + let exports = exported_symbols(&ast); + assert!( + exports + .iter() + .any(|export| matches!(export, ExportedSymbol::Function(name) if name == "get")), + "expected public partial callable export, got {exports:?}" + ); + Ok(()) + } + #[test] fn test_exported_symbols_ignores_module_imports() { let import = ImportDecl { diff --git a/src/frontend/typechecker/check_expr/access.rs b/src/frontend/typechecker/check_expr/access.rs index 07fb8682f..35f8ec9b0 100644 --- a/src/frontend/typechecker/check_expr/access.rs +++ b/src/frontend/typechecker/check_expr/access.rs @@ -2489,6 +2489,9 @@ impl TypeChecker { } let resolve_on = |checker: &mut Self, ty: &ResolvedType| -> ResolvedType { + if field == "__name__" && checker.is_generic_placeholder_type(ty) { + return ResolvedType::Str; + } match ty { ResolvedType::Unknown => ResolvedType::Unknown, // Trait default methods typecheck against `Self`, but field access must be declared via @@ -2512,6 +2515,7 @@ impl TypeChecker { checker.errors.push(errors::missing_field(&ty.to_string(), field, span)); ResolvedType::Unknown } + ResolvedType::Function(_, _) if field == "__name__" => ResolvedType::Str, ResolvedType::Named(type_name) => { if let Some(field_ty) = checker.resolve_nominal_field_type(type_name, None, field, span) { return field_ty; @@ -2537,6 +2541,9 @@ impl TypeChecker { ResolvedType::Unknown } ResolvedType::TypeVar(name) => { + if field == "__name__" { + return ResolvedType::Str; + } if let Some(property_ty) = checker.resolve_generic_placeholder_property(name, field, span) { return property_ty; } diff --git a/src/frontend/typechecker/tests.rs b/src/frontend/typechecker/tests.rs index aa9259243..4535c495f 100644 --- a/src/frontend/typechecker/tests.rs +++ b/src/frontend/typechecker/tests.rs @@ -529,6 +529,37 @@ def use() -> str: .unwrap_or_else(|errs| panic!("consumer should import public partial callable: {errs:?}")); } +#[test] +fn test_from_import_accepts_public_partial_export() { + let library = parse_program( + r#" +pub model Spec: + namespace: str + policy: str + klass: str + lifecycle: str + +pub core_spec = partial Spec(namespace="core", policy="portable") +"#, + "partial import library", + ); + let consumer = parse_program( + r#" +from presets import core_spec + +def use() -> str: + spec = core_spec(klass="scalar", lifecycle="v1") + return spec.namespace +"#, + "partial from-import consumer", + ); + + let mut checker = TypeChecker::new(); + checker + .check_with_imports(&consumer, &[("presets", &library)]) + .unwrap_or_else(|errs| panic!("consumer should import public partial callable by name: {errs:?}")); +} + #[test] fn test_method_partial_presets_project_as_defaults_for_trait_and_model() { let source = r#" @@ -5044,6 +5075,23 @@ def main() -> int: Ok(()) } +#[test] +fn test_function_callable_name_metadata_typechecks_issue694() { + let source = r#" +def capture(func: (int) -> int) -> ((int) -> int): + name: str = func.__name__ + return func + +def registered() -> (((int) -> int) -> ((int) -> int)): + return capture + +@registered() +pub def sample(value: int) -> int: + return value + 1 +"#; + assert_check_ok(source); +} + #[test] fn test_user_defined_decorator_factory_and_stacking_apply_bottom_up() { let source = r#" diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 650a4a3ab..2c65436cf 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1829,6 +1829,222 @@ def test_alias() -> None: Ok(()) } +#[test] +fn test_imported_public_partial_presets_keep_projected_call_surface_issue698() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "imported_public_partial_preset", "")?; + 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("presets.incn"), + r#"pub model Spec: + pub namespace: str + pub policy: str + pub klass: str + pub lifecycle: str + + +"""Build a core portable spec.""" +pub core_spec = partial Spec(namespace="core", policy="portable") +"#, + )?; + fs::write( + tests_dir.join("test_imported_partial.incn"), + r#"from presets import core_spec + + +def test_imported_partial_preset_keeps_presets() -> None: + spec = core_spec(klass="scalar", lifecycle="v1") + assert spec.namespace == "core" + assert spec.policy == "portable" + assert spec.klass == "scalar" + assert spec.lifecycle == "v1" +"#, + )?; + + let test_path = tests_dir.join("test_imported_partial.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 public partial issue698"); + Ok(()) +} + +#[test] +fn test_imported_partial_preset_defaults_survive_decorator_argument_issue698() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "imported_partial_decorator_argument", "")?; + 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("function_registry.incn"), + r#"pub model FunctionSpec: + pub namespace: str + pub deterministic: bool + pub lifecycle: str + + +pub static registered_names: list[str] = [] +pub static registered_namespaces: list[str] = [] + + +pub def capture(func: (int) -> int) -> ((int) -> int): + registered_names.append(func.__name__) + return func + + +pub def add(spec: FunctionSpec) -> (((int) -> int) -> ((int) -> int)): + registered_namespaces.append(spec.namespace) + return capture + + +pub deterministic_spec = partial FunctionSpec(namespace="core", deterministic=true) +"#, + )?; + fs::write( + src_dir.join("helpers.incn"), + r#"from function_registry import add, deterministic_spec + + +@add(deterministic_spec(lifecycle="stable")) +pub def 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 + + +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" +"#, + )?; + + let test_path = tests_dir.join("test_registry_intent.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 in decorator argument issue698", + ); + Ok(()) +} + +#[test] +fn test_decorator_callable_exposes_source_name_issue694() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "decorator_callable_name", "")?; + 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( + &main_path, + r#"def main() -> None: + pass +"#, + )?; + fs::write( + src_dir.join("registry.incn"), + r#"pub static names: list[str] = [] + + +pub def capture(func: (int) -> int) -> ((int) -> int): + names.append(func.__name__) + return func + + +pub def registered() -> (((int) -> int) -> ((int) -> int)): + return capture +"#, + )?; + fs::write( + tests_dir.join("test_callable_name.incn"), + r#"from registry import names, registered + + +@registered() +pub def sample(value: int) -> int: + return value + 1 + + +def test_decorator_can_read_specific_callable_name() -> None: + assert sample(1) == 2 + assert names[0] == "sample" +"#, + )?; + + let test_path = tests_dir.join("test_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 decorator callable name issue694"); + Ok(()) +} + +#[test] +fn test_generic_decorator_callable_exposes_source_name_issue694() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project(tmp.path(), "generic_decorator_callable_name", "")?; + 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 registered[F]() -> ((F) -> F): + return (func) => capture[F](func) +"#, + )?; + fs::write( + src_dir.join("helpers.incn"), + r#"from registry import names, registered + + +@registered[(int) -> int]() +pub def sample(value: int) -> int: + return value + 1 +"#, + )?; + fs::write( + tests_dir.join("test_generic_callable_name.incn"), + r#"from registry import names +from helpers import sample + + +def test_generic_decorator_can_read_callable_name() -> None: + assert sample(1) == 2 + assert names[0] == "sample" +"#, + )?; + + let test_path = tests_dir.join("test_generic_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 generic decorator callable name issue694"); + Ok(()) +} + #[test] fn build_frozen_uses_existing_lockfile_without_network() -> Result<(), Box> { let tmp = tempfile::tempdir()?; diff --git a/tests/fixtures/vocab_guardrails/semantic_string_audit.json b/tests/fixtures/vocab_guardrails/semantic_string_audit.json index 7ad8d79ef..401b471e5 100644 --- a/tests/fixtures/vocab_guardrails/semantic_string_audit.json +++ b/tests/fixtures/vocab_guardrails/semantic_string_audit.json @@ -102,6 +102,12 @@ "expected_count": 1, "expected_fingerprint": "0x79dfdfd491f691d1" }, + { + "path": "src/backend/ir/emit/expressions/indexing.rs", + "category": "callable source-name metadata emission", + "expected_count": 1, + "expected_fingerprint": "0x87dd19d1c7e84652" + }, { "path": "src/backend/ir/emit/expressions/methods.rs", "category": "quarantined metadata-free method compatibility", @@ -126,6 +132,12 @@ "expected_count": 1, "expected_fingerprint": "0x90822765d714d957" }, + { + "path": "src/backend/ir/emit/program.rs", + "category": "callable source-name metadata use analysis", + "expected_count": 2, + "expected_fingerprint": "0x497886b639dd72c9" + }, { "path": "src/backend/ir/emit/types.rs", "category": "Rust path and static trait emission compatibility", @@ -138,6 +150,12 @@ "expected_count": 23, "expected_fingerprint": "0x5c7ee976092c9c9a" }, + { + "path": "src/backend/ir/lower/decl/functions.rs", + "category": "generic callable source-name lowering compatibility", + "expected_count": 2, + "expected_fingerprint": "0xc94cea987d050f33" + }, { "path": "src/backend/ir/lower/decl/helpers.rs", "category": "primitive, derive, and Rust namespace lowering compatibility", @@ -231,8 +249,8 @@ { "path": "src/frontend/typechecker/check_expr/access.rs", "category": "method/type access surface classification", - "expected_count": 40, - "expected_fingerprint": "0xd87e478e91056a9f" + "expected_count": 43, + "expected_fingerprint": "0x8ea35141db935e1c" }, { "path": "src/frontend/typechecker/check_expr/basics.rs", diff --git a/workspaces/docs-site/docs/language/reference/feature_inventory.md b/workspaces/docs-site/docs/language/reference/feature_inventory.md index 23f4f1ab5..355577557 100644 --- a/workspaces/docs-site/docs/language/reference/feature_inventory.md +++ b/workspaces/docs-site/docs/language/reference/feature_inventory.md @@ -39,7 +39,7 @@ Use it when deciding whether code should use an existing Incan surface before ad | Symbol, method, and variant aliases | Syntax | 0.3 | None. | `pub average = alias avg`
`mean = avg`
`WARNING = alias WARN` | Aliases expose another resolved name for the same declaration, method, or enum variant without duplicating behavior. | Wrapper functions or duplicated enum variants used only for compatibility names. | [Symbol aliases](symbol_aliases.md), [Imports and modules](imports_and_modules.md), [Release 0.3](../../release_notes/0_3.md) | | Callable presets with `partial` | Syntax | 0.3 | None. | `pub get = partial route(method="GET")`
`set_alive = partial set_state(state=true)` | `partial` creates a callable surface from an existing callable by supplying named preset values. | Hand-written wrappers whose only job is to pass the same keyword defaults. | [Callable presets](callable_presets.md), [Callable presets explained](../explanation/callable_presets.md), [Release 0.3](../../release_notes/0_3.md) | | Rest parameters, unpacking, and spreads | Syntax | 0.3 | None. | `def log(*items: str, **fields: str) -> None:`
`f(*xs, **kw)`
`[*prefix, item]`
`{**base, "x": 1}` | Functions can capture `*args` / `**kwargs`; calls and literals support typed unpack/spread forms. | Manually spelling every forwarding arity or merging collections one element at a time. | [Functions and calls](functions.md), [Release 0.3](../../release_notes/0_3.md) | -| User-defined decorators | Syntax | 0.3 | None for user-defined decorators; compiler-owned decorators keep their documented imports. | `@logged`
`@registered("catalog.ref")`
`@registered[(str) -> ColumnExpr]("catalog.ref")` | Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type. | Boilerplate wrapper declarations around every function that needs the same callable transform. | [Language reference](language.md#decorators), [Derives and traits](derives_and_traits.md), [Release 0.3](../../release_notes/0_3.md) | +| User-defined decorators | Syntax | 0.3 | None for user-defined decorators; compiler-owned decorators keep their documented imports. | `@logged`
`@registered("catalog.ref")`
`func.__name__`
`@registered[(str) -> ColumnExpr]("catalog.ref")` | Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type and decorator helpers that expose `func.__name__`. | Boilerplate wrapper declarations around every function that needs the same callable transform. | [Language reference](language.md#decorators), [Derives and traits](derives_and_traits.md), [Release 0.3](../../release_notes/0_3.md) | | Generators | Syntax | 0.3 | None. | `def numbers() -> Generator[int]:`
`yield value`
`(x * 2 for x in values)` | `yield`-based functions and generator expressions produce lazy `Generator[T]` values. | Eager list construction when callers only need lazy iteration. | [Generators](generators.md), [Generators how-to](../how-to/generators.md), [Release 0.3](../../release_notes/0_3.md) | | Iterator adapters and terminal consumers | Stdlib | 0.3 | Use iterator values. | `values.iter().map(parse).filter(valid).collect()`
`items.enumerate().take(10)`
`numbers.fold(0, add)` | Iterator pipelines expose lazy adapters and explicit terminal consumers. | Manual loop accumulators for ordinary map/filter/fold pipeline shapes. | [Collection protocols](stdlib_traits/collection_protocols.md), [Release 0.3](../../release_notes/0_3.md) | | `Result[T, E]` combinators | Stdlib | 0.3 | Use `Result[T, E]` values. | `result.map(transform)`
`result.and_then(validate)`
`result.inspect(log_success)` | `Result` values support branch-local transforms, fallible chaining, recovery, and inspection taps. | Nested matches that only rewrap `Ok` / `Err` around one transformed branch. | [std.result](stdlib/result.md), [Fallible and infallible paths](../tutorials/fallible_and_infallible_paths.md), [Release 0.3](../../release_notes/0_3.md) | @@ -464,12 +464,13 @@ Canonical forms: - **Use instead of:** Boilerplate wrapper declarations around every function that needs the same callable transform. - **References:** [Language reference](language.md#decorators), [Derives and traits](derives_and_traits.md), [Release 0.3](../../release_notes/0_3.md) -Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type. +Decorators are ordinary callable values applied to functions and methods, including generic decorator factories that infer or accept the decorated function type and decorator helpers that expose `func.__name__`. Canonical forms: - `@logged` - `@registered("catalog.ref")` +- `func.__name__` - `@registered[(str) -> ColumnExpr]("catalog.ref")` ### Generators diff --git a/workspaces/docs-site/docs/language/reference/language.md b/workspaces/docs-site/docs/language/reference/language.md index 4eae0ce04..c9f1da930 100644 --- a/workspaces/docs-site/docs/language/reference/language.md +++ b/workspaces/docs-site/docs/language/reference/language.md @@ -332,6 +332,21 @@ pub def col(name: str) -> ColumnExpr: The post-decoration binding keeps the concrete callable signature of the decorated function unless the decorator deliberately returns a different callable shape. Checked API metadata and imports observe that concrete signature, not the generic helper's `F`. +The callable value passed into a decorator exposes `__name__` as the source callable name. Registry and catalog decorators can use this from concrete decorator helpers and from generic `(F) -> F` helpers, so a decorator can record `func.__name__` without requiring the decorated declaration to repeat its own public name in a string argument. + +```incan +def capture[F](func: F) -> F: + registry_names.append(func.__name__) + return func + +def registered[F]() -> ((F) -> F): + return (func) => capture[F](func) + +@registered() +pub def sample(value: int) -> int: + return value + 1 +``` + Method decorators receive an unbound callable shape with the receiver first. A decorator on `def label(self, value: int) -> str` sees `(&Box, int) -> str`; a decorator on `def bump(mut self, value: int) -> int` sees `(&mut Box, int) -> int`. The wrapper passes the actual receiver borrow through to the decorated callable, so method decorators do not require cloning the receiver. Class, model, trait, enum, newtype, field, alias, and module decorators remain limited to compiler-owned decorators. Compiler-owned decorators such as `@derive`, `@route`, `@rust.extern`, `@rust.allow`, `@staticmethod`, `@classmethod`, and `@requires` keep their existing special behavior. diff --git a/workspaces/docs-site/docs/release_notes/0_3.md b/workspaces/docs-site/docs/release_notes/0_3.md index 9e4dfcabf..489145d02 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. Read [Functions](../language/reference/functions.md) and [Checked API metadata](../tooling/reference/checked_api_metadata.md) ([RFC 036], #170, #640). +- **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). - **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). @@ -105,8 +105,10 @@ 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, 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). +- **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). - **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). - **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