From 658f1869c32dee6b7c541e1105b2a5d8a3ee7952 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 05:28:59 +0000 Subject: [PATCH 1/2] fix: bind Unknown-typed store results silently under gradual typing (#588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `store x as ` emitted an ERROR-level "Could not infer type for variable 'x'" whenever the callee's return type was statically `Unknown` — the normal case for any action that returns an expression built from its untyped parameters. The program ran correctly (exit 0); only the static diagnostic was wrong, and it was extremely noisy (104 of 106 diagnostics on a fully-correct Scribe run). Under gradual typing, an inferred `Unknown` means "statically unknown", not "known incompatible" — mirroring #587's treatment of variable references. The `VariableDeclaration` arm now binds the symbol as `Unknown` and continues instead of raising a type_error. The type-compatibility and symbol-recording paths that follow are unchanged and still record a more specific type when one is available, so the now-dead container-property/known-symbol suppression block is removed. Adds regression tests covering the minimal repro (prints 4, no error) and the chained Scribe-style helpers (prints [[x]]). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BPdEBReyK6s9FWYQjwkJvk --- src/typechecker/mod.rs | 40 ++++++--------------------- tests/github_issues_batch_test.rs | 46 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 32 deletions(-) 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..e829f532 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,46 @@ 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. + let (out, code) = run_src( + "define action called wrap with parameters s:\n return \"[\" with s with \"]\"\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}"); +} From b919dd540501a60682ec3d87f8d4db7ff5491cd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 07:30:57 +0000 Subject: [PATCH 2/2] test: make chained #588 regression test genuinely exercise Unknown bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chained `store_unknown_call_result_chained_binds_silently` test used a `wrap` helper that returned `"[" with s with "]"`. Concatenation always infers as `Text`, so `wrap`'s return type inferred to `Text` (concrete), not `Unknown` — the test passed even against pre-fix code and did not actually cover the silent-Unknown-bind regression. Rewrite `wrap` so its `otherwise` branch returns the untyped parameter `s` (statically `Unknown`), which widens the action's inferred return type to `Unknown` while the taken branch still bracket-wraps at runtime. Verified: against pre-fix code this now flags both `a` and `b` with `Could not infer type for variable`; with the fix it type-checks clean and still prints `[[x]]`. Existing assertions and the `wrap`/`go` symbols are preserved. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BPdEBReyK6s9FWYQjwkJvk --- tests/github_issues_batch_test.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/github_issues_batch_test.rs b/tests/github_issues_batch_test.rs index e829f532..008ac72e 100644 --- a/tests/github_issues_batch_test.rs +++ b/tests/github_issues_batch_test.rs @@ -367,8 +367,15 @@ fn store_unknown_call_result_binds_silently() { 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 return \"[\" with s with \"]\"\nend action\n\ + "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", );