Skip to content
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
71 changes: 53 additions & 18 deletions crates/vm/src/builtins/genericalias.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// spell-checker:ignore iparam gaiterobject
use crate::common::lock::LazyLock;
use crate::common::lock::{LazyLock, PyMutex};

use super::type_;
use crate::{
Expand Down Expand Up @@ -40,7 +40,7 @@ pub struct PyGenericAlias {
origin: PyObjectRef,
#[pymember(name = "__args__")]
args: PyTupleRef,
parameters: PyTupleRef,
parameters: PyMutex<Option<PyTupleRef>>,
#[pymember(name = "__unpacked__")]
starred: bool, // for __unpacked__ attribute
}
Expand Down Expand Up @@ -105,13 +105,12 @@ impl PyGenericAlias {
origin: impl Into<PyObjectRef>,
args: PyTupleRef,
starred: bool,
vm: &VirtualMachine,
_vm: &VirtualMachine,
) -> PyResult<Self> {
let parameters = make_parameters(&args, vm)?;
Ok(Self {
origin: origin.into(),
args,
parameters,
parameters: PyMutex::new(None),
starred,
})
}
Expand All @@ -130,6 +129,10 @@ impl PyGenericAlias {
Self::new(origin, args, false, vm)
}

fn parameters(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
cached_parameters(&self.parameters, &self.args, vm)
}

fn repr(&self, vm: &VirtualMachine) -> PyResult<String> {
fn repr_item(obj: &PyObject, vm: &VirtualMachine) -> PyResult<String> {
if obj.is(&vm.ctx.ellipsis) {
Expand Down Expand Up @@ -206,7 +209,8 @@ impl PyGenericAlias {
}

fn __getitem__(zelf: &Py<Self>, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let new_args = subs_parameters(zelf.as_object(), &zelf.args, &zelf.parameters, needle, vm)?;
let parameters = zelf.parameters(vm)?;
let new_args = subs_parameters(zelf.as_object(), &zelf.args, &parameters, needle, vm)?;

Ok(Self::new(zelf.origin.clone(), new_args, false, vm)?.into_pyobject(vm))
}
Expand Down Expand Up @@ -236,8 +240,8 @@ impl PyGenericAlias {
)]
impl Py<PyGenericAlias> {
#[pygetset]
fn __parameters__(&self) -> PyObjectRef {
self.parameters.clone().into()
fn __parameters__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
self.parameters(vm)
}

#[pygetset]
Expand Down Expand Up @@ -312,11 +316,36 @@ impl Py<PyGenericAlias> {
}
}

pub(crate) fn cached_parameters(
cache: &PyMutex<Option<PyTupleRef>>,
args: &Py<PyTuple>,
vm: &VirtualMachine,
) -> PyResult<PyTupleRef> {
let cached = cache.lock().clone();
if let Some(parameters) = cached {
return Ok(parameters);
}
// Attribute lookup can re-enter this alias or union. Compute without the
// lock, then replace even a cache populated by the re-entrant lookup.
let parameters = make_parameters(args, vm);
let previous = core::mem::replace(&mut *cache.lock(), parameters.as_ref().ok().cloned());
// A failed outer lookup clears the cache populated by a nested lookup too.
// Cached parameters can own objects with Python finalizers.
drop(previous);
parameters
}

pub(crate) fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
make_parameters_from_slice(args.as_slice(), vm)
}

fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyTupleRef> {
vm.with_recursion(" in __parameter__ calculation", || {
make_parameters_inner(args, vm)
})
}

fn make_parameters_inner(args: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<PyTupleRef> {
let mut parameters: Vec<PyObjectRef> = Vec::with_capacity(args.len());

for arg in args {
Expand All @@ -326,16 +355,21 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyRe
}

// Check for __typing_subst__ attribute
if arg.get_attr(identifier!(vm, __typing_subst__), vm).is_ok() {
if vm
.get_attribute_opt(arg, identifier!(vm, __typing_subst__))?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip custom-metaclass classes before probing typing hooks

When an alias argument is a class with a custom metaclass, the exact arg.class().is(type_type) guard above does not recognize it as a type, so a metaclass that raises while resolving __typing_subst__ now has that exception propagated by this ?; for example, list[C].__parameters__ raises RuntimeError if type(C).__getattribute__ raises for that name. CPython skips every class object here and returns (), while the previous is_ok() path at least swallowed this exception, so use a PyType payload/subclass check before either hook lookup.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

.is_some()
{
if tuple_index(&parameters, arg).is_none() {
parameters.push(arg.clone());
}
} else if let Ok(subparams) = arg.get_attr(identifier!(vm, __parameters__), vm)
&& let Ok(sub_params) = subparams.try_to_ref::<PyTuple>(vm)
} else if let Some(subparams) =
vm.get_attribute_opt(arg, identifier!(vm, __parameters__))?
Comment on lines +365 to +366

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve laziness when an alias enters a union

The fresh path after the earlier cache fix is that PyUnion::from_components (crates/vm/src/builtins/union.rs:45-46) still calls this shared helper eagerly, so placing a newly created GenericAlias in alias | int reaches this lookup and materializes the alias cache before either __parameters__ property is requested. For example, create an alias around an argument exposing __parameters__, form the union, then add __typing_subst__; alias[int] now raises the stale-parameter TypeError, while a raising argument descriptor can make alias | int itself fail. Python 3.15 keeps union parameters lazy, so make the union cache lazy instead of forcing nested aliases during construction.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

{
for sub_param in sub_params {
if tuple_index(&parameters, sub_param).is_none() {
parameters.push(sub_param.clone());
if let Some(sub_params) = subparams.downcast_ref::<PyTuple>() {
for sub_param in sub_params {
if tuple_index(&parameters, sub_param).is_none() {
parameters.push(sub_param.clone());
}
}
}
} else if arg.try_to_ref::<PyTuple>(vm).is_ok() || arg.try_to_ref::<PyList>(vm).is_ok() {
Expand All @@ -346,9 +380,7 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyRe
let list = arg.downcast_ref::<PyList>().unwrap();
list.borrow_vec().to_vec()
};
let sub = vm.with_recursion(" while computing __parameters__", || {
make_parameters_from_slice(&items, vm)
})?;
let sub = make_parameters_from_slice(&items, vm)?;
for sub_param in sub.as_slice() {
if tuple_index(&parameters, sub_param).is_none() {
parameters.push(sub_param.clone());
Expand Down Expand Up @@ -558,7 +590,10 @@ pub(crate) fn subs_parameters(
if let Some(iparam) = tuple_index(parameters.as_slice(), arg) {
subst.call((arg_items[iparam].clone(),), vm)?
} else {
subs_tvars(arg.clone(), parameters, &arg_items, vm)?
return Err(vm.new_type_error(format!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer stale checks until the parameter cache is materialized

When __typing_subst__ is added before an alias's first parameter lookup, this return treats the parameter as stale even though Python 3.15 has not cached __parameters__ yet. For example, let late.__parameters__ == (T,), create GenericAlias(list, late), set late.__typing_subst__ = lambda x: x, and evaluate alias[int]; Python returns list[int], but PyGenericAlias::new eagerly saved (T,), so late misses tuple_index and this branch raises. Lazily materialize or track the parameter cache so the guard applies only after it was actually cached.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

"argument {} with __typing_subst__ was not found in __parameters__",
arg.repr(vm)?
)));
}
} else {
subs_tvars(arg.clone(), parameters, &arg_items, vm)?
Expand Down
35 changes: 12 additions & 23 deletions crates/vm/src/builtins/union.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{genericalias, type_};
use crate::common::lock::LazyLock;
use crate::common::lock::{LazyLock, PyMutex};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
atomic_func,
Expand All @@ -24,7 +24,7 @@ pub struct PyUnion {
hashable_args: Option<PyRef<PyFrozenSet>>,
/// Tuple of initially unhashable args, or None if all args were hashable
unhashable_args: Option<PyTupleRef>,
parameters: PyTupleRef,
parameters: PyMutex<Option<PyTupleRef>>,
}

impl fmt::Debug for PyUnion {
Expand All @@ -42,14 +42,13 @@ impl PyPayload for PyUnion {

impl PyUnion {
/// Create a new union from dedup result (internal use)
fn from_components(result: UnionComponents, vm: &VirtualMachine) -> PyResult<Self> {
let parameters = make_parameters(&result.args, vm)?;
Ok(Self {
fn from_components(result: UnionComponents) -> Self {
Self {
args: result.args,
hashable_args: result.hashable_args,
unhashable_args: result.unhashable_args,
parameters,
})
parameters: PyMutex::new(None),
}
}

/// Direct access to args field (_Py_union_args)
Expand Down Expand Up @@ -127,8 +126,8 @@ impl Py<PyUnion> {
}

#[pygetset]
fn __parameters__(&self) -> PyObjectRef {
self.parameters.clone().into()
fn __parameters__(&self, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
genericalias::cached_parameters(&self.parameters, &self.args, vm)
}

#[pymethod]
Expand Down Expand Up @@ -239,12 +238,6 @@ pub(crate) fn or_op(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine)
make_union(&tuple, vm)
}

fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
let parameters = genericalias::make_parameters(args, vm)?;
let result = dedup_and_flatten_args(&parameters, vm)?;
Ok(result.args)
}

fn flatten_args(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyTupleRef {
let mut total_args = 0;
for arg in args {
Expand Down Expand Up @@ -372,19 +365,15 @@ pub fn make_union(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult {
let result = dedup_and_flatten_args(args, vm)?;
Ok(match result.args.as_slice().len() {
1 => result.args.as_slice()[0].to_owned(),
_ => PyUnion::from_components(result, vm)?.to_pyobject(vm),
_ => PyUnion::from_components(result).to_pyobject(vm),
})
}

impl PyUnion {
fn getitem(zelf: &Py<Self>, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult {
let new_args = genericalias::subs_parameters(
zelf.as_object(),
&zelf.args,
&zelf.parameters,
needle,
vm,
)?;
let parameters = genericalias::cached_parameters(&zelf.parameters, &zelf.args, vm)?;
let new_args =
genericalias::subs_parameters(zelf.as_object(), &zelf.args, &parameters, needle, vm)?;

Ok(if new_args.as_slice().is_empty() {
make_union(&new_args, vm)?
Expand Down
Loading
Loading