From c440ae77d4549cf0059f978ca44ab57fd36558d6 Mon Sep 17 00:00:00 2001 From: changjoon-park Date: Wed, 29 Apr 2026 16:55:12 +0900 Subject: [PATCH] Accept __index__-conforming objects for compile() flags / optimize CPython's compile() (Python/Python-ast.c) accepts any object with __index__ for the flags and optimize arguments. RustPython's CompileArgs typed both fields as OptionalArg, so a class with only __index__ raised 'TypeError: Expected type int but X found' during arg binding. bpo-36907's regression test (test_call.test_fastcall_clearing_dict) exercises exactly this: an IntWithDict.__index__ that mutates self.kwargs mid-call. CPython parses both args via __index__ and doesn't crash even when the kwargs dict is cleared between binding and use. Switch flags and optimize to OptionalArg>, the same helper already used by range, slice, bytes.__mul__, hex, oct, etc. ArgPrimitiveIndex calls try_index (= __index__ protocol) and converts to the requested primitive in one step, so the three call sites in compile() simplify from .map_or(Ok(d), |v| v.try_to_primitive(vm))? to .map_or(d, |v| v.value). Unmasks test_call.FastCallTests.test_fastcall_clearing_dict. --- Lib/test/test_call.py | 1 - crates/vm/src/stdlib/builtins.rs | 17 ++++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 86ba0aa4b63..7ea27929138 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -585,7 +585,6 @@ def test_vectorcall(self): result = _testcapi.pyobject_vectorcall(func, args, kwnames) self.check_result(result, expected) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'int' but 'IntWithDict' found. def test_fastcall_clearing_dict(self): # Test bpo-36907: the point of the test is just checking that this # does not crash. diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 008d342a79b..40f5ded5d44 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -21,8 +21,8 @@ mod builtins { common::hash::PyHash, function::{ ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping, - ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg, OptionalOption, - PosArgs, + ArgPrimitiveIndex, ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg, + OptionalOption, PosArgs, }, protocol::{PyIter, PyIterReturn}, py_io, @@ -99,12 +99,15 @@ mod builtins { source: PyObjectRef, filename: FsPath, mode: PyUtf8StrRef, + // CPython parity: flags / optimize accept any object with __index__, + // not just exact int. Matches the behavior of `int(x)` arg conversion + // used by Python/Python-ast.c::compile. #[pyarg(any, optional)] - flags: OptionalArg, + flags: OptionalArg>, #[pyarg(any, optional)] dont_inherit: OptionalArg, #[pyarg(any, optional)] - optimize: OptionalArg, + optimize: OptionalArg>, #[pyarg(any, optional)] _feature_version: OptionalArg, } @@ -264,7 +267,7 @@ mod builtins { let mode_str = args.mode.as_str(); - let optimize: i32 = args.optimize.map_or(Ok(-1), |v| v.try_to_primitive(vm))?; + let optimize: i32 = args.optimize.map_or(-1, |v| v.value); let optimize: u8 = if optimize == -1 { vm.state.config.settings.optimize } else { @@ -277,7 +280,7 @@ mod builtins { .source .fast_isinstance(&_ast::NodeAst::make_static_type()) { - let flags: i32 = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?; + let flags: i32 = args.flags.map_or(0, |v| v.value); let is_ast_only = !(flags & _ast::PY_CF_ONLY_AST).is_zero(); // func_type mode requires PyCF_ONLY_AST @@ -342,7 +345,7 @@ mod builtins { let source = decode_source_bytes(&source, &args.filename.to_string_lossy(), vm)?; let source = source.as_str(); - let flags = args.flags.map_or(Ok(0), |v| v.try_to_primitive(vm))?; + let flags: i32 = args.flags.map_or(0, |v| v.value); if !(flags & !_ast::PY_COMPILE_FLAGS_MASK).is_zero() { return Err(vm.new_value_error("compile() unrecognized flags"));