Skip to content

Renewd DictUpdate instruction #6085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4398,7 +4398,7 @@ impl Compiler {

for item in unpacked {
self.compile_expression(&item.value)?;
emit!(self, Instruction::DictUpdate);
emit!(self, Instruction::DictUpdate { index: 1 });
}

Ok(())
Expand Down
8 changes: 5 additions & 3 deletions compiler/core/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,9 @@ pub enum Instruction {
BuildMapForCall {
size: Arg<u32>,
},
DictUpdate,
DictUpdate {
index: Arg<u32>,
},
Comment on lines +670 to +672
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Clarify DictUpdate index semantics and verify call sites

Good move making DictUpdate carry index: Arg<u32>; naming aligns with CopyItem/Swap.

Please add a short doc comment clarifying:

  • What index is relative to (TOS before pop vs. after popping the source mapping).
  • Whether it’s a 0-based offset and any bounds expectations.

Also, since this changes the bytecode argument shape, ensure marshaled bytecode compatibility is addressed (e.g., version bump/invalidation or explicit handling when index == 0 to preserve legacy behavior).

Run this to find any stale patterns still matching/constructing the unit variant form:

I can draft the doc comment and add unit tests asserting stack effect and disassembly output once you confirm the intended “before/after pop” convention.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "All occurrences of DictUpdate with context (sanity check):"
rg -n --hidden --glob '!target' -A3 -B3 '\bDictUpdate\b'

echo
echo "Likely pattern matches not using struct form (manual review recommended):"
rg -n --hidden --glob '!target' -e '\bDictUpdate\s*=>' -e '\bInstruction::DictUpdate\s*[),]|,$'

echo
echo "Constructors not using struct form (look for missing braces):"
rg -n --hidden --glob '!target' -e '\bInstruction::DictUpdate\b(?!\s*\{)' -U

Length of output: 319276


Add documentation for DictUpdate index semantics & ensure marshaling compatibility

Please update compiler/core/src/bytecode.rs around the DictUpdate variant (lines 667–672) as follows:

  • Insert a doc comment explaining that index is a 0-based offset to the dict on the stack before popping the source mapping. For example:
    /// Pop a source mapping from the top of the stack and update
    /// the dict located `index` slots below it. `index` is a 0-based
    /// offset relative to the top-of-stack *before* the pop.
    DictUpdate {
        index: Arg<u32>,
    },
  • Note in the doc that legacy bytecode (with no index field) will decode with index == 0; if you intend to break compatibility, bump the bytecode version or handle index == 0 explicitly in tests.

Next steps:

  • No stale unit-variant constructors were found (grep output shows only struct-form usage), so no further code changes required there.
  • Add unit tests in the bytecode test module asserting:
    • Instruction::DictUpdate { index } yields stack_effect() == -1
    • fmt_dis() prints DictUpdate { index }
  • Add a marshaling test that serializes an old-format DictUpdate (without arg) and ensures it decodes to DictUpdate { index: 0 }.
🤖 Prompt for AI Agents
In compiler/core/src/bytecode.rs around lines 667 to 672, add a doc comment to
the DictUpdate variant explaining that the index field is a 0-based offset to
the dict on the stack before popping the source mapping, clarifying the
semantics as described. Also note in the doc that legacy bytecode without the
index field decodes with index == 0 and mention the need to handle compatibility
accordingly. Then, add unit tests in the bytecode test module to assert that
Instruction::DictUpdate { index } has a stack_effect() of -1 and that fmt_dis()
outputs the expected string. Finally, add a marshaling test that serializes an
old-format DictUpdate (without the index argument) and verifies it decodes to
DictUpdate { index: 0 }.

BuildSlice {
/// whether build a slice with a third step argument
step: Arg<bool>,
Expand Down Expand Up @@ -1397,7 +1399,7 @@ impl Instruction {
let nargs = size.get(arg);
-(nargs as i32) + 1
}
DictUpdate => -1,
DictUpdate { .. } => -1,
BuildSlice { step } => -2 - (step.get(arg) as i32) + 1,
ListAppend { .. } | SetAdd { .. } => -1,
MapAdd { .. } => -2,
Expand Down Expand Up @@ -1586,7 +1588,7 @@ impl Instruction {
BuildSetFromTuples { size } => w!(BuildSetFromTuples, size),
BuildMap { size } => w!(BuildMap, size),
BuildMapForCall { size } => w!(BuildMapForCall, size),
DictUpdate => w!(DictUpdate),
DictUpdate { index } => w!(DictUpdate, index),
BuildSlice { step } => w!(BuildSlice, step),
ListAppend { i } => w!(ListAppend, i),
SetAdd { i } => w!(SetAdd, i),
Expand Down
32 changes: 23 additions & 9 deletions vm/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,26 +800,40 @@ impl ExecutingFrame<'_> {
bytecode::Instruction::BuildMapForCall { size } => {
self.execute_build_map_for_call(vm, size.get(arg))
}
bytecode::Instruction::DictUpdate => {
let other = self.pop_value();
let dict = self
.top_value()
.downcast_ref::<PyDict>()
.expect("exact dict expected");
bytecode::Instruction::DictUpdate { index } => {
// Stack before: [..., dict, ..., source] (source at TOS)
// Stack after: [..., dict, ...] (source consumed)
// The dict to update is at position TOS-i (before popping source)

let idx = index.get(arg);

// Pop the source from TOS
let source = self.pop_value();

// Get the dict to update (it's now at TOS-(i-1) after popping source)
let dict = if idx <= 1 {
// DICT_UPDATE 0 or 1: dict is at TOS (after popping source)
self.top_value()
} else {
// DICT_UPDATE n: dict is at TOS-(n-1)
self.nth_value(idx - 1)
};

let dict = dict.downcast_ref::<PyDict>().expect("exact dict expected");

// For dictionary unpacking {**x}, x must be a mapping
// Check if the object has the mapping protocol (keys method)
if vm
.get_method(other.clone(), vm.ctx.intern_str("keys"))
.get_method(source.clone(), vm.ctx.intern_str("keys"))
.is_none()
{
return Err(vm.new_type_error(format!(
"'{}' object is not a mapping",
other.class().name()
source.class().name()
)));
Comment on lines 826 to 833
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don't mask attribute lookup errors when checking mapping protocol

vm.get_method(...) returns Option<PyResult<_>>; using is_none() ignores Some(Err(e)) and turns it into a TypeError, hiding the real exception. Propagate lookup errors instead.

-                if vm
-                    .get_method(source.clone(), vm.ctx.intern_str("keys"))
-                    .is_none()
-                {
-                    return Err(vm.new_type_error(format!(
-                        "'{}' object is not a mapping",
-                        source.class().name()
-                    )));
-                }
+                match vm.get_method(source.clone(), vm.ctx.intern_str("keys")) {
+                    Some(Ok(_)) => {}
+                    Some(Err(e)) => return Err(e),
+                    None => {
+                        return Err(vm.new_type_error(format!(
+                            "'{}' object is not a mapping",
+                            source.class().name()
+                        )));
+                    }
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if vm
.get_method(other.clone(), vm.ctx.intern_str("keys"))
.get_method(source.clone(), vm.ctx.intern_str("keys"))
.is_none()
{
return Err(vm.new_type_error(format!(
"'{}' object is not a mapping",
other.class().name()
source.class().name()
)));
match vm.get_method(source.clone(), vm.ctx.intern_str("keys")) {
Some(Ok(_)) => {}
Some(Err(e)) => return Err(e),
None => {
return Err(vm.new_type_error(format!(
"'{}' object is not a mapping",
source.class().name()
)));
}
}
🤖 Prompt for AI Agents
In vm/src/frame.rs around lines 826 to 833, the code uses is_none() on the
result of vm.get_method(...) which returns Option<PyResult<_>>, causing it to
ignore Some(Err(e)) and incorrectly convert it to a TypeError. To fix this,
handle the Option and PyResult separately: first check if the Option is None to
return the TypeError, but if it is Some, propagate any Err by returning it
instead of masking it.

}

dict.merge_object(other, vm)?;
dict.merge_object(source, vm)?;
Ok(None)
}
bytecode::Instruction::BuildSlice { step } => {
Expand Down
Loading