Skip to content

[checking] Allow recovery for do expressions with final let#159

Merged
purefunctor merged 2 commits into
mainfrom
justin/do-checking-recovery
Jun 5, 2026
Merged

[checking] Allow recovery for do expressions with final let#159
purefunctor merged 2 commits into
mainfrom
justin/do-checking-recovery

Conversation

@purefunctor

Copy link
Copy Markdown
Owner

Prerequisite work for inference-driven completions

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Lost in the diff? Review this PR in Change Stack to follow the change map from intent to exact ranges.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 716bf4ba-262f-4fc9-b0ce-8d3c82e4a863

📥 Commits

Reviewing files that changed from the base of the PR and between d9eed08 and a4cf1d3.

⛔ Files ignored due to path filters (2)
  • tests-integration/fixtures/checking/1772818740_do_final_let/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/checking/1780673760_do_final_let/Main.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • compiler-core/checking/src/source/terms/form_do.rs

Summary

This PR makes the do-expression inference recover when a do-block ends with a final let binding. It refactors the terminal-step handling in infer_do so the checker can continue inference (with placeholder continuation types) instead of bailing out immediately on a final let — preparatory work for inference-driven completions.

Key changes

  • compiler-core/checking/src/source/terms/form_do.rs

    • Introduces DoStep, DoBlockFinalStep and helpers (DoStep::is_action, DoBlockFinalStep::is_invalid_let) to explicitly model statement kinds and the final-step shape (Empty, InvalidBind, Discard, InvalidLet).
    • Reworks infer_do:
      • Builds a steps vector during a forward pass; computes final_step from steps.last().
      • Produces a final_expression only when appropriate; reports EmptyDoBlock and InvalidFinalBind where applicable.
      • For an invalid final let, records the InvalidFinalLet error and skips checking a final expression while allocating an extra continuation slot so inference proceeds with placeholder types.
      • Synthesises or looks up bind/discard/map/pure/apply as before, but continuation sizing now counts action steps and accounts for an invalid final let to avoid premature termination.
    • Small API rename: parameter statement_ids → statement_id (the parameter is still a slice).
  • tests-integration/fixtures/checking/1780673760_do_final_let/Main.purs

    • Adds an integration test demonstrating a do expression ending with a final let:
      • test = do { record <- before; let _ = expectString record.field }
    • Verifies the compiler emits InvalidFinalLet and recovers inference instead of failing outright.

Behavioural impact

  • The type checker now:
    • Emits clearer diagnostics for invalid final binds/discards and invalid final lets,
    • Continues inference past a final let using placeholder continuation types, enabling downstream tooling (e.g. completion) to operate on partially-written do-blocks.

Notes / Minor issues

  • The parameter rename (statement_ids → statement_id) is cosmetic and may be confusing; it does not change behaviour.

Confidence 4/5 — "Doesn't crash, now tutors the user."

  • Confidence uplift: focused, small-scope change; unit/regression test added that exercised the original failure and now passes.
  • Remaining uncertainty: minor API naming oddity and whether all call sites (outside shown file) expect the renamed parameter name; no behavioural regressions observed in the inspected files.

Walkthrough

Refactors infer_do to explicitly model the final do-block step and final expression, classify action steps, adjust continuation sizing, and conditionally perform final-expression checking; adds an integration fixture exercising final-let handling.

Changes

Do-block final step refactoring

Layer / File(s) Summary
Final-step state types and parameter signature
compiler-core/checking/src/source/terms/form_do.rs
Adds DoStep::is_action helper and internal DoBlockFinalStep/helpers; renames infer_do parameter slice from statement_ids to statement_id and updates iteration.
Final-step and final-expression computation
compiler-core/checking/src/source/terms/form_do.rs
Computes final_step from steps.last() and derives final_expression with granular empty-expression and invalid final bind/discard detection; revises continuation_count to count action steps plus an extra slot for an invalid final let and builds continuation_types.
Conditional final expression checking
compiler-core/checking/src/source/terms/form_do.rs
Calls super::check_expression only when final_expression is present; for InvalidFinalLet records the error and skips final-expression checking, then returns the computed first_continuation.
Test fixture for final-let validation
tests-integration/fixtures/checking/1780673760_do_final_let/Main.purs
Adds a Main module with foreign effect bindings and a test action that executes before, reads record.field, passes it to expectString, and discards the result to exercise final-let behaviour.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description relates to the changeset, indicating it is prerequisite work for inference-driven completions, which aligns with the changes to do-expression handling.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.80488% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.25%. Comparing base (c37c983) to head (a4cf1d3).

Files with missing lines Patch % Lines
compiler-core/checking/src/source/terms/form_do.rs 87.80% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #159      +/-   ##
==========================================
+ Coverage   83.21%   83.25%   +0.04%     
==========================================
  Files         133      133              
  Lines       25132    25149      +17     
  Branches    25132    25149      +17     
==========================================
+ Hits        20913    20939      +26     
+ Misses       2264     2259       -5     
+ Partials     1955     1951       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@compiler-core/checking/src/source/terms/form_do.rs`:
- Around line 402-407: The InvalidLet branch currently returns
context.unknown("invalid final let"), which discards the previously constructed
placeholder continuation and loses recovered monadic/effect information;
instead, preserve the placeholder continuation by routing this branch through
the same checking path as the valid final expression: call
super::check_expression with the appropriate synthetic/dummy final expression
(so the existing final_continuation is used) rather than returning a fresh
Unknown. Locate the DoBlockFinalExpression::InvalidLet branch and replace the
context.unknown return with a call that invokes super::check_expression(state,
context, /* synthetic final expression */, final_continuation) so the
placeholder continuation and recovered effect/shape are retained.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fcbf379-d33d-4d96-9bef-0d5592f397b7

📥 Commits

Reviewing files that changed from the base of the PR and between c37c983 and d9eed08.

⛔ Files ignored due to path filters (1)
  • tests-integration/fixtures/checking/1780673760_do_final_let/Main.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • compiler-core/checking/src/source/terms/form_do.rs
  • tests-integration/fixtures/checking/1780673760_do_final_let/Main.purs

Comment on lines +402 to +407
match final_expression {
DoBlockFinalExpression::Check(expression) => {
super::check_expression(state, context, expression, final_continuation)?;
}
DoBlockFinalExpression::InvalidLet => {
return Ok(context.unknown("invalid final let"));

@coderabbitai coderabbitai Bot Jun 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Returning Unknown here drops the recovered do-block type.

The placeholder continuation built above is what lets earlier bind/discard steps solve the monadic shape, but this branch throws that away and makes the whole do expression a fresh Unknown. In a block like do record <- before; let _ = ..., downstream inference still loses the recovered Effect _, which undercuts the recovery this change is aiming for.

🤖 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 `@compiler-core/checking/src/source/terms/form_do.rs` around lines 402 - 407,
The InvalidLet branch currently returns context.unknown("invalid final let"),
which discards the previously constructed placeholder continuation and loses
recovered monadic/effect information; instead, preserve the placeholder
continuation by routing this branch through the same checking path as the valid
final expression: call super::check_expression with the appropriate
synthetic/dummy final expression (so the existing final_continuation is used)
rather than returning a fresh Unknown. Locate the
DoBlockFinalExpression::InvalidLet branch and replace the context.unknown return
with a call that invokes super::check_expression(state, context, /* synthetic
final expression */, final_continuation) so the placeholder continuation and
recovered effect/shape are retained.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai Then if we return first_continuation, depending on the shape of the action statements we would get either an unsolved unification variable or the real type of the last action statement. I think this makes sense

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@purefunctor That reasoning is exactly right. Returning first_continuation is the correct fix here:

DoBlockFinalExpression::InvalidLet => {
    return Ok(first_continuation);
}
  • With prior action stepsfirst_continuation is already solved to the monadic shape established by bind/discard, so the do-expression type is recovered correctly.
  • With no prior action stepsfirst_continuation is the sole placeholder unification variable introduced by usize::from(final_step.is_invalid_let()), which is still better than a fresh Unknown because it remains in the constraint graph and can be solved by an outer context.

Either way it avoids silently throwing away the work done by the continuation-building loop. The current context.unknown(...) return is the thing that actually "undercuts recovery", as I flagged — returning first_continuation is the minimal, correct fix.


✏️ Learnings added
Learnt from: purefunctor
Repo: purefunctor/purescript-alexandrite PR: 159
File: compiler-core/checking/src/source/terms/form_do.rs:402-407
Timestamp: 2026-06-05T20:51:36.615Z
Learning: In `compiler-core/checking/src/source/terms/form_do.rs`, when `DoBlockFinalExpression::InvalidLet` is reached in `infer_do`, the correct recovery behavior is to return `Ok(first_continuation)` rather than `Ok(context.unknown("invalid final let"))`. `first_continuation` is solved through prior bind/discard action steps and preserves the monadic shape; if no action steps precede the final let, it is the placeholder unification variable introduced by `usize::from(final_step.is_invalid_let())`, which is still better than a fresh unknown that loses all constraint context.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: purefunctor
Repo: purefunctor/purescript-alexandrite PR: 155
File: compiler-core/lowering/src/algorithm.rs:119-127
Timestamp: 2026-06-01T19:07:01.118Z
Learning: In this codebase, `NonFatal` is not an error: it is shorthand for returning `Ok(None)` (meaning “no result, but not a failure”). During Rust code reviews, treat `NonFatal` flows as a non-error/“no result” path and avoid flagging them as error-handling or failure cases; only consider the code to be in an error path when it returns an actual `Err(...)`.

@purefunctor purefunctor force-pushed the justin/do-checking-recovery branch from d9eed08 to a4cf1d3 Compare June 5, 2026 20:54
@purefunctor purefunctor merged commit ab0d4a4 into main Jun 5, 2026
13 checks passed
@purefunctor purefunctor deleted the justin/do-checking-recovery branch June 5, 2026 21:12
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.

1 participant