Skip to content

Fix issue #560 residuals: returns in try blocks and container methods - #599

Merged
logbie merged 5 commits into
mainfrom
claude/issue-560-fix-lu08sd
Jul 10, 2026
Merged

Fix issue #560 residuals: returns in try blocks and container methods#599
logbie merged 5 commits into
mainfrom
claude/issue-560-fix-lu08sd

Conversation

@logbie

@logbie logbie commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes two remaining false-positive "Cannot index into Nothing" diagnostics from issue #560, where unannotated actions and container methods that clearly return values were incorrectly typed as Nothing:

  1. Returns inside try: blocks were invisible to return-type inferencecollect_return_types did not descend into try: bodies, when error clauses, otherwise, or finally blocks, causing actions whose only returns lived in these blocks to be inferred as Nothing.

  2. Unannotated container methods were registered with Nothing and never refined — mirroring the original Typechecker: unannotated action return types default to Nothing, causing false "Cannot index into Nothing" warnings #560 issue but for instance/static methods, which had no post-body inference pass.

Key Changes

  • src/typechecker/mod.rs:

    • Extended collect_return_types to descend into TryStatement (body, all when clauses, otherwise, finally) and WaitForStatement, so returns in error-handling blocks are now visible to inference.
    • Extended check_return_statements (the annotated-method validation path) with the same arms to keep both traversals in sync.
    • Added container method body checking in the ContainerDefinition arm: parameters are now in scope during body analysis (mirroring Type checker: more RHS forms fail inference inside included files — index / object-index / comparison / length of (follow-on to #551) #553 handling for top-level actions), and inferred return types are collected and written back to the registry via get_container_mut.
  • src/analyzer/mod.rs:

    • Changed unannotated instance and static method registration to seed Type::Unknown (instead of Type::Nothing), allowing graceful degradation before the type checker refines the type.
    • Added get_container_mut method to allow the type checker to update inferred return types in the container registry after analyzing method bodies.
  • tests/action_return_type_residuals_test.rs (new):

    • Four regression tests confirming the fixes: returns inside try/when error, returns inside try/when/otherwise, unannotated container method results, and inherited container method results all now type-check cleanly without false diagnostics.
  • Dev diary entry documenting the investigation, root causes, and design rationale.

Implementation Details

https://claude.ai/code/session_016urN3UdbLEQaGtHvNtLC7k

Summary by CodeRabbit

  • Bug Fixes
    • Improved return-type inference for actions containing try, when error, otherwise, finally, and wait for blocks so reachable return statements are no longer ignored.
    • Fixed unannotated container methods being treated as returning Nothing; provisional types are refined to the actual inferred return type, while void methods remain Nothing.
    • Improved type handling for inherited container method return types to prevent incorrect Nothing results at call sites.
  • Tests
    • Added regression tests covering residual return-type inference cases for error-handling and container methods.

…ethods (#560)

Two residual shapes of issue #560 still produced false 'Cannot index
into Nothing' diagnostics after #575/#591:

- collect_return_types never descended into try statements, so an
  action whose only returns live inside a try body, when-error clause,
  otherwise, or finally block was inferred as returning Nothing. It now
  traverses TryStatement and WaitForStatement (check_return_statements
  kept in sync).

- Container methods were registered with return_type Nothing when
  unannotated and never refined, so instance.method() results hit the
  same false error. The analyzer now seeds unannotated methods with a
  provisional Unknown, and the type checker infers the real return type
  from each method body (parameters in scope, mirroring the top-level
  action arm) and writes it back to the container registry via a new
  Analyzer::get_container_mut. Inherited method calls read the same
  registry entries, so they are fixed too.

Static-diagnostics-only change; runtime behavior is unchanged. TDD:
tests/action_return_type_residuals_test.rs was confirmed failing (4/4)
before the fix and passes after, alongside the full test suite and all
TestPrograms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016urN3UdbLEQaGtHvNtLC7k
Copilot AI review requested due to automatic review settings July 10, 2026 11:52
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3350f0e-dff3-4047-8279-dd96c3c5c212

📥 Commits

Reviewing files that changed from the base of the PR and between 10e5754 and c9a8455.

📒 Files selected for processing (2)
  • Dev diary/2026-07-10-issue-560-residuals-try-returns-container-methods.md
  • src/typechecker/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • Dev diary/2026-07-10-issue-560-residuals-try-returns-container-methods.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/typechecker/mod.rs

📝 Walkthrough

Walkthrough

The type checker now traverses returns inside try and wait for constructs, and infers unannotated container method return types from method bodies. Regression tests cover nested returns, direct methods, and inherited methods.

Changes

Return-type residual fixes

Layer / File(s) Summary
Nested return traversal
src/typechecker/mod.rs, tests/action_return_type_residuals_test.rs, Dev diary/...
Return inference and validation now cover try branches, finally, and wait for bodies. Tests cover try/when error and otherwise returns.
Container method return refinement
src/analyzer/mod.rs, src/typechecker/mod.rs, tests/action_return_type_residuals_test.rs, Dev diary/...
Unannotated methods use provisional Type::Unknown, are inferred from their bodies, and update the container registry for direct and inherited method calls.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: return inference for try-block paths and container methods in issue #560.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-560-fix-lu08sd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 10, 2026 12:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/typechecker/mod.rs (1)

1652-1663: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unannotated static container methods are never refined — static_methods is destructured but unused.

The new loop at Lines 1739-1781 only iterates methods (instance methods). static_methods is bound as _static_methods (Line 1660) and dropped entirely. Combined with the analyzer change (seeding both instance and static unannotated methods with Type::Unknown), this means:

  • Value-returning unannotated static methods stay Unknown forever instead of getting the real inferred type (defeats the stated purpose for statics).
  • Void unannotated static methods, which previously defaulted to Nothing, now also stay Unknown — a real strictness regression: indexing/misusing a void static method's result will no longer be caught.

Static methods should get the same body-check + infer + write-back treatment as instance methods, updating container_info.static_methods via get_container_mut afterward.

🐛 Proposed fix to also process static methods
             Statement::ContainerDefinition {
                 name: _name,
                 extends,
                 implements,
                 properties,
                 methods,
                 events: _events,
                 static_properties: _static_properties,
-                static_methods: _static_methods,
+                static_methods,
                 line,
                 column,
             } => {
@@
                 let mut inferred_method_returns: Vec<(String, Type)> = Vec::new();
-                for method in methods {
+                let mut inferred_static_method_returns: Vec<(String, Type)> = Vec::new();
+                for (method, is_static) in methods
+                    .iter()
+                    .map(|m| (m, false))
+                    .chain(static_methods.iter().map(|m| (m, true)))
+                {
                     if let Statement::ActionDefinition {
                         name: method_name,
                         parameters,
                         body,
                         return_type,
                         ..
                     } = method
                     {
                         // Set container context for method body analysis
                         let previous_container = self.current_container.clone();
                         self.current_container = Some(_name.clone());
@@
                         if return_type.is_none() {
                             let inferred = self.infer_action_return_type(body);
-                            inferred_method_returns.push((method_name.clone(), inferred));
+                            if is_static {
+                                inferred_static_method_returns.push((method_name.clone(), inferred));
+                            } else {
+                                inferred_method_returns.push((method_name.clone(), inferred));
+                            }
                         }
@@
                 if let Some(container_info) = self.analyzer.get_container_mut(_name) {
                     for (method_name, inferred) in inferred_method_returns {
                         if let Some(method_info) = container_info.methods.get_mut(&method_name) {
                             method_info.return_type = inferred;
                         }
                     }
+                    for (method_name, inferred) in inferred_static_method_returns {
+                        if let Some(method_info) = container_info.static_methods.get_mut(&method_name) {
+                            method_info.return_type = inferred;
+                        }
+                    }
                 }

Also applies to: 1739-1790

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typechecker/mod.rs` around lines 1652 - 1663, Process static methods
alongside instance methods in the ContainerDefinition handling: bind
static_methods instead of discarding it, run each unannotated static method
through the same body-checking and type-inference logic as the methods loop, and
write the inferred type back to container_info.static_methods using
get_container_mut. Preserve annotated methods and ensure void static methods are
refined to Nothing rather than remaining Unknown.
src/analyzer/mod.rs (1)

1343-1352: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Static methods are seeded as Unknown here but never refined downstream.

This seed is consistent with the instance-method change, but per the review of src/typechecker/mod.rs's ContainerDefinition arm, static_methods is never iterated to infer/write back the real return type. As a result, this Unknown seed becomes permanent for every unannotated static method, including void ones that used to resolve to Nothing. See the corresponding comment in src/typechecker/mod.rs for the fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/analyzer/mod.rs` around lines 1343 - 1352, Update the static-method
handling that creates the MethodInfo in the relevant analyzer branch and the
corresponding ContainerDefinition logic in the typechecker: ensure
static_methods are iterated for return-type inference and the inferred types are
written back, so the provisional Type::Unknown seed is refined for unannotated
methods, including void methods resolving to Nothing.
🧹 Nitpick comments (2)
src/typechecker/mod.rs (2)

1771-1774: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

check_return_statements is never invoked for annotated container methods.

Unlike the top-level ActionDefinition arm (Line 796-798: if let Some(ret_type) = return_type { self.check_return_statements(...) }), the container-method loop only handles the return_type.is_none() branch. Currently inert since WFL has no return-type annotation syntax, but worth mirroring for consistency/future-proofing given the arm explicitly aims to mirror the top-level handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typechecker/mod.rs` around lines 1771 - 1774, Update the container-method
handling near infer_action_return_type to mirror the top-level ActionDefinition
logic: when return_type is Some, invoke check_return_statements with the method
body and annotated return type; retain inference and inferred_method_returns for
the None branch.

3886-3911: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared try/wait for sub-statement traversal.

collect_return_types and check_return_statements now contain near-identical TryStatement/WaitForStatement destructuring and iteration logic, differing only in which recursive call they make. A small helper returning the list of contained statement slices (body, each when body, otherwise, finally, wait-for inner) would let both callers loop over it, reducing duplication and the risk of the two traversals drifting apart again in the future.

Also applies to: 4002-4029

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/typechecker/mod.rs` around lines 3886 - 3911, Extract the duplicated
TryStatement/WaitForStatement traversal from collect_return_types and
check_return_statements into a shared helper that returns or iterates over the
contained statement slices: try body, each when clause body, otherwise, finally,
and wait-for inner statement. Update both callers to use this helper while
preserving their respective recursive operations, ensuring traversal behavior
remains consistent.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dev` diary/2026-07-10-issue-560-residuals-try-returns-container-methods.md:
- Around line 70-83: The ContainerDefinition type-checking logic refines only
instance methods, leaving unannotated static methods as Unknown. Update the
relevant ContainerDefinition handling in the typechecker to iterate and infer
return types for static_methods as well, including void methods resolving to
Nothing; then verify the diary accurately describes both instance and static
method refinement.

In `@tests/action_return_type_residuals_test.rs`:
- Around line 82-134: Add a regression test alongside the existing residual type
tests for an unannotated static container method invoked as
Container.static_method(). Define a container with a static action returning a
list, assign the call result, index it, and display the element via
assert_typechecks_clean; name the test to clearly indicate static container
method results are not typed Nothing.

---

Outside diff comments:
In `@src/analyzer/mod.rs`:
- Around line 1343-1352: Update the static-method handling that creates the
MethodInfo in the relevant analyzer branch and the corresponding
ContainerDefinition logic in the typechecker: ensure static_methods are iterated
for return-type inference and the inferred types are written back, so the
provisional Type::Unknown seed is refined for unannotated methods, including
void methods resolving to Nothing.

In `@src/typechecker/mod.rs`:
- Around line 1652-1663: Process static methods alongside instance methods in
the ContainerDefinition handling: bind static_methods instead of discarding it,
run each unannotated static method through the same body-checking and
type-inference logic as the methods loop, and write the inferred type back to
container_info.static_methods using get_container_mut. Preserve annotated
methods and ensure void static methods are refined to Nothing rather than
remaining Unknown.

---

Nitpick comments:
In `@src/typechecker/mod.rs`:
- Around line 1771-1774: Update the container-method handling near
infer_action_return_type to mirror the top-level ActionDefinition logic: when
return_type is Some, invoke check_return_statements with the method body and
annotated return type; retain inference and inferred_method_returns for the None
branch.
- Around line 3886-3911: Extract the duplicated TryStatement/WaitForStatement
traversal from collect_return_types and check_return_statements into a shared
helper that returns or iterates over the contained statement slices: try body,
each when clause body, otherwise, finally, and wait-for inner statement. Update
both callers to use this helper while preserving their respective recursive
operations, ensuring traversal behavior remains consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfb1581f-30fe-468c-b3a6-66fb47e0931f

📥 Commits

Reviewing files that changed from the base of the PR and between 2ce9ad4 and 10e5754.

📒 Files selected for processing (4)
  • Dev diary/2026-07-10-issue-560-residuals-try-returns-container-methods.md
  • src/analyzer/mod.rs
  • src/typechecker/mod.rs
  • tests/action_return_type_residuals_test.rs

Comment thread tests/action_return_type_residuals_test.rs
…ed method returns

Address CodeRabbit review on #599:

- Static methods were seeded with the provisional Unknown but never
  refined: the ContainerDefinition arm only iterated instance methods.
  Value-returning statics stayed Unknown forever and void statics lost
  their previous Nothing type. Static methods now go through the same
  body-check + infer + write-back loop, updating
  container_info.static_methods. (Static method calls remain a runtime
  future feature; the registry refinement keeps Container.method member
  access accurate and restores Nothing for void statics.)

- Annotated container methods (action name: Type) now have their return
  statements validated against the annotation via
  check_return_statements, mirroring the top-level action arm.

- Added a registry-level unit test pinning both static cases (inferred
  List for a value-returning static, Nothing for a void static), since
  a typecheck-clean integration test cannot observe static calls that
  the runtime rejects. Dev diary updated to match the implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016urN3UdbLEQaGtHvNtLC7k
Copilot AI review requested due to automatic review settings July 10, 2026 12:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

flush_test_*.txt, test_output.txt, and a google_index.html overwrite
were produced by running the TestPrograms suite locally and swept in by
git add -A. Remove the artifacts and restore google_index.html to its
prior content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016urN3UdbLEQaGtHvNtLC7k
Copilot AI review requested due to automatic review settings July 10, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 10, 2026 14:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@logbie
logbie merged commit def3027 into main Jul 10, 2026
15 of 16 checks passed
@logbie
logbie deleted the claude/issue-560-fix-lu08sd branch July 10, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants