Skip to content

del statement support#309

Open
kmad wants to merge 9 commits intopydantic:mainfrom
kmad:del-statement
Open

del statement support#309
kmad wants to merge 9 commits intopydantic:mainfrom
kmad:del-statement

Conversation

@kmad
Copy link
Copy Markdown

@kmad kmad commented Apr 2, 2026

del statement support

Implements del across the full pipeline — parsing, prepare, bytecode compilation, and VM
execution.

What works

  • del x — unbinds variables (locals, globals, cells), subsequent access raises NameError or
    UnboundLocalError exactly as CPython does
  • del d['key'] — dict item removal via __delitem__
  • del lst[i] — list item removal with negative index support
  • del a, b — multi-target, executed left-to-right
  • Closuresdel on captured variables correctly propagates: outer scope gets
    UnboundLocalError, inner closures get NameError for free variables
  • nonlocal + del — deleting via nonlocal unbinds the cell in the enclosing scope

New opcodes

  • DeleteSubscr — pops index + container, calls __delitem__
  • DeleteLocalW — wide variant of DeleteLocal for slots > 255
  • DeleteCell — stores Undefined in a closure cell

All three appended to the enum to preserve discriminant stability.

Bug fix (pre-existing)

load_global was raising UnboundLocalError for undefined module-level variables that had
previously been assigned. CPython always raises NameError for globals. Fixed.

Not yet supported

  • del obj.attr — blocked on user-defined classes

Written in collaboration with claude-opus-4-6 and gpt-5.4-high

kmad and others added 5 commits April 2, 2026 06:28
Implements the `del` statement across the full pipeline: parsing,
prepare phase, bytecode compilation, and VM execution. Supports
`del x` (variable unbinding), `del d['key']` (dict item removal),
`del lst[i]` (list item removal), and multi-target `del a, b`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…bles

- `delete_local` and `delete_global` now raise errors when the target is
  already unbound (NameError or UnboundLocalError, matching CPython)
- Fix `load_global` to raise NameError (not UnboundLocalError) for
  module-level variables — globals never produce UnboundLocalError in CPython
- Reject `del` with slice targets at parse time (NotImplementedError)
- Fix test syntax errors (statements on same line without separator)
- Add tests for double-delete, missing global delete, error messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `DeleteCell` and `DeleteLocalW` opcodes (appended to preserve
discriminant stability) so `del` works correctly on cell variables
captured by closures and on local slots above 255.

- `DeleteCell` stores `Value::Undefined` in the cell, making both the
  outer scope (UnboundLocalError) and inner closures (NameError) see the
  variable as unbound — matching CPython semantics for `del x` where `x`
  is captured, and `del` via `nonlocal`
- `DeleteLocalW` is the wide variant of `DeleteLocal` for functions with
  more than 255 locals
- `load_cell` now distinguishes owned cell vars (UnboundLocalError) from
  free vars (NameError) via `is_owned_cell_slot`

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the parse-time rejection of slice targets in `del` statements.
List `py_delitem` now handles slice keys by collecting the selected
indices and removing them highest-first to preserve index validity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 2, 2026

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing kmad:del-statement (5bf0010) with main (35aae2c)

Open in CodSpeed

kmad and others added 2 commits April 2, 2026 07:47
- TypeError: "doesn't" not "does not" for item deletion
- IndexError: "list assignment index out of range" for del (matches setitem)
- Fix rustfmt formatting in parse_errors test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…und locals

`load_global` was changed to raise `NameError` for all assigned locals,
but comprehension loop variables (also globals in Monty) need
`UnboundLocalError`. Track explicitly deleted globals in a `HashSet`
so `load_global` can distinguish "deleted via `del`" (NameError) from
"unbound comprehension variable" (UnboundLocalError).

Also removes unused `clear_assigned_local` method from `Code`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

@samuelcolvin
Copy link
Copy Markdown
Member

@kmad unfortunately this seems to have had a meaningful impact on performance, can you figure out why, or do you need help?

@kmad
Copy link
Copy Markdown
Author

kmad commented Apr 2, 2026

Will look & revert back, sorry about that.

kmad and others added 2 commits April 2, 2026 19:35
The `HashSet<u16>` field added 48 bytes to the VM struct even when empty,
hurting cache locality in the hot execution loop and causing ~14-17%
regression on benchmarks that never use `del`.

Replace with `Option<Box<HashSet<u16>>>` (8 bytes when None) and guard
all accesses so the common path (no `del` used) pays zero cost:
- `store_global`: skip remove when None
- `load_global`: skip contains check when None
- `delete_global`: lazily allocate on first use

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kmad
Copy link
Copy Markdown
Author

kmad commented Apr 3, 2026

@samuelcolvin the performance regression has been resolved.

Copy link
Copy Markdown
Member

@samuelcolvin samuelcolvin left a comment

Choose a reason for hiding this comment

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

otherwise looking good.

///
/// Boxed and wrapped in `Option` to keep the VM struct small (8 bytes vs 48)
/// since `del` on globals is rare and the VM is accessed on every instruction.
deleted_globals: Option<Box<HashSet<u16>>>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't really understand the point of deleted_globals. suspect we can remove it.

Even if there are subtle differences between cpython and monty on this, it seems like a price worth paying.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we really need this to be the same, maybe we could add a new variant to Value, e.g. Value::GlobalDeleted instead of adding this deleted_globals?

}
}

#[cfg(test)]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd rather not add tests in code unless absolutely necessary.

// Wide variant not implemented yet
todo!("DeleteLocalW for slot > 255");
if matches!(target.scope, NameScope::Local) {
self.code.register_assigned_local(slot);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not clear why this is necessary?

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.

2 participants