Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 8 additions & 32 deletions src/typechecker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
53 changes: 53 additions & 0 deletions tests/github_issues_batch_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <call>` 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;
Expand Down Expand Up @@ -333,3 +336,53 @@ fn unknown_param_accepted_by_split() {
);
assert_eq!(code, Some(0), "program should exit 0: {out}");
}

// ---------------------------------------------------------------------------
// #588 — `store x as <Unknown-returning call>` 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}");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading