diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index 50024f5a..4ca3172b 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -719,14 +719,22 @@ impl TypeChecker { .collect::>(); // 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 { @@ -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 { diff --git a/tests/recursive_action_return_type_test.rs b/tests/recursive_action_return_type_test.rs new file mode 100644 index 00000000..c3240c65 --- /dev/null +++ b/tests/recursive_action_return_type_test.rs @@ -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"] +"#, + ); +}