diff --git a/Cargo.lock b/Cargo.lock index 2ad2036a8..d13e52ee8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "incan" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "blake2", "blake3", @@ -1527,14 +1527,14 @@ dependencies = [ [[package]] name = "incan_core" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "proc-macro2", "quote", @@ -1543,14 +1543,14 @@ dependencies = [ [[package]] name = "incan_semantics_core" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "incan_core", ] [[package]] name = "incan_semantics_stdlib" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "incan_core", "incan_semantics_core", @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "axum", "incan_core", @@ -1572,7 +1572,7 @@ dependencies = [ [[package]] name = "incan_syntax" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "incan_core", "incan_semantics_core", @@ -1593,7 +1593,7 @@ dependencies = [ [[package]] name = "incan_web_macros" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "proc-macro2", "quote", @@ -3151,7 +3151,7 @@ dependencies = [ [[package]] name = "rust_inspect" -version = "0.3.0-rc14" +version = "0.3.0-rc15" dependencies = [ "hex", "incan_core", diff --git a/Cargo.toml b/Cargo.toml index 3d2eed76e..64cc95ce9 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-rc14" +version = "0.3.0-rc15" 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 1bfd09e2d..6fa92cd9d 100644 --- a/src/backend/ir/codegen.rs +++ b/src/backend/ir/codegen.rs @@ -904,14 +904,15 @@ impl<'a> IrCodegen<'a> { // Generate module files by path let mut lowered_modules = Vec::new(); - for (name, ast, _) in &self.dependency_modules { - // Find matching path by comparing joined segments with module name - // Module name is path segments joined with "_" (e.g., "db_models") - for path in module_paths { - let path_name = path.join("_"); - if path_name != *name { - continue; - } + for (name, ast, stored_path_segments) in &self.dependency_modules { + let matching_path = if let Some(stored_path_segments) = stored_path_segments { + module_paths.iter().find(|path| *path == stored_path_segments) + } else { + // Legacy callers may still register only a flat module name. Prefer explicit path segments when they + // exist because distinct paths such as `a_b` and `a/b` share the same underscore-joined fallback. + module_paths.iter().find(|path| path.join("_") == *name) + }; + if let Some(path) = matching_path { let module_type_info = { use crate::frontend::typechecker::TypeChecker; let mut tc = TypeChecker::new(); @@ -931,7 +932,6 @@ impl<'a> IrCodegen<'a> { // newtypes (e.g., stdlib wrapper types like std.web.request.Query/Path). super::trait_bound_inference::infer_trait_bounds(&mut ir); lowered_modules.push((path.clone(), ir)); - break; } } for idx in 0..lowered_modules.len() { diff --git a/src/backend/ir/codegen/dependency_metadata.rs b/src/backend/ir/codegen/dependency_metadata.rs index 5ae5bbf2e..541e4cb13 100644 --- a/src/backend/ir/codegen/dependency_metadata.rs +++ b/src/backend/ir/codegen/dependency_metadata.rs @@ -98,7 +98,8 @@ fn has_web_route_passthrough_decorator( }) } -/// Collect dependency-module declarations that are referenced through imports. +/// Collect dependency-module declarations that must remain reachable from externally visible roots such as imports, +/// ambient logging, and web route registration. pub(super) fn collect_externally_reachable_items_by_module( main: &Program, dependency_modules: &[(&str, &Program, Option>)], diff --git a/src/backend/ir/emit/expressions/methods.rs b/src/backend/ir/emit/expressions/methods.rs index 9548b8773..e1c5093f0 100644 --- a/src/backend/ir/emit/expressions/methods.rs +++ b/src/backend/ir/emit/expressions/methods.rs @@ -432,7 +432,7 @@ impl<'a> IrEmitter<'a> { /// Return the explicitly registered compatibility borrow policy for a metadata-free external method argument. /// /// Signature metadata remains the source of truth for Rust-boundary borrowing. These policies are only for - /// default-build interop surfaces that v0.3 already emits without rust-inspect metadata. + /// default-build interop surfaces emitted without rust-inspect metadata. fn metadata_free_method_arg_borrow_policy( receiver: &TypedExpr, method: &str, diff --git a/src/backend/ir/lower/expr/calls.rs b/src/backend/ir/lower/expr/calls.rs index 1dfbfd748..d5bbea002 100644 --- a/src/backend/ir/lower/expr/calls.rs +++ b/src/backend/ir/lower/expr/calls.rs @@ -1661,6 +1661,11 @@ impl AstLowering { if let ast::Expr::Ident(name) = &f.node && let Some(builtin) = BuiltinFn::from_name(name) && imported_callee_path.is_none() + && self + .type_info + .as_ref() + .is_none_or(|info| info.ident_kind(f.span).is_none()) + && self.callable_signature_for_call_span(call_span).is_none() && !matches!(func.ty, IrType::Function { .. }) { let args_ir = self.lower_call_args(args)?.into_iter().map(|a| a.expr).collect(); diff --git a/src/backend/ir/reference_shape.rs b/src/backend/ir/reference_shape.rs index 46b668100..596aba446 100644 --- a/src/backend/ir/reference_shape.rs +++ b/src/backend/ir/reference_shape.rs @@ -1,7 +1,7 @@ //! Predicates for IR expressions that already emit Rust reference-shaped values. //! //! Ownership and coercion planning may still see these expressions as ordinary Incan surface types. Keep the -//! reference-shape predicate here so conversions, method emission, and future argument planners do not drift. +//! reference-shape predicate here so conversions, method emission, and argument planning do not drift. use super::expr::{IrExpr, IrExprKind}; use super::types::IrType; diff --git a/src/cli/test_runner/execution.rs b/src/cli/test_runner/execution.rs index 200443853..2c0fb0b69 100644 --- a/src/cli/test_runner/execution.rs +++ b/src/cli/test_runner/execution.rs @@ -220,14 +220,27 @@ fn dedupe_import_declarations(ast: &mut Program) { ast.declarations = declarations; } -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] struct TopLevelNames { types: HashSet, values: HashSet, + imported_types: HashSet, + imported_values: HashSet, +} + +#[derive(Debug, Clone)] +struct TopLevelNameSummary { + path: PathBuf, + names: TopLevelNames, } /// Collect top-level Rust item names that would collide if multiple Incan files were concatenated. fn collect_top_level_decl_names(program: &Program) -> TopLevelNames { + fn add_import_binding(name: &str, names: &mut TopLevelNames) { + names.imported_types.insert(name.to_string()); + names.imported_values.insert(name.to_string()); + } + /// Add the Rust type/value namespace names contributed by one declaration. fn collect_from_decl(decl: &Declaration, names: &mut TopLevelNames) { match decl { @@ -269,7 +282,40 @@ fn collect_top_level_decl_names(program: &Program) -> TopLevelNames { collect_from_decl(&nested.node, names); } } - Declaration::Import(_) | Declaration::Partial(_) | Declaration::Docstring(_) => {} + Declaration::Import(decl) => match &decl.kind { + ImportKind::Module(path) => { + let local = decl + .alias + .as_ref() + .or_else(|| path.segments.last()) + .map(String::as_str) + .unwrap_or("module"); + add_import_binding(local, names); + } + ImportKind::From { items, .. } + | ImportKind::PubFrom { items, .. } + | ImportKind::RustFrom { items, .. } => { + for item in items { + add_import_binding(item.alias.as_deref().unwrap_or(&item.name), names); + } + } + ImportKind::PubLibrary { library } => { + add_import_binding(decl.alias.as_deref().unwrap_or(library), names); + } + ImportKind::Python(pkg) => { + add_import_binding(decl.alias.as_deref().unwrap_or(pkg), names); + } + ImportKind::RustCrate { crate_name, path, .. } => { + let local = decl + .alias + .as_ref() + .or_else(|| path.last()) + .map(String::as_str) + .unwrap_or(crate_name); + add_import_binding(local, names); + } + }, + Declaration::Partial(_) | Declaration::Docstring(_) => {} } } @@ -280,51 +326,102 @@ fn collect_top_level_decl_names(program: &Program) -> TopLevelNames { names } -/// Return whether concatenating source files into one worker harness would collide at Rust module scope. -/// -/// Worker batches can share one process only when their source files can coexist in the generated crate. If two files -/// define the same model, function, or other top-level Rust item, the runner falls back to per-file harnesses. -fn batch_has_cross_file_top_level_collision( +fn collect_top_level_name_summary( + path: &Path, + source: &str, + library_imported_vocab: Option<&parser::ImportedLibraryVocab>, +) -> Option { + let tokens = lexer::lex(source).ok()?; + let ast = + parser::parse_with_context(&tokens, Some(path.to_string_lossy().as_ref()), library_imported_vocab).ok()?; + let names = collect_top_level_decl_names(&ast_with_inline_test_declarations(&ast)); + Some(TopLevelNameSummary { + path: path.to_path_buf(), + names, + }) +} + +fn collect_top_level_name_summaries( sources_by_file: &[(PathBuf, String)], library_imported_vocab: Option<&parser::ImportedLibraryVocab>, -) -> bool { - if sources_by_file.len() <= 1 { - return false; - } +) -> Option> { + sources_by_file + .iter() + .map(|(path, source)| collect_top_level_name_summary(path, source, library_imported_vocab)) + .collect() +} +fn top_level_summaries_have_collision<'a>(summaries: impl IntoIterator) -> bool { let mut type_owner: HashMap = HashMap::new(); let mut value_owner: HashMap = HashMap::new(); - for (path, source) in sources_by_file { - let Ok(tokens) = lexer::lex(source) else { - return false; - }; - let Ok(ast) = - parser::parse_with_context(&tokens, Some(path.to_string_lossy().as_ref()), library_imported_vocab) - else { - return false; - }; - let names = collect_top_level_decl_names(&ast_with_inline_test_declarations(&ast)); - for name in names.types { + let mut imported_type_owner: HashMap = HashMap::new(); + let mut imported_value_owner: HashMap = HashMap::new(); + for summary in summaries { + for name in &summary.names.types { + if imported_type_owner + .get(name) + .is_some_and(|owner| owner != &summary.path) + { + return true; + } if type_owner - .insert(name, path.clone()) - .is_some_and(|owner| owner != *path) + .insert(name.clone(), summary.path.clone()) + .is_some_and(|owner| owner != summary.path) { return true; } } - for name in names.values { + for name in &summary.names.values { + if imported_value_owner + .get(name) + .is_some_and(|owner| owner != &summary.path) + { + return true; + } if value_owner - .insert(name, path.clone()) - .is_some_and(|owner| owner != *path) + .insert(name.clone(), summary.path.clone()) + .is_some_and(|owner| owner != summary.path) { return true; } } + for name in &summary.names.imported_types { + if type_owner.get(name).is_some_and(|owner| owner != &summary.path) { + return true; + } + imported_type_owner + .entry(name.clone()) + .or_insert_with(|| summary.path.clone()); + } + for name in &summary.names.imported_values { + if value_owner.get(name).is_some_and(|owner| owner != &summary.path) { + return true; + } + imported_value_owner + .entry(name.clone()) + .or_insert_with(|| summary.path.clone()); + } } false } +/// Return whether concatenating source files into one worker harness would collide at Rust module scope. +/// +/// Worker batches can share one process only when their source files can coexist in the generated crate. If two files +/// define the same model, function, or another top-level Rust item, or when one file imports a name another file +/// declares, the runner falls back to per-file harnesses. +fn batch_has_cross_file_top_level_collision( + sources_by_file: &[(PathBuf, String)], + library_imported_vocab: Option<&parser::ImportedLibraryVocab>, +) -> bool { + if sources_by_file.len() <= 1 { + return false; + } + collect_top_level_name_summaries(sources_by_file, library_imported_vocab) + .is_some_and(|summaries| top_level_summaries_have_collision(&summaries)) +} + /// Partition files into greedy groups that can still share a generated Rust module scope. /// /// A single duplicate top-level name should not force the whole worker batch back to one Cargo harness per file. @@ -334,22 +431,26 @@ fn partition_collision_free_file_groups( sources_by_file: &[(PathBuf, String)], library_imported_vocab: Option<&parser::ImportedLibraryVocab>, ) -> Vec> { - let mut groups: Vec> = Vec::new(); - 'source: for (path, source) in sources_by_file { + let Some(summaries) = collect_top_level_name_summaries(sources_by_file, library_imported_vocab) else { + return vec![sources_by_file.iter().map(|(path, _)| path.clone()).collect()]; + }; + + let mut groups: Vec> = Vec::new(); + 'source: for summary in summaries { for group in &mut groups { let mut candidate = group.clone(); - candidate.push((path.clone(), source.clone())); - if !batch_has_cross_file_top_level_collision(&candidate, library_imported_vocab) { - group.push((path.clone(), source.clone())); + candidate.push(summary.clone()); + if !top_level_summaries_have_collision(&candidate) { + group.push(summary); continue 'source; } } - groups.push(vec![(path.clone(), source.clone())]); + groups.push(vec![summary]); } groups .into_iter() - .map(|group| group.into_iter().map(|(path, _)| path).collect()) + .map(|group| group.into_iter().map(|summary| summary.path).collect()) .collect() } @@ -456,7 +557,18 @@ fn parse_and_desugar_test_sources( } fn module_name_for_segments(segments: &[String]) -> String { - segments.join("_") + let mut hasher = Sha256::new(); + for segment in segments { + hasher.update(segment.as_bytes()); + hasher.update([0]); + } + let digest = hex::encode(hasher.finalize()); + let stem = if segments.is_empty() { + "module".to_string() + } else { + segments.join("_") + }; + format!("{stem}_{}", &digest[..8]) } fn read_conftest_sources(paths: &[PathBuf]) -> Result, String> { @@ -3759,6 +3871,52 @@ test test_runner_76001490ba86f677::__incan_file_tests::incan_harness_1_b ... FAI assert_eq!(name, "test_runner_76001490ba86f677"); } + #[test] + fn partition_collision_free_file_groups_considers_import_bindings() { + let sources = vec![ + ( + PathBuf::from("tests/test_imports_col.incn"), + "from helpers import col\n\ndef test_imported_col() -> None:\n assert col() == 1\n".to_string(), + ), + ( + PathBuf::from("tests/test_declares_col.incn"), + "def col() -> int:\n return 2\n\ndef test_local_col() -> None:\n assert col() == 2\n".to_string(), + ), + ]; + + let groups = partition_collision_free_file_groups(&sources, None); + + assert_eq!(groups.len(), 2); + } + + #[test] + fn partition_collision_free_file_groups_allows_repeated_import_bindings() { + let sources = vec![ + ( + PathBuf::from("tests/test_a.incn"), + "from std.testing import assert_eq\n\ndef test_a() -> None:\n assert_eq(1, 1)\n".to_string(), + ), + ( + PathBuf::from("tests/test_b.incn"), + "from std.testing import assert_eq\n\ndef test_b() -> None:\n assert_eq(2, 2)\n".to_string(), + ), + ]; + + let groups = partition_collision_free_file_groups(&sources, None); + + assert_eq!(groups.len(), 1); + } + + #[test] + fn module_name_for_segments_disambiguates_join_collisions() { + let flat = module_name_for_segments(&["a_b".to_string()]); + let nested = module_name_for_segments(&["a".to_string(), "b".to_string()]); + + assert_ne!(flat, nested); + assert!(flat.starts_with("a_b_")); + assert!(nested.starts_with("a_b_")); + } + #[test] fn inject_file_test_harness_emits_tests_module() { let rust = "fn test_a() {}\nfn test_b() {}\n"; diff --git a/src/frontend/typechecker/check_expr/calls/builtins.rs b/src/frontend/typechecker/check_expr/calls/builtins.rs index a2af26d76..f1870b73c 100644 --- a/src/frontend/typechecker/check_expr/calls/builtins.rs +++ b/src/frontend/typechecker/check_expr/calls/builtins.rs @@ -8,7 +8,7 @@ use crate::frontend::typechecker::helpers::{collection_type_id, dict_ty, list_ty use incan_core::lang::builtins::{self as core_builtins, BuiltinFnId}; use incan_core::lang::stdlib; use incan_core::lang::surface::constructors::{self as surface_constructors, ConstructorId}; -use incan_core::lang::surface::functions::{self as surface_functions, SurfaceFnId}; +use incan_core::lang::surface::functions::SurfaceFnId; use incan_core::lang::surface::types::{self as surface_types, SurfaceTypeId}; use incan_core::lang::types::collections::CollectionTypeId; @@ -118,10 +118,19 @@ impl TypeChecker { call_span: Span, respect_shadowing: bool, ) -> Option { - let has_function_symbol = respect_shadowing && self.has_non_builtin_function_definition(name); + let has_call_root_binding = respect_shadowing && self.has_non_builtin_call_root_binding(name); + let surface_function_binding = respect_shadowing + .then(|| self.active_surface_function_import(name)) + .flatten(); + let surface_type_binding = respect_shadowing + .then(|| self.active_surface_type_import(name)) + .flatten(); // Constructors (variant-like) if let Some(cid) = surface_constructors::from_str(name) { + if has_call_root_binding { + return None; + } return match cid { ConstructorId::Ok | ConstructorId::Err => { let arg_types = self.check_call_arg_types(args); @@ -178,7 +187,7 @@ impl TypeChecker { // Core builtin functions (registry-driven) if let Some(bid) = core_builtins::from_str(name) { - if has_function_symbol { + if has_call_root_binding { return None; } return match bid { @@ -459,10 +468,7 @@ impl TypeChecker { } // Surface/runtime functions (registry-driven) - if let Some(fid) = surface_functions::from_str(name) { - if !has_function_symbol { - return None; - } + if let Some(fid) = surface_function_binding { return match fid { SurfaceFnId::SleepMs => { if let Some(arg) = args.first() { @@ -542,7 +548,17 @@ impl TypeChecker { } // Surface types that behave like constructors and whose result type depends on args. - if let Some(tid) = surface_types::from_str(name) { + let surface_type = surface_type_binding.or_else(|| { + if has_call_root_binding { + None + } else { + surface_types::from_str(name) + } + }); + if let Some(tid) = surface_type { + if has_call_root_binding { + debug_assert_eq!(surface_type_binding, Some(tid)); + } return match tid { SurfaceTypeId::Json | SurfaceTypeId::Query => { Some(self.check_json_query_constructor_call(tid, args, call_span)) @@ -587,6 +603,9 @@ impl TypeChecker { // Python-like type conversion helpers (surface). These are not part of `lang::builtins`. if let Some(cid) = collection_type_id(name) { + if has_call_root_binding { + return None; + } return match cid { CollectionTypeId::Dict => { let (key_ty, val_ty) = if let Some(arg) = args.first() { diff --git a/src/frontend/typechecker/collect/stdlib_imports.rs b/src/frontend/typechecker/collect/stdlib_imports.rs index 12871e9d2..5cb4cd7d1 100644 --- a/src/frontend/typechecker/collect/stdlib_imports.rs +++ b/src/frontend/typechecker/collect/stdlib_imports.rs @@ -21,6 +21,7 @@ use crate::library_manifest::{ }; use incan_core::interop::{RustItemKind, RustTraitAssoc, fallback_rust_trait_methods, is_rust_capability_bound}; use incan_core::lang::stdlib::{self, is_typechecker_only_stdlib}; +use incan_core::lang::surface::functions as surface_functions; use incan_core::lang::surface::types as surface_types; use incan_semantics_core::{DecoratorFeature, SurfaceFeatureKey}; @@ -122,16 +123,12 @@ impl StdlibFromImportContext { }) } - /// Return `true` when a surface type import is legal from this stdlib module. - fn allows_surface_type_import(&self, item_name: &str) -> bool { - let Some(id) = surface_types::from_str(item_name) else { - return false; - }; - let Some(expected_module_path) = surface_types::stdlib_module_path(id) else { - return false; - }; + /// Return the imported surface type when it is legal from this stdlib module. + fn allowed_surface_type_import(&self, item_name: &str) -> Option { + let id = surface_types::from_str(item_name)?; + let expected_module_path = surface_types::stdlib_module_path(id)?; - match expected_module_path { + let allowed = match expected_module_path { "std.web" => self.is_web_namespace, "std.reflection" => self.is_reflection_module, _ if expected_module_path.starts_with("std.async.") => { @@ -140,7 +137,8 @@ impl StdlibFromImportContext { self.is_async_namespace && (async_root_or_prelude || self.module_path_str == expected_module_path) } _ => false, - } + }; + allowed.then_some(id) } } @@ -384,8 +382,12 @@ impl TypeChecker { if self.materialize_typechecker_only_stdlib_import(context.module, item, span) { return true; } - if stdlib_context.allows_surface_type_import(&item.name) { - self.define_from_import_symbol(item, SymbolKind::Type(TypeInfo::Builtin), span); + if let Some(surface_type) = stdlib_context.allowed_surface_type_import(&item.name) { + let local_name = Self::import_item_local_name(item); + let symbol_id = + self.define_named_import_symbol(local_name.clone(), SymbolKind::Type(TypeInfo::Builtin), span); + self.surface_type_import_bindings + .insert(local_name, (surface_type, symbol_id)); return true; } if self.materialize_stdlib_submodule_import(context.module, item, span) { @@ -451,8 +453,13 @@ impl TypeChecker { ) -> bool { if let Some(info) = self.stdlib_cache.lookup_function(&context.module.segments, &item.name) { let local_name = Self::import_item_local_name(item); + let surface_function = surface_functions::from_str(&item.name); self.record_testing_marker_import(context, item, &local_name, testing_semantics); - self.define_named_import_symbol(local_name, SymbolKind::Function(info), span); + let symbol_id = self.define_named_import_symbol(local_name.clone(), SymbolKind::Function(info), span); + if let Some(surface_function) = surface_function { + self.surface_function_import_bindings + .insert(local_name, (surface_function, symbol_id)); + } return true; } @@ -565,14 +572,14 @@ impl TypeChecker { } /// Define one already named imported symbol after root namespace validation. - fn define_named_import_symbol(&mut self, name: Ident, kind: SymbolKind, span: Span) { + fn define_named_import_symbol(&mut self, name: Ident, kind: SymbolKind, span: Span) -> SymbolId { self.validate_root_namespace(&name, span); self.symbols.define(Symbol { name, kind, span, scope: 0, - }); + }) } fn validate_pub_library_entry(&mut self, library: &str, span: Span) { @@ -1658,8 +1665,7 @@ impl TypeChecker { fn existing_from_import_symbol_kind(&self, name: &str) -> Option { let id = self.symbols.lookup(name)?; let sym = self.symbols.get(id)?; - let is_implicit_builtin = sym.scope == 0 && sym.span == Span::default(); - if is_implicit_builtin { + if Self::is_implicit_builtin_symbol(sym) { return None; } Some(sym.kind.clone()) diff --git a/src/frontend/typechecker/helpers/symbols.rs b/src/frontend/typechecker/helpers/symbols.rs index 56093f40c..4432a44f8 100644 --- a/src/frontend/typechecker/helpers/symbols.rs +++ b/src/frontend/typechecker/helpers/symbols.rs @@ -4,17 +4,37 @@ //! expression checking make the same shadowing decision. use crate::frontend::ast::Span; -use crate::frontend::symbols::SymbolKind; +use crate::frontend::symbols::Symbol; use crate::frontend::typechecker::TypeChecker; +use incan_core::lang::surface::functions::SurfaceFnId; +use incan_core::lang::surface::types::SurfaceTypeId; impl TypeChecker { - /// Return `true` when `name` resolves to a non-builtin function definition. + /// Return whether a symbol is one of the ambient builtins seeded into the root symbol table before source + /// collection. + pub(in crate::frontend::typechecker) fn is_implicit_builtin_symbol(sym: &Symbol) -> bool { + sym.scope == 0 && sym.span == Span::default() + } + + /// Return `true` when an implicit builtin-call root is shadowed by a real source/import binding. /// - /// Call checking uses this to decide whether builtin dispatch should yield to a user/imported function of the same - /// name. - pub(in crate::frontend::typechecker) fn has_non_builtin_function_definition(&self, name: &str) -> bool { - self.lookup_symbol(name).is_some_and(|sym| { - matches!(sym.kind, SymbolKind::Function(_)) && !(sym.scope == 0 && sym.span == Span::default()) - }) + /// Decorated functions are intentionally rebound from `Function` symbols to callable `Variable` symbols after + /// decorator checking. Builtin dispatch therefore has to ask whether the name is still the ambient builtin binding, + /// not whether the symbol is specifically a `Function`. + pub(in crate::frontend::typechecker) fn has_non_builtin_call_root_binding(&self, name: &str) -> bool { + self.lookup_symbol(name) + .is_some_and(|sym| !Self::is_implicit_builtin_symbol(sym)) + } + + /// Return the active stdlib surface helper imported under `name`, if the import has not been shadowed. + pub(in crate::frontend::typechecker) fn active_surface_function_import(&self, name: &str) -> Option { + let (id, imported_symbol_id) = self.surface_function_import_bindings.get(name)?; + (self.symbols.lookup(name) == Some(*imported_symbol_id)).then_some(*id) + } + + /// Return the active stdlib surface type imported under `name`, if the import has not been shadowed. + pub(in crate::frontend::typechecker) fn active_surface_type_import(&self, name: &str) -> Option { + let (id, imported_symbol_id) = self.surface_type_import_bindings.get(name)?; + (self.symbols.lookup(name) == Some(*imported_symbol_id)).then_some(*id) } } diff --git a/src/frontend/typechecker/mod.rs b/src/frontend/typechecker/mod.rs index f00bfef2c..13db28081 100644 --- a/src/frontend/typechecker/mod.rs +++ b/src/frontend/typechecker/mod.rs @@ -84,8 +84,9 @@ use incan_core::interop::{ }; use incan_core::lang::conventions; use incan_core::lang::decorators::{self as core_decorators, DecoratorId}; +use incan_core::lang::surface::functions::SurfaceFnId; use incan_core::lang::surface::types as surface_types; -use incan_core::lang::surface::types::SurfaceTypeKind; +use incan_core::lang::surface::types::{SurfaceTypeId, SurfaceTypeKind}; use incan_core::lang::traits::{self as builtin_traits, TraitId}; use incan_core::lang::types::collections::CollectionTypeId; use incan_core::lang::types::numerics::{self, NumericFamily, NumericTypeId}; @@ -232,6 +233,16 @@ pub struct TypeChecker { /// These names are disallowed in runtime call expressions; markers are decorator-only semantics consumed by the /// test runner. pub(crate) testing_marker_import_bindings: HashSet, + /// Local names bound to stdlib surface helpers that still need compiler-known call typing. + /// + /// The stored symbol id must remain the active lookup binding; later local declarations or user imports with the + /// same name shadow these helper semantics. + pub(crate) surface_function_import_bindings: HashMap, + /// Local names bound to stdlib surface types that still need compiler-known constructor typing. + /// + /// The stored symbol id must remain the active lookup binding; later local declarations or user imports with the + /// same name shadow these constructor semantics. + pub(crate) surface_type_import_bindings: HashMap, /// Fixture function names collected before body checking so dependency metadata is order-independent. pub(crate) testing_fixture_names: HashSet, /// Import aliases collected from `import` / `from ... import` declarations. @@ -303,6 +314,8 @@ impl TypeChecker { declared_crate_names: None, stdlib_cache: stdlib_loader::StdlibAstCache::new(), testing_marker_import_bindings: HashSet::new(), + surface_function_import_bindings: HashMap::new(), + surface_type_import_bindings: HashMap::new(), testing_fixture_names: HashSet::new(), import_aliases: HashMap::new(), surface_context: SurfaceContext::default(), @@ -3309,6 +3322,8 @@ impl TypeChecker { self.warnings.clear(); self.errors.clear(); self.testing_marker_import_bindings.clear(); + self.surface_function_import_bindings.clear(); + self.surface_type_import_bindings.clear(); self.testing_fixture_names.clear(); self.surface_context = SurfaceContext::from_program(program); self.import_aliases = self.surface_context.import_aliases().clone(); diff --git a/src/frontend/typechecker/tests.rs b/src/frontend/typechecker/tests.rs index e4b2adbd6..aa9259243 100644 --- a/src/frontend/typechecker/tests.rs +++ b/src/frontend/typechecker/tests.rs @@ -9402,6 +9402,69 @@ def foo() -> str: assert!(check_str(source).is_ok()); } +#[test] +fn test_local_function_named_sleep_ms_shadows_surface_helper() { + let source = r#" +def sleep_ms(value: str) -> str: + return value + +def foo() -> str: + return sleep_ms("ok") +"#; + assert_check_ok(source); +} + +#[test] +fn test_local_function_named_some_shadows_option_constructor() { + let source = r#" +def Some(value: str) -> str: + return value + +def foo() -> str: + return Some("ok") +"#; + assert_check_ok(source); +} + +#[test] +fn test_local_function_named_list_shadows_collection_helper() { + let source = r#" +def list(value: str) -> str: + return value + +def foo() -> str: + return list("ok") +"#; + assert_check_ok(source); +} + +#[test] +fn test_decorated_function_named_sum_shadows_builtin_sum_in_inline_module_tests() { + let source = r#" +model IntExpr: + value: int + +model Measure: + kind: str + +def registered[F](function_ref: str) -> ((F) -> F): + return (func) => func + +def expr(value: int) -> IntExpr: + return IntExpr(value=value) + +@registered("demo.sum") +def sum(value: IntExpr) -> Measure: + return Measure(kind="local") + +module tests: + def test_inline_sum() -> None: + measure = sum(expr(1)) + assert measure.kind == "local" +"#; + assert_check_ok(source); +} + #[test] fn test_explicit_std_builtins_sum_call() { let source = r#" diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 59df189ba..94acd4bca 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -8710,6 +8710,126 @@ def test_inferred_generic_decorator_factory_signature() -> None: Ok(()) } + #[test] + fn e2e_inline_decorated_sum_shadows_builtin_sum_issue677() -> Result<(), Box> { + let dir = write_test_project( + "incan.toml", + r#"[project] +name = "decorated_sum_inline" +version = "0.1.0" +"#, + ); + let src_dir = dir.join("src"); + std::fs::create_dir_all(&src_dir)?; + let source_path = src_dir.join("functions.incn"); + std::fs::write( + &source_path, + r#" +pub model IntExpr: + pub value: int + +pub model TextExpr: + pub value: str + +pub type Expr = IntExpr | TextExpr + +pub model Measure: + pub kind: str + +pub def registered[F](function_ref: str) -> ((F) -> F): + return (func) => func + +pub def expr(value: int) -> Expr: + return IntExpr(value=value) + +@registered("demo.sum") +pub def sum(value: Expr) -> Measure: + return Measure(kind="local") + +module tests: + def test_inline_test_resolves_decorated_sum_before_builtin_sum() -> None: + measure = sum(expr(1)) + assert measure.kind == "local" +"#, + )?; + + let output = run_incan_test_path(&source_path); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "expected decorated inline sum test to pass.\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr, + ); + assert!( + stdout.contains("functions.incn::test_inline_test_resolves_decorated_sum_before_builtin_sum"), + "expected the #677 inline test to run.\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr, + ); + Ok(()) + } + + #[test] + fn e2e_conventional_test_batches_split_import_declaration_collisions_issue676() + -> Result<(), Box> { + let dir = write_test_project( + "incan.toml", + r#"[project] +name = "import_collision_batch" +version = "0.1.0" +"#, + ); + let src_dir = dir.join("src"); + let tests_dir = dir.join("tests"); + std::fs::create_dir_all(&src_dir)?; + std::fs::create_dir_all(&tests_dir)?; + std::fs::write( + src_dir.join("helpers.incn"), + r#" +pub def col() -> int: + return 1 +"#, + )?; + std::fs::write( + tests_dir.join("test_imports_col.incn"), + r#" +from helpers import col + +def test_imported_col() -> None: + assert col() == 1 +"#, + )?; + std::fs::write( + tests_dir.join("test_declares_col.incn"), + r#" +def col() -> int: + return 2 + +def test_local_col() -> None: + assert col() == 2 +"#, + )?; + + let output = run_incan_test(&dir); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "expected import/local declaration collision batch to split and pass.\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr, + ); + assert!( + stdout.contains("test_imported_col") && stdout.contains("test_local_col"), + "expected both split test files to run.\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr, + ); + Ok(()) + } + #[test] fn e2e_method_call_decorator_factories_use_checked_receiver_lowering() -> Result<(), Box> { let dir = write_test_project( @@ -10509,527 +10629,6 @@ pub def display[T](data: DataSet[T]) -> None: Ok(()) } - #[allow(dead_code)] - fn write_nested_wasm_vocab_companion_crate( - project_root: &Path, - relative_path: &str, - package_name: &str, - ) -> Result<(), Box> { - let crate_root = project_root.join(relative_path); - std::fs::create_dir_all(crate_root.join("src"))?; - std::fs::write( - crate_root.join("Cargo.toml"), - format!( - "[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\npath = \"src/lib.rs\"\ncrate-type = [\"rlib\", \"cdylib\"]\n\n[dependencies]\nincan_vocab = {{ path = \"{}\" }}\n\n[workspace]\n", - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("crates") - .join("incan_vocab") - .display() - ), - )?; - std::fs::write( - crate_root.join("src/lib.rs"), - r#"use incan_vocab::{ - DesugarError, DesugarOutput, HelperBinding, IncanExpr, IncanStatement, KeywordActivation, - KeywordPlacement, KeywordRegistration, KeywordSpec, KeywordSurfaceKind, LibraryManifest, - VocabBodyItem, VocabClause, VocabClauseBody, VocabDeclaration, VocabDesugarer, - VocabFieldSpec, VocabRegistration, VocabSyntaxNode, -}; - -#[derive(Default)] -pub struct NestedOutputDesugarer; - -pub fn library_vocab() -> VocabRegistration { - VocabRegistration::new() - .with_keyword_registration(KeywordRegistration { - activation: KeywordActivation::OnImport { - namespace: "nested.dsl".to_string(), - }, - keywords: vec![ - KeywordSpec::new("compose", KeywordSurfaceKind::BlockDeclaration), - context_keyword("action", &["compose"]), - context_keyword("layout", &["compose"]), - context_keyword("page", &["compose"]), - context_keyword("projection", &["compose"]), - context_keyword("region", &["layout", "page"]), - context_keyword("heading", &["region"]), - context_keyword("text", &["region"]), - context_keyword("interaction", &["page"]), - context_keyword("require", &["interaction"]), - ], - valid_decorators: Vec::new(), - }) - .with_library_manifest(LibraryManifest { - helper_bindings: vec![ - helper_binding("action"), - helper_binding("layout"), - helper_binding("page_with_interactions"), - helper_binding("projection"), - helper_binding("region"), - helper_binding("heading"), - helper_binding("text"), - helper_binding("interaction"), - helper_binding("required_input"), - helper_binding("surface_with_governance"), - ], - ..LibraryManifest::default() - }) - .with_desugarer(NestedOutputDesugarer) -} - -impl VocabDesugarer for NestedOutputDesugarer { - fn desugar(&self, node: &VocabSyntaxNode) -> Result { - match node { - VocabSyntaxNode::Declaration(declaration) if declaration.keyword == "compose" => Ok( - DesugarOutput::Statements(vec![complex_artifact_let_statement(declaration)?]), - ), - VocabSyntaxNode::Declaration(_) => Err(DesugarError::new( - "nested output desugarer expected a compose declaration", - )), - _ => Err(DesugarError::new( - "nested output desugarer expected a declaration node", - )), - } - } -} - -fn helper_binding(name: &str) -> HelperBinding { - HelperBinding { - key: name.to_string(), - exported_name: name.to_string(), - } -} - -fn context_keyword(name: &str, parents: &[&str]) -> KeywordSpec { - KeywordSpec::new(name, KeywordSurfaceKind::BlockContextKeyword) - .with_placement(KeywordPlacement::in_block(parents.iter().copied())) -} - -fn complex_artifact_let_statement(declaration: &VocabDeclaration) -> Result { - Ok(IncanStatement::Let { - name: "nested_artifact".to_string(), - mutable: false, - value: complex_artifact_call(declaration)?, - }) -} - -fn complex_artifact_call(declaration: &VocabDeclaration) -> Result { - let name = declaration - .head - .name - .clone() - .ok_or_else(|| DesugarError::new("compose declarations require a name"))?; - let mut title = name.clone(); - let mut base = "/".to_string(); - let mut actions = Vec::new(); - let mut layouts = Vec::new(); - let mut pages = Vec::new(); - let mut projections = Vec::new(); - - for item in &declaration.body { - match item { - VocabBodyItem::Statement(statement) => apply_surface_statement(&mut title, &mut base, statement)?, - VocabBodyItem::Clause(clause) => match clause.keyword.as_str() { - "action" => actions.push(desugar_action(clause)?), - "layout" => layouts.push(desugar_layout(clause)?), - "page" => pages.push(desugar_page(clause)?), - "projection" => projections.push(desugar_projection(clause)?), - other => return Err(DesugarError::new(format!("unsupported compose clause `{other}`"))), - }, - VocabBodyItem::Declaration(declaration) => { - return Err(DesugarError::new(format!( - "unsupported nested declaration `{}`", - declaration.keyword - ))); - } - _ => return Err(DesugarError::new("unsupported compose body item")), - } - } - - Ok(call( - "surface_with_governance", - vec![ - string(&name), - string(&title), - string(&base), - list(actions), - list(layouts), - list(pages), - list(projections), - ], - )) -} - -fn apply_surface_statement(title: &mut String, base: &mut String, statement: &IncanStatement) -> Result<(), DesugarError> { - match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } => match name.as_str() { - "title" => *title = string_value(value, "compose title")?, - "base" => *base = string_value(value, "compose base")?, - other => return Err(DesugarError::new(format!("unsupported compose assignment `{other}`"))), - }, - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("compose body only supports assignments and nested clauses")), - } - Ok(()) -} - -fn desugar_action(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "action")?; - let mut capability = name.clone(); - let mut required_evidence = String::new(); - match &clause.body { - VocabClauseBody::FieldSet(fields) => { - for field in fields { - apply_action_assignment( - &mut capability, - &mut required_evidence, - &field.name, - field_value(field, "action assignment")?, - )?; - } - } - _ => { - for item in clause_items(clause)? { - match item { - VocabBodyItem::Statement(statement) => match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } => { - apply_action_assignment(&mut capability, &mut required_evidence, name, value)?; - } - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("action body only supports assignments")), - }, - VocabBodyItem::Clause(other) => return Err(DesugarError::new(format!("unsupported `{}` block inside action", other.keyword))), - VocabBodyItem::Declaration(_) => return Err(DesugarError::new("action body only supports assignments")), - _ => return Err(DesugarError::new("action body only supports assignments")), - } - } - } - } - Ok(call("action", vec![string(&name), string(&capability), string(&required_evidence)])) -} - -fn apply_action_assignment( - capability: &mut String, - required_evidence: &mut String, - name: &str, - value: &IncanExpr, -) -> Result<(), DesugarError> { - match name { - "capability" => *capability = string_value(value, "action capability")?, - "requires" | "required_evidence" => *required_evidence = string_value(value, "action required evidence")?, - other => return Err(DesugarError::new(format!("unsupported action assignment `{other}`"))), - } - Ok(()) -} - -fn desugar_layout(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "layout")?; - let mut regions = Vec::new(); - for item in clause_items(clause)? { - match item { - VocabBodyItem::Clause(region_clause) if region_clause.keyword == "region" => { - regions.push(string(&required_head_name(region_clause, "layout region")?)); - } - VocabBodyItem::Statement(IncanStatement::Pass) => {} - VocabBodyItem::Clause(other) => return Err(DesugarError::new(format!("unsupported `{}` block inside layout", other.keyword))), - _ => return Err(DesugarError::new("layout body only supports region blocks")), - } - } - Ok(call("layout", vec![string(&name), list(regions)])) -} - -fn desugar_page(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "page")?; - let mut route = "/".to_string(); - let mut title = name.clone(); - let mut layout_name = "SimplePage".to_string(); - let mut regions = Vec::new(); - let mut interactions = Vec::new(); - for item in clause_items(clause)? { - match item { - VocabBodyItem::Statement(statement) => match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } => match name.as_str() { - "route" => route = string_value(value, "page route")?, - "title" => title = string_value(value, "page title")?, - "layout" => layout_name = string_or_name_value(value, "page layout")?, - other => return Err(DesugarError::new(format!("unsupported page assignment `{other}`"))), - }, - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("page body only supports assignments, regions, and interactions")), - }, - VocabBodyItem::Clause(region_clause) if region_clause.keyword == "region" => { - regions.push(desugar_region(region_clause)?); - } - VocabBodyItem::Clause(interaction_clause) if interaction_clause.keyword == "interaction" => { - interactions.push(desugar_interaction(interaction_clause)?); - } - VocabBodyItem::Clause(other) => return Err(DesugarError::new(format!("unsupported `{}` block inside page", other.keyword))), - VocabBodyItem::Declaration(_) => return Err(DesugarError::new("page body does not support nested declarations")), - _ => return Err(DesugarError::new("page body only supports assignments, regions, and interactions")), - } - } - Ok(call( - "page_with_interactions", - vec![ - string(&name), - string(&route), - string(&title), - string(&layout_name), - list(regions), - list(interactions), - ], - )) -} - -fn desugar_region(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "region")?; - let mut nodes = Vec::new(); - for item in clause_items(clause)? { - match item { - VocabBodyItem::Clause(node_clause) if node_clause.keyword == "heading" => { - nodes.push(call("heading", vec![string(&required_head_string(node_clause, "heading")?)])); - } - VocabBodyItem::Clause(node_clause) if node_clause.keyword == "text" => { - nodes.push(call("text", vec![string(&required_head_string(node_clause, "text")?)])); - } - VocabBodyItem::Statement(IncanStatement::Pass) => {} - VocabBodyItem::Clause(other) => return Err(DesugarError::new(format!("unsupported `{}` block inside region", other.keyword))), - _ => return Err(DesugarError::new("region body only supports heading and text blocks")), - } - } - Ok(call("region", vec![string(&name), list(nodes)])) -} - -fn desugar_interaction(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "interaction")?; - let mut action = name.clone(); - let mut constraints = Vec::new(); - match &clause.body { - VocabClauseBody::FieldSet(fields) => { - for field in fields { - if field.name == "action" { - action = string_or_name_value(field_value(field, "interaction action")?, "interaction action")?; - } else { - return Err(DesugarError::new(format!("unsupported interaction assignment `{}`", field.name))); - } - } - } - _ => { - for item in clause_items(clause)? { - match item { - VocabBodyItem::Statement(statement) => match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } => match name.as_str() { - "action" => action = string_or_name_value(value, "interaction action")?, - other => return Err(DesugarError::new(format!("unsupported interaction assignment `{other}`"))), - }, - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("interaction body only supports assignments and require blocks")), - }, - VocabBodyItem::Clause(require_clause) if require_clause.keyword == "require" => { - constraints.push(desugar_required_input(&name, require_clause)?); - } - VocabBodyItem::Clause(other) => return Err(DesugarError::new(format!("unsupported `{}` block inside interaction", other.keyword))), - VocabBodyItem::Declaration(_) => return Err(DesugarError::new("interaction body does not support nested declarations")), - _ => return Err(DesugarError::new("interaction body only supports assignments and require blocks")), - } - } - } - } - Ok(call("interaction", vec![string(&name), string(&action), list(constraints)])) -} - -fn desugar_required_input(interaction_name: &str, clause: &VocabClause) -> Result { - let mut field = required_input_field(clause)?; - let mut label = field.clone(); - let mut min_length = "1".to_string(); - let mut evidence_key = String::new(); - match &clause.body { - VocabClauseBody::FieldSet(fields) => { - for field_spec in fields { - apply_required_input_assignment( - &mut field, - &mut label, - &mut min_length, - &mut evidence_key, - &field_spec.name, - field_value(field_spec, "require input assignment")?, - )?; - } - } - _ => { - for item in clause_items(clause)? { - match item { - VocabBodyItem::Statement(statement) => match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } => { - apply_required_input_assignment( - &mut field, - &mut label, - &mut min_length, - &mut evidence_key, - name, - value, - )?; - } - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("require input body only supports assignments")), - }, - _ => return Err(DesugarError::new("require input body only supports assignments")), - } - } - } - } - Ok(call( - "required_input", - vec![ - string(interaction_name), - string(&field), - string(&label), - string(&min_length), - string(&evidence_key), - ], - )) -} - -fn apply_required_input_assignment( - field: &mut String, - label: &mut String, - min_length: &mut String, - evidence_key: &mut String, - name: &str, - value: &IncanExpr, -) -> Result<(), DesugarError> { - match name { - "field" => *field = string_or_name_value(value, "required input field")?, - "label" => *label = string_value(value, "required input label")?, - "min_length" => *min_length = int_or_string_value(value, "required input min_length")?, - "evidence" | "evidence_key" => *evidence_key = string_value(value, "required input evidence")?, - other => return Err(DesugarError::new(format!("unsupported require input assignment `{other}`"))), - } - Ok(()) -} - -fn desugar_projection(clause: &VocabClause) -> Result { - let name = required_head_name(clause, "projection")?; - let mut target = "static-web".to_string(); - match &clause.body { - VocabClauseBody::FieldSet(fields) => { - for field in fields { - if field.name == "target" { - target = string_value(field_value(field, "projection target")?, "projection target")?; - } else { - return Err(DesugarError::new(format!("unsupported projection assignment `{}`", field.name))); - } - } - } - _ => { - for item in clause_items(clause)? { - match item { - VocabBodyItem::Statement(statement) => match statement { - IncanStatement::Let { name, value, .. } | IncanStatement::Assign { target: name, value } if name == "target" => { - target = string_value(value, "projection target")?; - } - IncanStatement::Pass => {} - _ => return Err(DesugarError::new("projection body only supports target assignment")), - }, - _ => return Err(DesugarError::new("projection body only supports target assignment")), - } - } - } - } - Ok(call("projection", vec![string(&name), string(&target)])) -} - -fn clause_items(clause: &VocabClause) -> Result<&[VocabBodyItem], DesugarError> { - match &clause.body { - VocabClauseBody::Empty => Ok(&[]), - VocabClauseBody::Items(items) => Ok(items.as_slice()), - _ => Err(DesugarError::new(format!("unsupported `{}` body shape", clause.keyword))), - } -} - -fn required_head_name(clause: &VocabClause, label: &str) -> Result { - let Some(value) = clause.head.first() else { - return Err(DesugarError::new(format!("{label} requires a name"))); - }; - string_or_name_value(value, label) -} - -fn required_head_string(clause: &VocabClause, label: &str) -> Result { - let Some(value) = clause.head.first() else { - return Err(DesugarError::new(format!("{label} requires text"))); - }; - string_value(value, label) -} - -fn required_input_field(clause: &VocabClause) -> Result { - if !clause.compound_tokens.is_empty() && clause.compound_tokens[0] == "input" { - if let Some(value) = clause.head.first() { - return string_or_name_value(value, "require input field"); - } - return Ok(String::new()); - } - if !clause.head.is_empty() { - let first = string_or_name_value(&clause.head[0], "require input marker")?; - if first == "input" { - if clause.head.len() >= 2 { - return string_or_name_value(&clause.head[1], "require input field"); - } - return Ok(String::new()); - } - } - Err(DesugarError::new("required-input constraints must use `require input`")) -} - -fn string_value(expr: &IncanExpr, label: &str) -> Result { - match expr { - IncanExpr::Str(value) => Ok(value.clone()), - _ => Err(DesugarError::new(format!("{label} must be a string literal"))), - } -} - -fn string_or_name_value(expr: &IncanExpr, label: &str) -> Result { - match expr { - IncanExpr::Str(value) | IncanExpr::Name(value) => Ok(value.clone()), - _ => Err(DesugarError::new(format!("{label} must be a name or string literal"))), - } -} - -fn int_or_string_value(expr: &IncanExpr, label: &str) -> Result { - match expr { - IncanExpr::Int(value) => Ok(value.to_string()), - IncanExpr::Str(value) => Ok(value.clone()), - _ => Err(DesugarError::new(format!("{label} must be an integer or string literal"))), - } -} - -fn field_value<'a>(field: &'a VocabFieldSpec, label: &str) -> Result<&'a IncanExpr, DesugarError> { - field - .default_value - .as_ref() - .ok_or_else(|| DesugarError::new(format!("{label} `{}` requires a value", field.name))) -} - -fn call(helper: &str, args: Vec) -> IncanExpr { - IncanExpr::Call { - callee: Box::new(IncanExpr::Helper(helper.to_string())), - args, - } -} - -fn list(items: Vec) -> IncanExpr { - IncanExpr::List(items) -} - -fn string(value: &str) -> IncanExpr { - IncanExpr::Str(value.to_string()) -} - -incan_vocab::export_wasm_desugarer!(NestedOutputDesugarer); -"#, - )?; - Ok(()) - } - fn wat_bytes_string(bytes: &[u8]) -> String { let mut escaped = String::new(); for byte in bytes { @@ -12766,6 +12365,29 @@ def main() -> None: String::from_utf8_lossy(&screen_check.stdout), String::from_utf8_lossy(&screen_check.stderr) ); + + let where_out_dir = tmp.path().join("where_out"); + let where_build = run_build(&where_main, &where_out_dir)?; + assert!( + where_build.status.success(), + "expected helper-backed `where` build to succeed.\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&where_build.stdout), + String::from_utf8_lossy(&where_build.stderr) + ); + let screen_out_dir = tmp.path().join("screen_out"); + let screen_build = run_build(&screen_main, &screen_out_dir)?; + assert!( + screen_build.status.success(), + "expected helper-backed `screen` build to succeed.\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&screen_build.stdout), + String::from_utf8_lossy(&screen_build.stderr) + ); + let where_generated = std::fs::read_to_string(where_out_dir.join("src/main.rs"))?; + let screen_generated = std::fs::read_to_string(screen_out_dir.join("src/main.rs"))?; + assert_eq!( + where_generated, screen_generated, + "equivalent helper-backed keywords should emit identical Rust" + ); Ok(()) } diff --git a/workspaces/docs-site/docs/release_notes/0_3.md b/workspaces/docs-site/docs/release_notes/0_3.md index 243bc529b..1a437723b 100644 --- a/workspaces/docs-site/docs/release_notes/0_3.md +++ b/workspaces/docs-site/docs/release_notes/0_3.md @@ -16,15 +16,6 @@ The main direction is not "more syntax for its own sake." `0.3` moves common pro - **Tooling**: `incan test`, `incan fmt`, `incan lock`, lifecycle commands, doctor diagnostics, checked API metadata, LSP metadata, and generated Rust audits are more deterministic and CI-friendly. - **Architecture**: More behavior is registry- and metadata-driven, and generated Rust relies less on scattered special cases. -## Read the details - -Use these entry points when a feature group needs more than release-note depth. - -- **Language**: [Numeric semantics](../language/reference/numeric_semantics.md), [Choosing numeric types](../language/how-to/choosing_numeric_types.md), [Control Flow](../language/explanation/control_flow.md), [Union types](../language/reference/union_types.md), [Enums](../language/explanation/enums.md), [Traits as language hooks](../language/explanation/traits_as_language_hooks.md), [Derives and traits](../language/reference/derives_and_traits.md), and [Callable objects](../language/reference/stdlib_traits/callable.md). -- **Stdlib**: [Choosing collection types](../language/how-to/choosing_collections.md), [`std.collections`](../language/reference/stdlib/collections.md), [Why `OrdinalMap` exists](../language/explanation/ordinal_map.md), [`std.graph`](../language/reference/stdlib/graph.md), [Working with graphs](../language/how-to/working_with_graphs.md), [`std.json`](../language/reference/stdlib/json.md), [Dynamic JSON](../language/how-to/dynamic_json.md), [`std.regex`](../language/reference/stdlib/regex.md), [Regular expressions](../language/how-to/regular_expressions.md), [`std.datetime`](../language/reference/stdlib/datetime.md), [Dates and times](../language/how-to/dates_and_times.md), [`std.logging`](../language/reference/stdlib/logging.md), [Logging](../language/how-to/logging.md), [`std.encoding`](../language/reference/stdlib/encoding.md), [Binary-text encoding](../language/how-to/binary_text_encoding.md), [`std.hash`](../language/reference/stdlib/hash.md), [`std.compression`](../language/reference/stdlib/compression.md), and [Compression](../language/how-to/compression.md). -- **Rust interop**: [Rust interop](../language/how-to/rust_interop.md), [Understanding Rust types](../language/how-to/rust_types_for_python_devs.md), [Rust-shaped confidence](../language/explanation/rust_shaped_confidence.md), [Derives and traits](../language/explanation/derives_and_traits.md), and [`std.traits`](../language/reference/stdlib/traits.md). -- **Tooling**: [Formatting with `incan fmt`](../tooling/how-to/formatting.md), [Tooling: Testing](../tooling/how-to/testing.md), [Project lifecycle](../language/how-to/project_lifecycle.md), [Project configuration](../tooling/reference/project_configuration.md), and [Checked API metadata](../tooling/reference/checked_api_metadata.md). - ## Migrating from 0.2 Most ordinary `0.2` programs should continue to compile. The changes below are the ones most likely to show up during adoption. @@ -35,46 +26,166 @@ Most ordinary `0.2` programs should continue to compile. The changes below are t 4. **Lockfiles are less noisy.** `incan.lock` no longer records generation timestamps, and routine `build` / `test` runs warn and reuse stale lock payloads instead of rewriting committed lockfiles. Run `incan lock` when you intentionally refresh the lock. 5. **New features are additive.** `loop:`, `if let`, `while let`, value enums, protocol hooks, iterator adapters, and `Result` combinators do not require rewriting existing `match`, `while True`, helper-function, or nested-`match` code. -## Features and Enhancements - -- **Language**: Numeric annotations now cover exact integer widths, pointer-sized integers, `f32` / `f64`, analytics aliases, fixed-scale `decimal[p, s]` / `numeric[p, s]`, lossless widening, explicit resize helpers, and Rust boundary adaptation ([RFC 009], #325). -- **Language**: Control flow gained `loop:` with `break `, single-pattern `if let` / `while let`, pattern alternation, anonymous union annotations with narrowing, and value enums with raw `str` / `int` representations ([RFC 016], [RFC 049], [RFC 071], [RFC 029], [RFC 032], #327, #333, #387, #317). -- **Language**: Enums can own methods and adopt traits; models, classes, traits, and wrappers gained computed properties, protocol hooks, operator hooks, typed decorators, generic decorator factories, declaration aliases, RHS partial callable presets, variadic parameters, call unpacking, and generator values ([RFC 050], [RFC 046], [RFC 068], [RFC 028], [RFC 036], [RFC 083], [RFC 084], [RFC 038], [RFC 006], #334, #203, #86, #162, #170, #437, #453, #83, #324, #640). -- **Rust interop**: Newtypes and rusttypes can adopt Rust traits with Incan source syntax, disambiguate same-name methods with `for Trait`, declare associated types, forward supported `@rust.derive(...)` metadata, and preserve inspected Rust signatures through generated calls ([RFC 043], [RFC 041], #200, #175). -- **Stdlib**: `std.collections` adds specialized containers, and `std.collections.OrdinalMap` adds deterministic immutable key-to-ordinal lookup for schemas, catalogs, dictionary-encoded domains, and reproducible serialized lookup tables ([RFC 030], [RFC 101]). -- **Stdlib**: `std.graph`, `std.json.JsonValue`, `std.regex`, `std.datetime`, and `std.logging` add first-party surfaces for dependency graphs, dynamic JSON payloads, safe-default regular expressions, temporal values, and structured logging ([RFC 047], [RFC 051], [RFC 059], [RFC 058], [RFC 072]). -- **Stdlib**: `std.encoding`, `std.hash`, `std.compression`, `std.fs`, `std.io`, `std.tempfile`, and `std.uuid` cover binary-text transforms, byte/file/reader hashing, codec-based compression, path-centric filesystem work, in-memory byte streams, temporary resources, and UUID parsing/formatting/generation ([RFC 064], [RFC 065], [RFC 061], [RFC 055], [RFC 056], [RFC 010], [RFC 060]). -- **Stdlib**: Iterator values gained lazy adapters and terminal consumers, `Result[T, E]` gained Rust-shaped `map`, `map_err`, `and_then`, `or_else`, `inspect`, and `inspect_err`, and `list.repeat(value, count)` provides import-free fixed-length list initialization ([RFC 088], [RFC 070], [RFC 069], #127, #386, #385). -- **Testing**: The `assert` statement is a language primitive, inline `module tests:` blocks are discovered by `incan test`, the runner now supports fixtures, parametrization, markers, resource-aware parallelism, JSON Lines, JUnit XML, durations, shuffling, `--nocapture`, built-in temp/env fixtures, and async fixtures ([RFC 018], [RFC 019], [RFC 004], #76). -- **Async**: `Awaitable[T]`, expression-position `race for`, `std.async.race`, channel reservation APIs, timeout-join helpers, cancellation-safe barrier behavior, and diagnostics for un-awaited async calls tighten the async surface ([RFC 039], #173, #415, #416, #417, #418, #146). -- **Tooling**: `incan fmt` now follows the vertical-spacing contract and wraps more long calls/signatures; `incan build`, `incan run`, and `incan test` support offline/locked/frozen policy; `incan tools doctor` reports offline readiness and editor binary health; lifecycle commands cover `incan new`, `incan init`, `incan version`, and `incan env` ([RFC 053], [RFC 020], [RFC 015], #73, #460, #426). -- **Tooling**: Checked contract metadata now flows through model bundles, project materialization, `.incnlib` artifacts, `incan tools metadata model`, `incan tools metadata api`, generated API docs, and LSP hover/command surfaces ([RFC 048], #205, #438). +## Feature guide + +Use this section as the map. The release note names each larger feature, says what it is for, and links to the docs that carry the real detail. + +### Language features + +- **Numeric types and fixed-scale decimals**: Use exact widths and schema-shaped names when a boundary needs them, while keeping `int` and `float` ergonomic for ordinary code. Start with [Choosing numeric types](../language/how-to/choosing_numeric_types.md), then [Numeric semantics](../language/reference/numeric_semantics.md) ([RFC 009], #325). +- **Loop expressions**: Use `loop:` plus `break ` for search, retry, and accumulator-free loops that produce a value. Read [Control Flow](../language/explanation/control_flow.md) ([RFC 016], #327, #387). +- **Pattern control flow**: Use `if let` and `while let` when one successful pattern should run and the miss case should do nothing. Read [Control Flow](../language/explanation/control_flow.md) ([RFC 049], #333). +- **Union narrowing and pattern alternation**: Model inputs that can take several shapes, then narrow them with checked patterns instead of hand-written tag logic. Read [Union types](../language/reference/union_types.md) ([RFC 071], [RFC 029]). +- **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). +- **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). +- **Generators and lazy iterators**: Build pipelines with generator values, lazy adapters, and explicit terminal consumers such as `collect`, `count`, `find`, and `fold`. Read [Generators](../language/how-to/generators.md) and [Generator semantics](../language/explanation/generators.md) ([RFC 006], [RFC 088], #127, #324, #386). + +### Rust interop and API metadata + +- **Rust trait adoption from Incan source**: Newtypes and rusttypes can adopt Rust traits with `with Trait`, method-level `for Trait`, and associated type declarations. Read [Rust interop](../language/how-to/rust_interop.md), [Rust types for Python developers](../language/how-to/rust_types_for_python_devs.md), and [`std.traits`](../language/reference/stdlib/traits.md) ([RFC 043], #200). +- **Derived and inspected Rust metadata**: Supported `@rust.derive(...)`, associated types, inspected Rust signatures, and metadata-backed call boundaries now survive further through generated calls. Read [Derives and traits](../language/explanation/derives_and_traits.md) and [Rust-shaped confidence](../language/explanation/rust_shaped_confidence.md) ([RFC 041], #175). +- **Checked public API metadata**: Public declarations, aliases, partials, models, enum variants, and selected decorator metadata can be emitted for tools and downstream consumers. Read [Checked API metadata](../tooling/reference/checked_api_metadata.md) and [LSP protocol support](../tooling/reference/lsp_protocol_support.md) ([RFC 048], #205, #438). + +### Standard Library + +- **[`std.async`](../language/reference/stdlib/async.md)**: Awaitable races, channel reservation, timeout joins, cancellation-safe barriers, and un-awaited-call diagnostics move async workflows into documented stdlib APIs ([RFC 039], #173, #415, #416, #417, #418, #146). +- **[`std.collections`](../language/reference/stdlib/collections.md)**: Ordered, sorted, counter, queue, stack, multimap, bidict, and `list.repeat(value, count)` workflows have first-party containers and helpers; see also [Choosing collection types](../language/how-to/choosing_collections.md) ([RFC 030], [RFC 069], #385). +- **[`std.collections.OrdinalMap`](../language/explanation/ordinal_map.md)**: Deterministic immutable key-to-ordinal lookup supports schemas, catalogs, dictionary-encoded domains, and reproducible serialized lookup tables ([RFC 101]). +- **[`std.compression`](../language/reference/stdlib/compression.md)**: Gzip, zlib, deflate, bzip2, lzma, zstd, and Snappy-oriented byte/stream workflows are codec-explicit; see [Compression](../language/how-to/compression.md) ([RFC 061]). +- **[`std.datetime`](../language/reference/stdlib/datetime.md)**: Dates, times, datetimes, durations, parsing, formatting, clocks, and timezone offsets share one temporal vocabulary; see [Dates and times](../language/how-to/dates_and_times.md) ([RFC 058]). +- **[`std.encoding`](../language/reference/stdlib/encoding.md)**: Base64, hex, URL, and related byte/text transforms are strict and named; see [Binary-text encoding](../language/how-to/binary_text_encoding.md) ([RFC 064]). +- **[`std.fs`](../language/reference/stdlib/fs.md)**: Path-centric filesystem work covers paths, metadata, directory traversal, and file operations; see [File I/O](../language/how-to/file_io.md) ([RFC 055]). +- **[`std.graph`](../language/reference/stdlib/graph.md)**: Directed graph, DAG, traversal, dependency ordering, path query, and cycle-aware workflows are available without ad hoc containers; see [Working with graphs](../language/how-to/working_with_graphs.md) and [Graph model](../language/explanation/graph_model.md) ([RFC 047]). +- **[`std.hash`](../language/reference/stdlib/hash.md)**: Byte, file, and reader hashing use algorithm-specific helpers with normalized digest output; see [Hashing data](../language/how-to/hashing_data.md) ([RFC 065]). +- **[`std.io`](../language/reference/stdlib/io.md)**: In-memory byte streams and buffered readers cover byte-oriented I/O without direct Rust interop ([RFC 056]). +- **[`std.json`](../language/reference/stdlib/json.md)**: `JsonValue` supports dynamic payload construction, inspection, conversion, and extraction at API boundaries; see [Dynamic JSON](../language/how-to/dynamic_json.md) ([RFC 051]). +- **[`std.logging`](../language/reference/stdlib/logging.md)**: Structured logging gives modules stable logger names, levels, fields, and runtime-friendly generated Rust output; see [Logging](../language/how-to/logging.md) ([RFC 072]). +- **[`std.regex`](../language/reference/stdlib/regex.md)**: Safe-default regular expressions cover matching, captures, iteration, splitting, and replacement without backtracking-only features; see [Regular expressions](../language/how-to/regular_expressions.md) ([RFC 059]). +- **[`std.result`](../language/reference/stdlib/result.md)**: `Result[T, E]` gained Rust-shaped `map`, `map_err`, `and_then`, `or_else`, `inspect`, and `inspect_err` helpers for fallible pipelines ([RFC 070], #386). +- **[`std.tempfile`](../language/reference/stdlib/tempfile.md)**: Scoped temporary files and directories are first-party test and application resources ([RFC 010]). +- **[`std.testing`](../language/reference/stdlib/testing.md)**: Fixtures, parametrization, markers, temp/env fixtures, async fixtures, and assertion helpers back the `incan test` workflow; see [Testing in Incan](../language/how-to/testing_stdlib.md) ([RFC 018], [RFC 019], [RFC 004], #76). +- **[`std.uuid`](../language/reference/stdlib/uuid.md)**: UUID parsing, formatting, generation, version inspection, and byte/string conversion are available as source-defined helpers; see [Working with UUIDs](../language/how-to/working_with_uuids.md) ([RFC 060]). + +### Tooling + +- **`incan test`**: Inline `module tests:` blocks are discovered by the runner, with fixtures, parametrization, markers, resource-aware parallelism, JSON Lines, JUnit XML, durations, shuffling, and `--nocapture`; read [Tooling: Testing](../tooling/how-to/testing.md) and [Testing in Incan](../language/how-to/testing_stdlib.md) ([RFC 018], [RFC 019], [RFC 004], #76). +- **`incan fmt`**: Formatting follows the vertical-spacing contract and wraps more long calls and signatures; read [Formatting with `incan fmt`](../tooling/how-to/formatting.md) and the [Code Style Guide](../language/reference/code_style.md) ([RFC 053], #73). +- **Cargo policy and lockfiles**: `incan build`, `incan run`, and `incan test` propagate offline, locked, and frozen policy while `incan lock` owns intentional lock refreshes; read [Project configuration](../tooling/reference/project_configuration.md) ([RFC 020], #460). +- **Lifecycle and diagnostics**: `incan new`, `incan init`, `incan version`, `incan env`, and `incan tools doctor` cover project startup, environment inspection, offline readiness, and editor binary health; read [Project lifecycle](../language/how-to/project_lifecycle.md) ([RFC 015], #426). ## Bugfixes and Hardening -- **Release stabilization**: Validation against InQL and release smoke tests fixed formatter/parser drift for tuple-unpack comprehensions and f-string debug markers, generated-Rust ownership/coercion failures for loop-item fields and union call arguments, public alias reexports across library boundaries, union model variant construction/clone/alias equivalence, static list index assignment, collection f-string formatting, `std.regex` Rust-boundary text borrowing, Rust bridge type identity for inspected methods whose Rust signatures retain unknown generic or lifetime placeholders, test-runner source-module ordering for public aliases of imported items, `?` propagation in comprehension bodies, decorated-function source signatures in checked API metadata, imported/decorator `const str` argument materialization, generic decorator factory inference across package-style module boundaries, typed failure lowering for `assert false` in non-`None` return paths, method-call decorator factories on class/static registry receivers, const model metadata constructors, lowercase exported static imports, generated script/test Cargo manifests that omit unreachable package-level Rust dependencies, keyword-named public symbols, imported static decorator string arguments, storage-rooted method calls used as match scrutinees, and directory inline-test batching that preserves each file's parser and import scope (#615, #616, #617, #620, #621, #622, #624, #625, #627, #630, #631, #633, #636, #638, #640, #644, #645, #658, #659, #665, #669, #671, #674, #676). -- **Compiler**: Ownership and Rust-boundary coercion now route through shared argument-use and generated-use planning instead of parallel caller/emitter heuristics, which makes borrowed `str`/bytes calls, backend-inserted `Clone` bounds, method fallback borrowing, and retained generated imports follow the same metadata-backed decisions across typechecking, lowering, and emission. Remaining semantic string comparisons in high-risk compiler paths are now fingerprinted by a guardrail so new string-based behavior has to be centralized or explicitly classified. -- **Compiler**: Duckborrowing and generated-Rust ownership planning now cover more typed use sites, including arguments, returns, assignments, match scrutinees, collection elements, tuple elements, lookups, comprehensions, mutable aggregate parameters, Rust interop calls, and backend-inserted `Clone` bounds. This removes many user-authored `.clone()`, `.as_ref()`, `str(...)`, and `.into()` workarounds (#121, #241, #364, #366, #367, #372, #383, #391, #602). -- **Compiler**: Multi-file and cross-module codegen is stricter: imported defaults expand with qualification, same-shaped union wrappers are shared through the crate root, wide union narrowing fully lowers, keyword-named modules escape consistently, public submodule reexports work under `src/`, and private route handlers/models are retained for web registration (#395, #457, #461, #458, #122, #287, #117). -- **Compiler**: Rust interop fixes cover retained enum-pattern imports, owned Incan values passed to shared borrowed generic Rust parameters, `Vec` adaptation from `list[T]`, prost-style inherent and trait-provided `decode(buf: T)` calls, extension-trait import retention from metadata, Rust bridge type identity across re-exported and placeholder-generic argument displays, and trait-typed local annotation diagnostics (#459, #506, #128, #609, #612, #447, #630, #462). -- **Compiler**: Typechecking and lowering now preserve more generic information, including `Self` substitution on instantiated generic receivers, generic model/class field access, generic instance methods, list literals containing `Self`, trait/supertrait upcasts, imported prost oneof payloads, explicit call-site generic cycles, and locals initialized from static factory calls (#237, #231, #253, #230, #184, #218, #279, #252, #255). -- **Compiler**: Runtime and generated-manifest hardening routes collection/JSON extraction and decorator misuse stubs through named helpers, keeps Tokio and `serde_json` behind feature gates, prunes unused generated Rust without broad `allow` attributes, and improves runtime diagnostics for f-string unknown symbols and collection/string conversion failures (#351, #157, #214, #71, #81). -- **Tooling**: `incan test` reuses more generated harness state, isolates single-file runs, keeps project cwd stable, includes generated helper modules such as `std.result` when test files use helper-backed surfaces, and treats project manifests as one lock surface across scripts and test harness inputs (#268, #269, #271, #288, #378, #610, #505). -- **Tooling**: Formatter fixes preserve escaped f-string newlines, numeric literal spelling, qualified enum/constructor patterns, `mut` markers, block blank-line intent, docstrings, multiline trailing commas, long logical-expression wrapping, and parseable class trait-adoption wrapping (#235, #250, #264, #289, #247, #394, #484, #565). -- **Docs**: User-facing docs were reorganized around reference/how-to/explanation boundaries for stdlib modules including graph, regex, logging, hash, UUID, tempfile, collections, encoding, compression, and datetime. Contributor docs now describe crate boundaries, ownership metadata, staged Rust inspection, and the quarantined `std.web` host-runtime bridge (#284). -- **Dependencies**: Release hardening removes the `atomic-polyfill` advisory path through the local rust-analyzer proc-macro API patch, updates Wasmtime/WASI and MSRV together, remediates Dependabot alerts across docs-site Python pins, VS Code lockfiles, Rust lock entries, and GitHub Actions, and bumps `pymdown-extensions` to `10.21.3` for `GHSA-62q4-447f-wv8h` (#260, #475, #464). +This section is grouped by outcome rather than by every minimized repro. Issue numbers are kept for traceability when you need the exact bug report. + +### Compiler Correctness + +- **Argument planning is shared**: Ownership and Rust-boundary coercion now route through one argument-use plan instead of parallel caller/emitter heuristics. +- **Duckborrowing covers more real use sites**: Generated Rust handles arguments, returns, assignments, match scrutinees, aggregate elements, lookups, comprehensions, mutable aggregate parameters, Rust calls, and generated `Clone` bounds with fewer user-authored workarounds (#121, #241, #364, #366, #367, #372, #383, #391, #602). +- **Release smoke paths are less fragile**: InQL and release smoke testing fixed loop-item fields, union call arguments, storage-rooted match scrutinees, static list index assignment, typed `assert false` lowering, and const model metadata constructors (#620, #621, #622, #627, #630, #644, #671, #674). +- **Generic and trait flow keeps more type information**: Instantiated receivers, generic fields, generic methods, `list[Self]`, trait/supertrait upcasts, imported prost oneofs, explicit generic cycles, and static factory locals now survive typechecking and lowering more consistently (#237, #231, #253, #230, #184, #218, #279, #252, #255). +- **Runtime-boundary errors are clearer**: `std.regex` text borrowing, collection f-string formatting, and collection/string conversion diagnostics fail closer to the Incan source instead of surfacing as obscure Rust/runtime errors (#624, #625, #71, #81). +- **Stringly compiler behavior is guarded**: Remaining semantic string comparisons in high-risk compiler paths are fingerprinted so new string-based behavior has to be centralized or explicitly classified. + +### Rust Interop And Generated Rust + +- **Borrowing decisions are metadata-backed**: Borrowed `str`/bytes calls, method fallback borrowing, and retained generated imports now follow the same decisions across typechecking, lowering, and emission. +- **Rust bridge identity is preserved**: Inspected methods with unknown generic or lifetime placeholders and re-exported Rust argument displays keep stable bridge identity (#645, #630). +- **Collection adaptation is less manual**: Owned Incan values can flow to shared borrowed generic Rust parameters, and `list[T]` can adapt to `Vec` where metadata proves the boundary (#506, #128). +- **Protobuf-style APIs need fewer workarounds**: Prost-style inherent and trait-provided `decode(buf: T)` calls lower correctly (#609, #612). +- **Generated Rust pruning is safer**: Enum-pattern imports and metadata-derived extension-trait imports survive pruning, while unused generated Rust is pruned without broad `allow` attributes (#459, #447, #214). +- **Generated manifests stay smaller**: Tokio and `serde_json` stay behind feature gates, and generated helper stubs use named helpers (#351, #157). +- **Trait annotation failures are Incan diagnostics**: Trait-typed local annotations now produce diagnostics instead of obscure lowering or generated-Rust failures (#462). + +### Multi-File And Packages + +- **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). +- **Decorator metadata crosses package boundaries**: Source signatures, imported/decorator `const str` arguments, generic decorator factories, and method-call decorator factories are represented in checked metadata more reliably (#636, #638, #640, #669). +- **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 + +- **Formatter output preserves meaning**: Tuple-unpack comprehensions, f-string debug markers, escaped f-string newlines, numeric spelling, qualified enum/constructor patterns, `mut`, docstrings, trailing commas, logical-expression wrapping, and class trait-adoption wrapping now round-trip more safely (#615, #616, #235, #250, #264, #289, #247, #394, #484, #565). +- **Comprehension behavior is more complete**: `?` propagation works inside comprehensions, and collection/string conversion diagnostics are clearer when runtime coercion fails. +- **Inline tests keep file-local scope**: Directory inline-test runs preserve each file's parser and import scope, conventional test batches split on imported-name collisions, and decorated functions named like builtins resolve to the source binding inside inline tests (#676, #677). +- **The test harness reuses more correctly**: `incan test` reuses generated harness state, isolates single-file runs, keeps project cwd stable, and includes helper modules such as `std.result` when test files use helper-backed surfaces (#268, #269, #271, #288, #378, #610, #505). + +### Docs And Dependencies + +- **User docs are closer to Divio shape**: Stdlib pages for graph, regex, logging, hash, UUID, tempfile, collections, encoding, compression, datetime, and related modules now separate reference contracts from how-to or explanation material (#284). +- **Contributor docs name the important boundaries**: Crate boundaries, ownership metadata, staged Rust inspection, and the quarantined `std.web` host-runtime bridge are documented for maintainers (#284). +- **Dependency alerts are closed out**: The `atomic-polyfill` advisory path is removed, Wasmtime/WASI and MSRV move together, Dependabot alerts are remediated across docs-site Python pins, VS Code lockfiles, Rust lock entries, and GitHub Actions, and `pymdown-extensions` is pinned to `10.21.3` for `GHSA-62q4-447f-wv8h` (#260, #475, #464). ## Known limitations - Decimal arithmetic is not yet general language behavior. The `0.3` decimal surface covers typed annotations, literal validation, formatting, generated Rust representation, and display; arithmetic semantics need a follow-up language/library decision. - `incan fmt` is still conservative. RFC 053 gives vertical spacing and common wrapping rules, but it is not a general pretty-printer overhaul for every nested expression shape. -- `std.regex` is a safe-default regular-expression surface, not a Python/PCRE compatibility layer. Lookaround, pattern backreferences, and other backtracking-only features belong in a separate package or future stdlib track if standardized. +- `std.regex` is a safe-default regular-expression surface, not a Python/PCRE compatibility layer. Lookaround, pattern backreferences, and other backtracking-only features are tracked separately by [RFC 100] for a future `std.re` surface. - Native Windows filesystem behavior is not part of the `0.3` contract. `std.fs` documents Unix-like host behavior until the stdlib has an explicit platform split. ## RFCs implemented -- **Language and compiler**: [RFC 004], [RFC 006], [RFC 009], [RFC 016], [RFC 017], [RFC 018], [RFC 024], [RFC 025], [RFC 028], [RFC 029], [RFC 032], [RFC 036], [RFC 038], [RFC 039], [RFC 043], [RFC 044], [RFC 046], [RFC 049], [RFC 050], [RFC 053], [RFC 057], [RFC 068], [RFC 069], [RFC 070], [RFC 071], [RFC 083], [RFC 084], [RFC 088]. -- **Stdlib**: [RFC 010], [RFC 030], [RFC 047], [RFC 051], [RFC 055], [RFC 056], [RFC 058], [RFC 059], [RFC 060], [RFC 061], [RFC 064], [RFC 065], [RFC 072], [RFC 101]. -- **Tooling and metadata**: [RFC 015], [RFC 019], [RFC 020], [RFC 040], [RFC 045], [RFC 048]. +### Language and compiler + +- [RFC 004]: async fixtures +- [RFC 006]: Python-style generators +- [RFC 009]: numeric type system and builtin type registry +- [RFC 016]: `loop` and `break ` loop expressions +- [RFC 017]: validated newtypes with implicit coercion +- [RFC 018]: language primitives for testing +- [RFC 024]: extensible derive protocol +- [RFC 025]: multi-instantiation trait dispatch +- [RFC 028]: trait-based operator overloading +- [RFC 029]: union types and type narrowing +- [RFC 032]: value enums with `str` and `int` backing values +- [RFC 036]: user-defined decorators +- [RFC 038]: variadic args and unpacking +- [RFC 039]: `race` for awaitable concurrency +- [RFC 043]: Rust trait implementation from Incan +- [RFC 044]: open-ended trait methods +- [RFC 046]: computed properties +- [RFC 049]: `if let` and `while let` pattern control flow +- [RFC 050]: enum methods and enum trait adoption +- [RFC 053]: formatter vertical spacing buckets +- [RFC 057]: targeted Rust lint suppression for generated code +- [RFC 068]: protocol hooks for core language syntax +- [RFC 069]: `list.repeat` helper for fixed-length initialization +- [RFC 070]: result combinators for `Result[T, E]` +- [RFC 071]: pattern alternation in `match` and `if let` +- [RFC 083]: symbol and method aliases +- [RFC 084]: RHS partial callable presets +- [RFC 088]: iterator adapter surface + +### Standard library + +- [RFC 010]: Python-style `tempfile` standard library +- [RFC 030]: `std.collections` extended collection types +- [RFC 047]: lightweight directed graph types +- [RFC 051]: `JsonValue` for `std.json` +- [RFC 055]: `std.fs` filesystem APIs +- [RFC 056]: `std.io` byte streams and binary parsing helpers +- [RFC 058]: `std.datetime` temporal values, intervals, and runtime timing +- [RFC 059]: `std.regex` regular expressions, matches, captures, and replacement +- [RFC 060]: `std.uuid` parsing, generation, and formatting +- [RFC 061]: `std.compression` codec-based compression and decompression +- [RFC 064]: `std.encoding` binary-text encoding and decoding utilities +- [RFC 065]: `std.hash` stable hashing primitives +- [RFC 072]: `std.logging` logger acquisition, configuration, and structured events +- [RFC 101]: `std.collections.OrdinalMap` deterministic key-to-ordinal lookup + +### Tooling and metadata + +- [RFC 015]: hatch-like project lifecycle tooling +- [RFC 019]: test runner, CLI, and ecosystem +- [RFC 020]: offline, locked, and reproducible builds +- [RFC 040]: scoped DSL surface forms +- [RFC 045]: scoped DSL symbol surfaces +- [RFC 048]: checked contract metadata, Incan emit, and interrogation tooling --8<-- "_snippets/rfcs_refs.md" diff --git a/workspaces/docs-site/docs/roadmap.md b/workspaces/docs-site/docs/roadmap.md index ea52040e5..866cc1e0e 100644 --- a/workspaces/docs-site/docs/roadmap.md +++ b/workspaces/docs-site/docs/roadmap.md @@ -134,7 +134,7 @@ The 1.0 milestone consolidates the post-cutover compiler architecture, ABI/packa ## Status by Area - Core language: see [RFC 000] / [RFC 008]. -- Testing surface: see [RFC 001] / [RFC 002] / [RFC 004] / [RFC 007]. +- Testing surface: see [RFC 018] / [RFC 019] / [RFC 004]. - Tooling and first-contact: install, starter, diagnostics, explain, codegraph, artifact inspection, and build reports are the immediate release surface. - Rust interop: see [RFC 005] / [RFC 013] and the [Rust Interop guide](language/how-to/rust_interop.md). Rust-hosted consumption should be reframed through ABI and Cargo-native package direction instead of generated Rust as the public semantic path. - Web and interactive runtime: see the [Web Framework guide](language/tutorials/web_framework.md), [RFC 092](RFCs/092_interactive_runtime_stdlib_contracts.md), and related runtime/DSL RFCs. @@ -144,12 +144,9 @@ The 1.0 milestone consolidates the post-cutover compiler architecture, ABI/packa The following items remain intentionally deferred until they have a focused RFC or implementation lane: -- SSR/SSG for frontend: Server-Side Rendering / Static Site Generation for the WASM/UI stack (render pages ahead of time - or on the server, then hydrate). -- Desktop/mobile via wgpu: using the wgpu graphics stack to run Incan apps as native desktop/mobile apps (instead of - browser-only). -- CRDT/collab features: real-time collaboration primitives (Conflict-free Replicated Data Types) for things like - collaborative editing, shared state, etc. +- SSR/SSG for frontend: Server-Side Rendering / Static Site Generation for the WASM/UI stack (render pages ahead of time or on the server, then hydrate). +- Desktop/mobile via wgpu: using the wgpu graphics stack to run Incan apps as native desktop/mobile apps instead of browser-only. +- CRDT/collab features: real-time collaboration primitives (Conflict-free Replicated Data Types) for collaborative editing, shared state, and similar workflows. ### Guides