Skip to content

Allow field use after a conditional field move#994

Open
shindakioku wants to merge 3 commits into
inko-lang:mainfrom
shindakioku:fix-conditional-field-move
Open

Allow field use after a conditional field move#994
shindakioku wants to merge 3 commits into
inko-lang:mainfrom
shindakioku:fix-conditional-field-move

Conversation

@shindakioku

Copy link
Copy Markdown

When a field is conditionally moved out of self (e.g. if x { drop(@a) }), using a different field afterwards was wrongly rejected as "self has been moved". Joining the branches leaves self as MaybeMoved, which was treated the same as a whole move. We now report this only when self itself is moved on one of the paths.

This fixes #962.

Changelog: fixed

When a field is conditionally moved out of `self` (e.g.
`if x { drop(@A) }`), using a different field afterwards was wrongly
rejected as "self has been moved". Joining the branches leaves `self`
as `MaybeMoved`, which was treated the same as a whole move. We now
report this only when `self` itself is moved on one of the paths.

This fixes inko-lang#962.

Changelog: fixed
@yorickpeterse
yorickpeterse self-requested a review July 9, 2026 20:37
Comment thread compiler/src/mir/passes.rs Outdated
// field is moved. In the latter case `self` itself is still valid,
// so reading a *different* field is fine. To tell the two apart we
// walk the predecessor blocks and only report an error if `self`
// was actually moved as a whole on one of them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment isn't really useful as it mostly just replicates what's already in the issue, so it can/should just be removed.

Comment thread compiler/src/mir/passes.rs Outdated
// walk the predecessor blocks and only report an error if `self`
// was actually moved as a whole on one of them.
RegisterState::MaybeMoved => {
if self.self_fully_moved_on_any_path() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This method isn't the direction we should take. It essentially implements a similar approach to the register_state method but that's not necessary.

I hinted at this in Discord but I think I didn't do a good job, but what I think we need to do is this:

  1. LowerMethod::partially_move_self_if_field needs to be removed along with its call sites
  2. When the state of a field is updated, we only update that field register's state (which the previous step should accomplish)
  3. In self_expression and other places where self is used (e.g. as the implicit receiver of a method call), we iterate over the values in LowerMethod::field_mapping and for each register check if its state is Available. If not, we produce the "self is partially moved" diagnostic
  4. In LowerMethod::field we only check the state of the the field's register, not that of LowerMethod::self_register, so basically the match is replaced with just the appropriate call to LowerMethod::check_if_moved
  5. If self is moved, in addition to updating the state of its own register we also update the register state of each field to the same state as self, so basically move(self) translates to move(self), move(@field1), move(@fieldN), ...


type Inner {}

impl Drop for Inner {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These drop implementations are not necessary for the purpose of this test, so they can be removed. The same is true for the other diagnostic test.

Instead of marking `self` as partially moved and walking predecessor
blocks to distinguish a whole-`self` move from a field move, the move
state is now tracked per field register.

Reading a field checks only that field's register; moving `self`
marks all of its fields as moved; and whether `self` is partially
moved is derived from its fields where needed (e.g. when using
`self` directly or as a method receiver).
Comment thread compiler/src/mir/passes.rs Outdated

// Moving `self` as a whole also moves all of its fields.
if register == self.self_register {
let fields: Vec<RegisterId> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cloning and collecting here is redundant and can be avoided by just copying the body of update_register_state here. It's just one simple line so there's no harm in duplicating that.

Comment thread compiler/src/mir/passes.rs Outdated
/// Returns `true` if any field of `self` has been (possibly) moved.
fn self_has_moved_field(&mut self) -> bool {
let fields: Vec<RegisterId> =
self.field_mapping.values().cloned().collect();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here, cloning and collecting is redundant here.

@@ -0,0 +1,22 @@
import std.drop (drop)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There should also be a diagnostic test added for when self is used explicitly after moving a field, and a test for when a method is called on an implicit self after a field is moved.

While we're at it, a diagnostic test in which self is captured by a closure (both explicitly and implicitly) after it's partially moved would be useful to ensure that continues to work as expected.

Comment thread compiler/src/mir/passes.rs Outdated
);
}
} else {
self.check_if_moved(reg, SELF_NAME, node.location);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this call only happens if self.self_register is not moved, using check_if_moved instead of directly calling self.state.diagnostics.moved_variable() is just redundant. It's also a bit weird because in the if body you do use moved_variable directly.

Comment thread compiler/src/mir/passes.rs Outdated
RegisterState::PartiallyMoved => {
// Fully moved: its fields are moved too, so there's nothing to drop.
RegisterState::Moved => {}
// Maybe moved as a whole: `drop_register` drops it conditionally.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The "drop_register drops it conditionally" is redundant because it just describes what the code does but in English. More precisely, comments should be used to describe intent/meaning or something else that isn't necessarily obvious, rather than describe in English what the code already describes.

Comment thread compiler/src/mir/passes.rs Outdated

match self.register_state(self_reg) {
RegisterState::PartiallyMoved => {
// Fully moved: its fields are moved too, so there's nothing to drop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment isn't really useful because self being Moved as a whole is obvious enough on its own.

Comment thread compiler/src/mir/passes.rs Outdated
self.drop_register(self_reg, location);
}
// Available but with some fields moved out: drop the remaining
// fields and free self's memory without running its dropper.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here as above: don't use comments to describe what the code already describes, there's no value in that. In fact, because of the if partially_moved below I feel this entire comment is redundant.

Comment thread compiler/src/mir/passes.rs Outdated
self.drop_partial_self(&fields, location);
}
RegisterState::Available | RegisterState::MaybeMoved => {
RegisterState::Available | RegisterState::PartiallyMoved => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mark_register_as_partially_moved gets called when pattern matching with bindings, flagging self as PartiallyMoved when you match against it, so something like this:

match self {
  case { @field1 = a, @field2 = b, ... } -> { ... }
}

This would then cause this pattern body to treat it as if self were fully available when it is in fact not, causing the fields to be (potentially) dropped twice.

I think in this case the PartiallyMoved pattern/body should just remain the same as-is.

@yorickpeterse yorickpeterse mentioned this pull request Jul 17, 2026
14 tasks
Detect when `self` (or one of its fields) is used after a field has been
moved out, emitting a `moved` diagnostic. Handle the `PartiallyMoved`
state explicitly when dropping `self` so remaining fields are dropped and
`self`'s memory is freed without running its dropper. Switch
`field_mapping` to an `IndexMap` for stable positional iteration.

Adds diagnostics fixtures covering self use, implicit method calls, and
closure captures after a field move.
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.

When one field is conditionally moved other fields can no longer be used

2 participants