diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index b115cabc..50024f5a 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -613,38 +613,14 @@ impl TypeChecker { return; } - // Check if this is a container property assignment within a method - // In this case, we might know the property type from the container definition - let mut is_container_property_assignment = false; - if inferred_type == Type::Unknown { - // Check if we're in a container method and this is a property assignment - if let Some(ref container_name) = self.current_container - && let Some(container_info) = self.analyzer.get_container(container_name) - && container_info.properties.contains_key(name) - { - // This is a container property assignment - is_container_property_assignment = true; - } - - // Also check if the analyzer has this symbol (fallback) - if !is_container_property_assignment - && let Some(symbol) = self.analyzer.get_symbol(name) - && symbol.symbol_type.is_some() - { - // Variable already exists with a known type - is_container_property_assignment = true; - } - } - - if inferred_type == Type::Unknown && !is_container_property_assignment { - self.type_error( - format!("Could not infer type for variable '{name}'"), - None, - None, - *_line, - *_column, - ); - } + // Under gradual typing, an inferred `Unknown` means "statically + // unknown", not "known incompatible": e.g. `store x as helper of ...` + // where `helper` returns an expression built from its untyped + // parameters has an `Unknown` return type. Bind `x` as `Unknown` + // silently rather than raising a false `Could not infer type` + // ERROR (issue #588), mirroring #587's treatment of variable + // references. The type-compatibility and symbol-recording paths + // below still record the more specific type when one is available. let symbol_type_option = if let Some(symbol) = self.analyzer.get_symbol(name) { symbol.symbol_type.clone() diff --git a/tests/github_issues_batch_test.rs b/tests/github_issues_batch_test.rs index 917007c0..008ac72e 100644 --- a/tests/github_issues_batch_test.rs +++ b/tests/github_issues_batch_test.rs @@ -14,6 +14,9 @@ //! * #567 — `Any`/`Unknown` values (list-index results, untyped parameters) //! must be accepted by the `add`/`split`/arithmetic type-checker rules rather //! than producing false ERROR-level diagnostics (gradual typing). +//! * #588 — `store x as ` where the callee's return type is statically +//! `Unknown` must bind `x` as `Unknown` silently instead of raising a false +//! `Could not infer type for variable 'x'` ERROR (gradual typing). use std::fs; use std::process::Command; @@ -333,3 +336,53 @@ fn unknown_param_accepted_by_split() { ); assert_eq!(code, Some(0), "program should exit 0: {out}"); } + +// --------------------------------------------------------------------------- +// #588 — `store x as ` binds silently +// --------------------------------------------------------------------------- + +#[test] +fn store_unknown_call_result_binds_silently() { + // `add_one` returns `n plus 1`; `n` is an untyped parameter so the return + // type is statically `Unknown`. Binding that result with `store` must not + // raise `Could not infer type for variable 'x'` — it binds `x` as Unknown. + let (out, code) = run_src( + "define action called add_one with parameters n:\n return n plus 1\nend action\n\ + define action called use_it:\n store x as add_one of 3\n return x\nend action\n\ + display use_it\n", + ); + assert!(out.contains('4'), "program should print 4: {out}"); + assert!( + !out.contains("Could not infer type for variable"), + "binding an Unknown-typed call result must not raise a type error (#588): {out}" + ); + assert!( + !out.contains(TYPE_WARN_BANNER), + "no false type warnings expected (#588): {out}" + ); + assert_eq!(code, Some(0), "program should exit 0: {out}"); +} + +#[test] +fn store_unknown_call_result_chained_binds_silently() { + // Chained helpers (Scribe-style): each `store` binds an Unknown-typed + // result and feeds the next call. None of them should be flagged. + // + // `wrap` must genuinely return `Unknown` for this to exercise the bind + // path: its `otherwise` branch returns the untyped parameter `s` + // (statically `Unknown`), which widens the action's inferred return type + // to `Unknown` even though the taken branch produces `Text`. A plain + // `return "[" with s with "]"` would infer `Text` (concatenation is always + // `Text`) and would NOT reproduce the regression. + let (out, code) = run_src( + "define action called wrap with parameters s:\n check if length of s is greater than 0:\n return \"[\" with s with \"]\"\n otherwise:\n return s\n end check\nend action\n\ + define action called go:\n store a as wrap of \"x\"\n store b as wrap of a\n return b\nend action\n\ + display go\n", + ); + assert!(out.contains("[[x]]"), "program should print [[x]]: {out}"); + assert!( + !out.contains("Could not infer type for variable"), + "chained Unknown-typed binds must not be flagged (#588): {out}" + ); + assert_eq!(code, Some(0), "program should exit 0: {out}"); +}