Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix AST field defaults and compile() type check
- Extract empty_arguments_object helper from expression.rs
- Fix LIST_FIELDS ambiguity: "args" and "body" have different
  ASDL types per node (e.g. Lambda.args is `arguments`, not
  `expr*`; Lambda.body is `expr`, not `stmt*`)
- Replace class name string comparison in compile() with
  fast_isinstance to accept AST subclasses
  • Loading branch information
youknowone committed Feb 3, 2026
commit 00cdb307e39ad8c852d6d70fe28dbec679757cf7
1 change: 0 additions & 1 deletion Lib/test/test_genericclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ def __class_getitem__(cls, one, two):
with self.assertRaises(TypeError):
C_too_many[int]

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_class_getitem_errors_2(self):
class C:
def __class_getitem__(cls, item):
Expand Down
1 change: 1 addition & 0 deletions crates/vm/src/protocol/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ impl PyObject {

if let Some(class_getitem) =
vm.get_attribute_opt(self.to_owned(), identifier!(vm, __class_getitem__))?
&& !vm.is_none(&class_getitem)
{
return class_getitem.call((needle,), vm);
}
Expand Down
36 changes: 36 additions & 0 deletions crates/vm/src/stdlib/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,42 @@ fn node_add_location(
.unwrap();
}

/// Return the expected AST mod type class for a compile() mode string.
pub(crate) fn mode_type_and_name(
ctx: &Context,
mode: &str,
) -> Option<(PyRef<PyType>, &'static str)> {
match mode {
"exec" => Some((pyast::NodeModModule::make_class(ctx), "Module")),
"eval" => Some((pyast::NodeModExpression::make_class(ctx), "Expression")),
"single" => Some((pyast::NodeModInteractive::make_class(ctx), "Interactive")),
"func_type" => Some((pyast::NodeModFunctionType::make_class(ctx), "FunctionType")),
_ => None,
}
}

/// Create an empty `arguments` AST node (no parameters).
fn empty_arguments_object(vm: &VirtualMachine) -> PyObjectRef {
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
for list_field in [
"posonlyargs",
"args",
"kwonlyargs",
"kw_defaults",
"defaults",
] {
dict.set_item(list_field, vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
}
for none_field in ["vararg", "kwarg"] {
dict.set_item(none_field, vm.ctx.none(), vm).unwrap();
}
node.into()
}

#[cfg(feature = "parser")]
pub(crate) fn parse(
vm: &VirtualMachine,
Expand Down
26 changes: 1 addition & 25 deletions crates/vm/src/stdlib/ast/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,31 +330,7 @@ impl Node for ast::ExprLambda {
// Lambda with no parameters should have an empty arguments object, not None
let args = match parameters {
Some(params) => params.ast_to_object(vm, source_file),
None => {
// Create an empty arguments object
let args_node = NodeAst
.into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned())
.unwrap();
let args_dict = args_node.as_object().dict().unwrap();
args_dict
.set_item("posonlyargs", vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
args_dict
.set_item("args", vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
args_dict.set_item("vararg", vm.ctx.none(), vm).unwrap();
args_dict
.set_item("kwonlyargs", vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
args_dict
.set_item("kw_defaults", vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
args_dict.set_item("kwarg", vm.ctx.none(), vm).unwrap();
args_dict
.set_item("defaults", vm.ctx.new_list(vec![]).into(), vm)
.unwrap();
args_node.into()
}
None => empty_arguments_object(vm),
};
dict.set_item("args", args, vm).unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
Expand Down
31 changes: 24 additions & 7 deletions crates/vm/src/stdlib/ast/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,11 @@ pub(crate) mod _ast {
.is_some();
if has_field_types {
// ASDL list fields (type*) default to empty list,
// optional fields (type?) default to None.
// optional/required fields default to None.
// Fields that are always list-typed regardless of node class.
const LIST_FIELDS: &[&str] = &[
"args",
"argtypes",
"bases",
"body",
"cases",
"comparators",
"decorator_list",
Expand All @@ -113,31 +112,49 @@ pub(crate) mod _ast {
"items",
"keys",
"kw_defaults",
"kwd_attrs",
"kwd_patterns",
"keywords",
"kwonlyargs",
"names",
"orelse",
"ops",
"patterns",
"posonlyargs",
"targets",
"type_ignores",
"type_params",
"values",
];

let class_name = zelf.class().name().to_string();

for field in &fields {
if !set_fields.contains(field.as_str()) {
let default: PyObjectRef = if LIST_FIELDS.contains(&field.as_str()) {
let field_name = field.as_str();
// Some field names have different ASDL types depending on the node.
// For example, "args" is `expr*` in Call but `arguments` in Lambda.
// "body" and "orelse" are `stmt*` in most nodes but `expr` in IfExp.
let is_list_field = if field_name == "args" {
class_name == "Call" || class_name == "arguments"
} else if field_name == "body" || field_name == "orelse" {
!matches!(
class_name.as_str(),
"Lambda" | "Expression" | "IfExp"
)
} else {
LIST_FIELDS.contains(&field_name)
};

let default: PyObjectRef = if is_list_field {
vm.ctx.new_list(vec![]).into()
} else {
vm.ctx.none()
};
zelf.set_attr(vm.ctx.intern_str(field.as_str()), default, vm)?;
zelf.set_attr(vm.ctx.intern_str(field_name), default, vm)?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment on lines +129 to +151
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd crates/vm && head -200 src/stdlib/ast/python.rs | tail -100

Repository: RustPython/RustPython

Length of output: 3868


🏁 Script executed:

sed -n '100,160p' crates/vm/src/stdlib/ast/python.rs

Repository: RustPython/RustPython

Length of output: 2601


🏁 Script executed:

# Search for fast_issubclass usage in the codebase to understand the pattern
rg "fast_issubclass" crates/vm/src/stdlib/ast/ -A 2 -B 2

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

# Look for NodeExprCall, NodeArguments, NodeExprLambda types in the codebase
rg "NodeExprCall|NodeArguments|NodeExprLambda|NodeModExpression|NodeExprIfExp" crates/vm/src/stdlib/ast/

Repository: RustPython/RustPython

Length of output: 1907


🏁 Script executed:

# Check what node types are available in the pyast module
fd "pyast" crates/vm/src/stdlib/ast/ && ls -la crates/vm/src/stdlib/ast/pyast* 2>/dev/null || find crates/vm/src/stdlib/ast -name "*pyast*" -type f

Repository: RustPython/RustPython

Length of output: 182


🏁 Script executed:

rg "fast_issubclass" crates/vm --type rust

Repository: RustPython/RustPython

Length of output: 6337


🏁 Script executed:

# Search for issubclass patterns in the codebase
rg "\.is\(.*::static_type" crates/vm/src/stdlib/ast/ -A 1 -B 1

Repository: RustPython/RustPython

Length of output: 25507


🏁 Script executed:

# Check what methods are available on PyClass for subclass checking
rg "impl.*PyClass" crates/vm/src/builtins/ -A 10 | head -50

Repository: RustPython/RustPython

Length of output: 3469


🏁 Script executed:

# Look for subclass checking methods in the PyClass implementation
fd "class.rs" crates/vm/src && head -100 "$(fd 'class.rs' crates/vm/src)"

Repository: RustPython/RustPython

Length of output: 3451


🏁 Script executed:

# Search for how .is() method is used on PyClass
rg "\.is\(" crates/vm/src/stdlib/ast/python.rs -B 2 -A 2

Repository: RustPython/RustPython

Length of output: 47


Use subclass checking instead of exact class name matching for field defaults.

When _field_types is inherited by subclasses, string comparison like class_name == "Call" misclassifies subclasses and defaults fields incorrectly. Use fast_issubclass against the base node types so subclasses follow the same defaults as their parents.

🛠️ Suggested fix
-                let class_name = zelf.class().name().to_string();
+                let class = zelf.class();
+                let class_name = class.name().to_string();
 ...
-                        let is_list_field = if field_name == "args" {
-                            class_name == "Call" || class_name == "arguments"
-                        } else if field_name == "body" || field_name == "orelse" {
-                            !matches!(
-                                class_name.as_str(),
-                                "Lambda" | "Expression" | "IfExp"
-                            )
-                        } else {
-                            LIST_FIELDS.contains(&field_name)
-                        };
+                        let is_list_field = if field_name == "args" {
+                            class.fast_issubclass(super::pyast::NodeExprCall::static_type())
+                                || class.fast_issubclass(super::pyast::NodeArguments::static_type())
+                        } else if field_name == "body" || field_name == "orelse" {
+                            !(class.fast_issubclass(super::pyast::NodeExprLambda::static_type())
+                                || class.fast_issubclass(super::pyast::NodeModExpression::static_type())
+                                || class.fast_issubclass(super::pyast::NodeExprIfExp::static_type()))
+                        } else {
+                            LIST_FIELDS.contains(&field_name)
+                        };
🤖 Prompt for AI Agents
In `@crates/vm/src/stdlib/ast/python.rs` around lines 129 - 154, The code
incorrectly uses exact class name string comparisons (class_name == "Call",
"Lambda", "Expression", "IfExp") to decide list defaults, which misclassifies
subclasses; change those comparisons to subclass checks using
vm.ctx.fast_issubclass against the canonical AST base classes so subclasses
inherit the same defaults. Specifically, in the is_list_field logic replace
checks like class_name == "Call" or class_name == "arguments" and the
matches!(...) for "Lambda"|"Expression"|"IfExp" with calls to
vm.ctx.fast_issubclass(zelf.class(), <Call_class_obj>) etc.; obtain the base
class objects once (e.g., look up the Call, Lambda, Expression, IfExp class
objects from the VM context) and use those in fast_issubclass checks, keeping
LIST_FIELDS and the rest of the default/list logic intact and falling back to
the string-based behavior only if the type lookup fails.

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Special defaults that are not None or empty list
let class_name = &*zelf.class().name();
if class_name == "ImportFrom" && !set_fields.contains("level") {
zelf.set_attr("level", vm.ctx.new_int(0), vm)?;
}
Expand Down
40 changes: 24 additions & 16 deletions crates/vm/src/stdlib/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,24 +142,32 @@ mod builtins {
.fast_isinstance(&ast::NodeAst::make_class(&vm.ctx))
{
use num_traits::Zero;
let flags = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?;
let flags: i32 = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?;
let is_ast_only = !(flags & ast::PY_COMPILE_FLAG_AST_ONLY).is_zero();

// func_type mode requires PyCF_ONLY_AST
if mode_str == "func_type" && !is_ast_only {
return Err(vm.new_value_error(
"compile() mode 'func_type' requires flag PyCF_ONLY_AST".to_owned(),
));
}

// compile(ast_node, ..., PyCF_ONLY_AST) returns the AST after validation
if !(flags & ast::PY_COMPILE_FLAG_AST_ONLY).is_zero() {
let expected_type = match mode_str {
"exec" => "Module",
"eval" => "Expression",
"single" => "Interactive",
"func_type" => "FunctionType",
_ => {
return Err(vm.new_value_error(format!(
"compile() mode must be 'exec', 'eval', 'single' or 'func_type', got '{mode_str}'"
)));
}
};
let cls_name = args.source.class().name().to_string();
if cls_name != expected_type {
if is_ast_only {
let (expected_type, expected_name) = ast::mode_type_and_name(
&vm.ctx, mode_str,
)
.ok_or_else(|| {
vm.new_value_error(
"compile() mode must be 'exec', 'eval', 'single' or 'func_type'"
.to_owned(),
)
})?;
if !args.source.fast_isinstance(&expected_type) {
return Err(vm.new_type_error(format!(
"expected {expected_type} node, got {cls_name}"
"expected {} node, got {}",
expected_name,
args.source.class().name()
)));
}
return Ok(args.source);
Expand Down
Loading