Summary
When an action calls itself and uses the recursive result inside its own body (e.g. indexes it), the type checker types that result as Nothing and raises a false Cannot index into Nothing - Expected List of Unknown but found Nothing. The program runs correctly and exits 0; only the static diagnostic is wrong.
This is the recursive residual of #560. #560's non-recursive repro is now fixed on current main (verified below) because #575 infers an action's return type after checking its body. But that ordering can't help a self-recursive call: the body is type-checked while the action's own return type is still the provisional Nothing, so the self-reference resolves to Nothing.
Environment
Minimal reproduction
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) // self-recursive call
return other of (r["val"]) // <-- error[ERROR]: Cannot index into Nothing
end check
return other of n
end action
display (p_unary of 3)["val"]
Output (the program runs, exit 0):
Type checking warnings:
error[ERROR]: Cannot index into Nothing - Expected List of Unknown but found Nothing
┌─ repro.wfl:12:...
│
12 │ return other of (r["val"])
│ ^ Type error occurred here
...
The r["val"] is flagged because r (the result of the self-recursive p_unary call) is typed Nothing.
Contrast: the non-recursive case (#560) is now fixed
define action called get_list:
return [1 and 2]
end action
store xs as call get_list
store x0 as xs[0] // #560: used to error; now clean
display x0 // prints 1, no diagnostic on 34512e8
So the remaining gap is specifically self-reference inside the recursive action's own body.
Root cause
Statement::ActionDefinition in src/typechecker/mod.rs seeds the action's return type provisionally as Nothing, then type-checks the body before inferring the real return type:
// ~line 729 — provisional seed
let return_type_value = return_type.as_ref().cloned().unwrap_or(Type::Nothing);
// ... registers the action symbol as Function { .., return_type: Nothing } ...
// ~line 760 — body checked while the seed is still Nothing
for stmt in body {
self.check_statement_types(stmt); // a self-recursive call here resolves to Nothing
}
// ~line 769 — return type inferred only afterward
let inferred_return = if return_type.is_none() {
Some(self.infer_action_return_type(body))
} else { None };
A self-recursive call in the body is resolved against the provisional Nothing, so using/indexing its result raises strict errors during the body check — before the inference at 769-790 can refine it.
Suggested fix
Seed the provisional return type as Type::Unknown instead of Type::Nothing. After #589, an Unknown-typed value degrades gracefully (indexing/using it no longer raises ERROR-level diagnostics), so a self-recursive call would resolve to Unknown and check cleanly, while the post-body inference (#575) still records the concrete return type for external callers. (A fixpoint/two-pass inference would also work but is heavier.) The comment at line 722 notes Nothing was chosen to avoid found Nothing errors in builtin positions — but seeding Unknown avoids those too and additionally fixes this recursive case.
Impact
Small and cosmetic (non-fatal), but it's the last false diagnostic emitted by the Scribe engine — scribe_p_unary is a recursive-descent helper that negates its recursive result (0 minus (r["val"])), producing 2 of these notes on an otherwise-clean run (down from 106 before #589).
Related
Summary
When an action calls itself and uses the recursive result inside its own body (e.g. indexes it), the type checker types that result as
Nothingand raises a falseCannot index into Nothing - Expected List of Unknown but found Nothing. The program runs correctly and exits 0; only the static diagnostic is wrong.This is the recursive residual of #560. #560's non-recursive repro is now fixed on current
main(verified below) because #575 infers an action's return type after checking its body. But that ordering can't help a self-recursive call: the body is type-checked while the action's own return type is still the provisionalNothing, so the self-reference resolves toNothing.Environment
main@34512e8(after fix: infer user-defined action return types in the type checker #575, Fix five GitHub issues: string coercion, parameter shadowing, operators, scope, typing #587, fix: bind Unknown-typed store results silently under gradual typing (#588) #589), Linux, release buildMinimal reproduction
Output (the program runs, exit 0):
The
r["val"]is flagged becauser(the result of the self-recursivep_unarycall) is typedNothing.Contrast: the non-recursive case (#560) is now fixed
So the remaining gap is specifically self-reference inside the recursive action's own body.
Root cause
Statement::ActionDefinitioninsrc/typechecker/mod.rsseeds the action's return type provisionally asNothing, then type-checks the body before inferring the real return type:A self-recursive call in the body is resolved against the provisional
Nothing, so using/indexing its result raises strict errors during the body check — before the inference at 769-790 can refine it.Suggested fix
Seed the provisional return type as
Type::Unknowninstead ofType::Nothing. After #589, anUnknown-typed value degrades gracefully (indexing/using it no longer raises ERROR-level diagnostics), so a self-recursive call would resolve toUnknownand check cleanly, while the post-body inference (#575) still records the concrete return type for external callers. (A fixpoint/two-pass inference would also work but is heavier.) The comment at line 722 notesNothingwas chosen to avoidfound Nothingerrors in builtin positions — but seedingUnknownavoids those too and additionally fixes this recursive case.Impact
Small and cosmetic (non-fatal), but it's the last false diagnostic emitted by the Scribe engine —
scribe_p_unaryis a recursive-descent helper that negates its recursive result (0 minus (r["val"])), producing 2 of these notes on an otherwise-clean run (down from 106 before #589).Related
34512e8(so Typechecker: unannotated action return types default to Nothing, causing false "Cannot index into Nothing" warnings #560 may be closeable). This issue is the recursive case fix: infer user-defined action return types in the type checker #575's ordering can't reach.store x as <action call>raises ERROR "Could not infer type for variable" whenever the callee's return type is Unknown #588 / fix: bind Unknown-typed store results silently under gradual typing (#588) #589 — madeUnknowndegrade gracefully, which is what makes the suggestedUnknown-seed fix viable.