Skip to content

Type checker: self-recursive action's result is typed Nothing inside its own body (false "Cannot index into Nothing") #590

Description

@logbie

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions