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
28 changes: 19 additions & 9 deletions src/typechecker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,22 @@ impl TypeChecker {
.collect::<Vec<Type>>();

// WFL has no return-type annotation syntax, so `return_type` is
// effectively always `None`; default to `Nothing` provisionally
// effectively always `None`; seed a provisional return type
// (so recursive calls in the body resolve to a function type),
// then refine it below by inferring from the body's `return`
// expressions (issue #569). Without this, every action-call
// result is typed `Nothing`, producing spurious "Expected Text
// but found Nothing" errors when the result feeds a builtin
// position that requires Text (e.g. `respond to req with ...`).
let return_type_value = return_type.as_ref().cloned().unwrap_or(Type::Nothing);
// expressions (issue #569).
//
// Seed with `Unknown` rather than `Nothing`. The body is
// type-checked before the real return type is inferred (#575's
// ordering), so a self-recursive call in the body resolves
// against this provisional type. `Nothing` made such a call — and
// any indexing/use of its result — raise strict "found Nothing"
// errors (issue #590). After #588/#589 an `Unknown`-typed value
// degrades gracefully, so `Unknown` resolves self-references
// cleanly while still avoiding the spurious "Expected Text but
// found Nothing" errors that motivated seeding a concrete type
// in builtin positions (e.g. `respond to req with ...`, #569).
let return_type_value = return_type.as_ref().cloned().unwrap_or(Type::Unknown);

if let Some(symbol) = self.analyzer.get_symbol_mut(name) {
symbol.symbol_type = Some(Type::Function {
Expand Down Expand Up @@ -779,10 +787,12 @@ impl TypeChecker {
self.analyzer.pop_scope();

// Update the action's symbol so call sites see the real result
// type instead of the provisional `Nothing`. Skip pure `Nothing`
// results (void actions) to preserve existing behavior.
// type instead of the provisional `Unknown` seed. Pure `Nothing`
// results (void actions) are recorded as `Nothing` — the seed is
// only `Unknown` so self-references resolve gracefully during the
// body check (#590); external callers still see the same `Nothing`
// return type void actions had before.
if let Some(inferred) = inferred_return
&& inferred != Type::Nothing
&& let Some(symbol) = self.analyzer.get_symbol_mut(name)
{
symbol.symbol_type = Some(Type::Function {
Expand Down
85 changes: 85 additions & 0 deletions tests/recursive_action_return_type_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Regression tests for issue #590: a self-recursive action's result must not be
//! typed `Nothing` inside its own body.
//!
//! When an action calls itself and uses the recursive result inside its own body
//! (e.g. indexes it), the type checker used to seed the action's provisional
//! return type as `Nothing`. The body is type-checked *before* the real return
//! type is inferred (#575's ordering), so the self-reference resolved to
//! `Nothing`, producing a false `Cannot index into Nothing` diagnostic. Seeding
//! the provisional type as `Unknown` (which degrades gracefully after #589) fixes
//! this while post-body inference still records the concrete return type.

use wfl::lexer::lex_wfl_with_positions;
use wfl::parser::Parser;
use wfl::typechecker::TypeChecker;

/// Type-check `code` and assert it produces zero diagnostics. Asserting a fully
/// clean result (rather than only the absence of one error substring) is a
/// tighter regression guard: it catches both a re-introduced "Cannot index into
/// Nothing" error and any new spurious diagnostic on the same recursive path.
fn assert_typechecks_clean(code: &str) {
let tokens = lex_wfl_with_positions(code);
let mut parser = Parser::new(&tokens);
let program = parser.parse().expect("Should parse");

let mut type_checker = TypeChecker::new();
let result = type_checker.check_types(&program);

assert!(
result.is_ok(),
"Self-recursive action should type-check clean; got: {:?}",
result.err()
);
}

/// Issue #590: a self-recursive call whose result is indexed directly inside the
/// body must not raise a false "Cannot index into Nothing" diagnostic.
#[test]
fn test_self_recursive_action_result_not_typed_nothing() {
assert_typechecks_clean(
r#"
define action called other with parameters n:
create map m:
"val" is n
end map
return m
end action

define action called p_unary with parameters n:
check if n is greater than 0:
store r as p_unary of (n minus 1)
return other of (r["val"])
end check
return other of n
end action

display (p_unary of 3)["val"]
"#,
);
}

/// A self-recursive action that negates its recursive result before indexing
/// (the Scribe `scribe_p_unary` shape) must also type-check clean.
#[test]
fn test_self_recursive_action_negating_result_typechecks_clean() {
assert_typechecks_clean(
r#"
define action called other with parameters n:
create map m:
"val" is n
end map
return m
end action

define action called p_unary with parameters n:
check if n is greater than 0:
store r as p_unary of (n minus 1)
return other of (0 minus (r["val"]))
end check
return other of n
end action

display (p_unary of 3)["val"]
"#,
);
}
Loading