diff --git a/src/python/pants/backend/python/util_rules/interpreter_constraints.py b/src/python/pants/backend/python/util_rules/interpreter_constraints.py index f12e6093e9f..80dc44b180e 100644 --- a/src/python/pants/backend/python/util_rules/interpreter_constraints.py +++ b/src/python/pants/backend/python/util_rules/interpreter_constraints.py @@ -89,14 +89,14 @@ def for_fixed_python_version( ) -> InterpreterConstraints: return cls([f"{interpreter_type}=={python_version_str}"]) - def __init__(self, constraints: Iterable[str | Requirement] = ()) -> None: + def __new__(cls, constraints: Iterable[str | Requirement] = ()) -> InterpreterConstraints: # #12578 `parse_constraint` will sort the requirement's component constraints into a stable form. # We need to sort the component constraints for each requirement _before_ sorting the entire list # for the ordering to be correct. parsed_constraints = ( i if isinstance(i, Requirement) else parse_constraint(i) for i in constraints ) - super().__init__(sorted(parsed_constraints, key=lambda c: str(c))) + return super().__new__(cls, sorted(parsed_constraints, key=lambda c: str(c))) def __str__(self) -> str: return " OR ".join(str(constraint) for constraint in self) diff --git a/src/python/pants/backend/python/util_rules/pex.py b/src/python/pants/backend/python/util_rules/pex.py index 0dc5f9580d5..69ebfe91173 100644 --- a/src/python/pants/backend/python/util_rules/pex.py +++ b/src/python/pants/backend/python/util_rules/pex.py @@ -138,7 +138,6 @@ class CompletePlatforms(DeduplicatedCollection[str]): sort_input = True def __init__(self, iterable: Iterable[str] = (), *, digest: Digest = EMPTY_DIGEST): - super().__init__(iterable) self._digest = digest @classmethod diff --git a/src/python/pants/engine/collection.py b/src/python/pants/engine/collection.py index cbcdddb9a0c..6dc7921c8dd 100644 --- a/src/python/pants/engine/collection.py +++ b/src/python/pants/engine/collection.py @@ -79,10 +79,11 @@ class Examples(DeduplicatedCollection[Example]): sort_input: ClassVar[bool] = False - def __init__(self, iterable: Iterable[T] = ()) -> None: - super().__init__( - iterable if not self.sort_input else sorted(iterable) # type: ignore[type-var] + def __new__(cls, iterable: Iterable[T] = (), **_kwargs: object) -> DeduplicatedCollection[T]: + return super().__new__( + cls, + iterable if not cls.sort_input else sorted(iterable), # type: ignore[type-var] ) def __repr__(self) -> str: - return f"{self.__class__.__name__}({list(self._items)})" + return f"{self.__class__.__name__}({list(self)})" diff --git a/src/python/pants/engine/internals/native_engine.pyi b/src/python/pants/engine/internals/native_engine.pyi index e2ec0bc3427..04d45629668 100644 --- a/src/python/pants/engine/internals/native_engine.pyi +++ b/src/python/pants/engine/internals/native_engine.pyi @@ -6,11 +6,11 @@ from __future__ import annotations -from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence from datetime import datetime from io import RawIOBase from pathlib import Path -from typing import Any, ClassVar, Protocol, Self, TextIO, TypeVar, overload +from typing import AbstractSet, Any, ClassVar, Protocol, Self, TextIO, TypeVar, overload from pants.engine.fs import ( CreateDigest, @@ -81,6 +81,35 @@ class FrozenDict(Mapping[K, V]): def __hash__(self) -> int: ... def __repr__(self) -> str: ... +T_co = TypeVar("T_co", covariant=True) + +class FrozenOrderedSet(AbstractSet[T_co], Hashable): + """A frozen (i.e. immutable) ordered set backed by Rust. + + This is safe to use with the V2 engine. + """ + + def __new__(cls, iterable: Iterable[T_co] | None = None) -> Self: ... + def __len__(self) -> int: ... + def __contains__(self, key: Any) -> bool: ... + def __iter__(self) -> Iterator[T_co]: ... + def __reversed__(self) -> Iterator[T_co]: ... + def __hash__(self) -> int: ... + def __eq__(self, other: Any) -> bool: ... + def __or__(self, other: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... # type: ignore[override] # widens from AbstractSet + def __and__(self, other: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def __sub__(self, other: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def __xor__(self, other: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... # type: ignore[override] # widens from AbstractSet + def __bool__(self) -> bool: ... + def __repr__(self) -> str: ... + def union(self, *others: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def intersection(self, *others: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def difference(self, *others: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def symmetric_difference(self, other: Iterable[T_co]) -> FrozenOrderedSet[T_co]: ... + def issubset(self, other: Iterable[T_co]) -> bool: ... + def issuperset(self, other: Iterable[T_co]) -> bool: ... + def isdisjoint(self, other: Iterable[T_co]) -> bool: ... + # ------------------------------------------------------------------------------ # Address # ------------------------------------------------------------------------------ diff --git a/src/python/pants/util/ordered_set.py b/src/python/pants/util/ordered_set.py index b2421addb8f..6943c7cffc7 100644 --- a/src/python/pants/util/ordered_set.py +++ b/src/python/pants/util/ordered_set.py @@ -15,9 +15,11 @@ from __future__ import annotations import itertools -from collections.abc import Hashable, Iterable, Iterator, MutableSet +from collections.abc import Iterable, Iterator, MutableSet from typing import AbstractSet, Any, TypeVar, cast +from pants.engine.internals.native_engine import FrozenOrderedSet as FrozenOrderedSet # noqa: F401 + T = TypeVar("T") T_co = TypeVar("T_co", covariant=True) _TAbstractOrderedSet = TypeVar("_TAbstractOrderedSet", bound="_AbstractOrderedSet") @@ -195,21 +197,3 @@ def symmetric_difference_update(self, other: Iterable[T]) -> None: self._items = {item: None for item in self._items.keys() if item not in items_to_remove} for item in items_to_add: self._items[item] = None - - -class FrozenOrderedSet(_AbstractOrderedSet[T_co], Hashable): # type: ignore[type-var] - """A frozen (i.e. immutable) set that retains its order. - - This is safe to use with the V2 engine. - """ - - def __init__(self, iterable: Iterable[T_co] | None = None) -> None: - super().__init__(iterable) - self.__hash: int | None = None - - def __hash__(self) -> int: - if self.__hash is None: - self.__hash = 0 - for item in self._items.keys(): - self.__hash ^= hash(item) - return self.__hash diff --git a/src/rust/engine/src/externs/collection.rs b/src/rust/engine/src/externs/collection.rs new file mode 100644 index 00000000000..27b642db1c8 --- /dev/null +++ b/src/rust/engine/src/externs/collection.rs @@ -0,0 +1,119 @@ +// Copyright 2026 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +use std::fmt::Debug; +use std::sync::OnceLock; + +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyIterator}; + +pub trait HashCache: Debug + Send + Sync { + fn new_eager(hash: isize) -> Self; + fn new_lazy() -> Self; + fn get( + &self, + dict: &Bound, + compute: fn(&Bound) -> PyResult, + ) -> PyResult; +} + +#[derive(Debug)] +pub struct EagerHash(isize); + +impl HashCache for EagerHash { + fn new_eager(hash: isize) -> Self { + Self(hash) + } + fn new_lazy() -> Self { + panic!("EagerHash requires a value at construction") + } + fn get( + &self, + _dict: &Bound, + _compute: fn(&Bound) -> PyResult, + ) -> PyResult { + Ok(self.0) + } +} + +#[derive(Debug)] +pub struct LazyHash(OnceLock); + +impl HashCache for LazyHash { + fn new_eager(hash: isize) -> Self { + let lock = OnceLock::new(); + let _ = lock.set(hash); + Self(lock) + } + fn new_lazy() -> Self { + Self(OnceLock::new()) + } + fn get( + &self, + dict: &Bound, + compute: fn(&Bound) -> PyResult, + ) -> PyResult { + if let Some(&h) = self.0.get() { + return Ok(h); + } + let h = compute(dict)?; + let _ = self.0.set(h); + Ok(h) + } +} + +#[derive(Debug)] +pub struct FrozenCollectionData { + pub data: Py, + hash: H, +} + +impl FrozenCollectionData { + pub fn new(dict: Bound, hash: isize) -> Self { + Self { + data: dict.unbind(), + hash: H::new_eager(hash), + } + } + + pub fn new_lazy(dict: Bound) -> Self { + Self { + data: dict.unbind(), + hash: H::new_lazy(), + } + } + + pub fn get_hash( + &self, + py: Python, + compute: fn(&Bound) -> PyResult, + ) -> PyResult { + self.hash.get(&self.data.bind_borrowed(py), compute) + } + + pub fn len(&self, py: Python) -> usize { + self.data.bind_borrowed(py).len() + } + + pub fn contains(&self, key: &Bound) -> PyResult { + self.data.bind_borrowed(key.py()).contains(key) + } + + pub fn iter<'py>(&self, py: Python<'py>) -> PyResult> { + self.data.as_any().bind_borrowed(py).try_iter() + } + + pub fn reversed<'py>(&self, py: Python<'py>) -> PyResult> { + let keys = self.data.bind_borrowed(py).keys(); + keys.reverse()?; + keys.try_iter() + } +} + +pub fn xor_hash_keys(dict: &Bound) -> PyResult { + let mut h: isize = 0; + for key in dict.keys() { + h ^= key.hash()?; + } + Ok(h) +} diff --git a/src/rust/engine/src/externs/frozen_ordered_set.rs b/src/rust/engine/src/externs/frozen_ordered_set.rs new file mode 100644 index 00000000000..2129ee7d933 --- /dev/null +++ b/src/rust/engine/src/externs/frozen_ordered_set.rs @@ -0,0 +1,338 @@ +// Copyright 2026 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyIterator, PyList, PyNotImplemented, PySet, PyTuple}; +use pyo3::{IntoPyObjectExt, PyTypeInfo}; + +use super::collection::{self, FrozenCollectionData, LazyHash}; + +#[pyclass( + subclass, + frozen, + sequence, + generic, + module = "pants.engine.internals.native_engine" +)] +#[derive(Debug)] +pub struct FrozenOrderedSet { + inner: FrozenCollectionData, +} + +#[pymethods] +impl FrozenOrderedSet { + #[new] + #[pyo3(signature = (iterable=None))] + fn __new__(iterable: Option<&Bound>) -> PyResult { + Self::from_iterable(iterable) + } + + fn __len__(slf: PyRef) -> usize { + slf.inner.len(slf.py()) + } + + fn __contains__(&self, key: &Bound) -> PyResult { + self.inner.contains(key) + } + + fn __iter__<'py>(slf: &Bound<'py, Self>) -> PyResult> { + slf.get().inner.iter(slf.py()) + } + + fn __reversed__<'py>(slf: &Bound<'py, Self>) -> PyResult> { + slf.get().inner.reversed(slf.py()) + } + + fn __repr__(slf: &Bound) -> PyResult { + let py = slf.py(); + let name = slf.get_type().qualname()?; + let data = slf.get().inner.data.bind_borrowed(py); + if data.is_empty() { + return Ok(format!("{name}()")); + } + let items: Vec<_> = data + .keys() + .into_iter() + .map(|k| k.repr().map(|r| r.to_string())) + .collect::>()?; + Ok(format!("{name}([{}])", items.join(", "))) + } + + fn __hash__(&self, py: Python) -> PyResult { + self.inner.get_hash(py, collection::xor_hash_keys) + } + + fn __eq__<'py>( + self_: &Bound<'py, Self>, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = other.py(); + if !other.is_instance(&self_.get_type())? { + return PyNotImplemented::get(py).into_bound_py_any(py); + } + let this = self_.get(); + let other_fos = other.cast::()?; + let self_dict = this.inner.data.bind_borrowed(py); + let other_dict = other_fos.get().inner.data.bind_borrowed(py); + + if self_dict.len() != other_dict.len() { + return false.into_bound_py_any(py); + } + for (a, b) in self_dict + .keys() + .into_iter() + .zip(other_dict.keys().into_iter()) + { + if !a.eq(&b)? { + return false.into_bound_py_any(py); + } + } + true.into_bound_py_any(py) + } + + fn __or__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let merged = self.inner.data.bind_borrowed(py).copy()?; + for item in other.try_iter()? { + merged.set_item(item?, py.None())?; + } + Self::from_pydict(merged)?.into_bound_py_any(py) + } + + #[pyo3(signature = (*others))] + fn union<'py>(&self, others: &Bound<'py, PyTuple>) -> PyResult> { + let py = others.py(); + let merged = self.inner.data.bind_borrowed(py).copy()?; + for other in others.iter() { + for item in other.try_iter()? { + merged.set_item(item?, py.None())?; + } + } + Self::from_pydict(merged)?.into_bound_py_any(py) + } + + fn __and__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let other_set = to_pyset(&other)?; + filter_keys(self, py, |key| other_set.contains(key)) + } + + #[pyo3(signature = (*others))] + fn intersection<'py>(&self, others: &Bound<'py, PyTuple>) -> PyResult> { + let py = others.py(); + if others.is_empty() { + return Self::from_pydict(self.inner.data.bind_borrowed(py).copy()?)? + .into_bound_py_any(py); + } + let sets = others + .iter() + .map(|o| to_pyset(&o)) + .collect::>>()?; + filter_keys(self, py, |key| { + Ok(sets.iter().all(|s| s.contains(key).unwrap_or(false))) + }) + } + + fn __sub__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let other_set = to_pyset(&other)?; + filter_keys(self, py, |key| other_set.contains(key).map(|b| !b)) + } + + #[pyo3(signature = (*others))] + fn difference<'py>(&self, others: &Bound<'py, PyTuple>) -> PyResult> { + let py = others.py(); + if others.is_empty() { + return Self::from_pydict(self.inner.data.bind_borrowed(py).copy()?)? + .into_bound_py_any(py); + } + let excluded = PySet::empty(py)?; + for other in others.iter() { + for item in other.try_iter()? { + excluded.add(item?)?; + } + } + filter_keys(self, py, |key| excluded.contains(key).map(|b| !b)) + } + + fn __xor__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + self.symmetric_difference(other) + } + + fn symmetric_difference<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let self_dict = self.inner.data.bind_borrowed(py); + let other_dict = iter_to_pydict(&other)?; + + let result = PyDict::new(py); + for key in self_dict.keys() { + if !other_dict.contains(&key)? { + result.set_item(&key, py.None())?; + } + } + for key in other_dict.keys() { + if !self_dict.contains(&key)? { + result.set_item(&key, py.None())?; + } + } + Self::from_pydict(result)?.into_bound_py_any(py) + } + + fn __lt__(&self, other: &Bound) -> PyResult { + Ok(self.issubset(other)? && self.inner.len(other.py()) != other.len()?) + } + + fn __le__(&self, other: &Bound) -> PyResult { + self.issubset(other) + } + + fn __gt__(&self, other: &Bound) -> PyResult { + Ok(self.issuperset(other)? && self.inner.len(other.py()) != other.len()?) + } + + fn __ge__(&self, other: &Bound) -> PyResult { + self.issuperset(other) + } + + fn __rand__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + self.__and__(other) + } + + fn __ror__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let merged = iter_to_pydict(&other)?; + for key in self.inner.data.bind_borrowed(py).keys() { + merged.set_item(&key, py.None())?; + } + Self::from_pydict(merged)?.into_bound_py_any(py) + } + + fn __rsub__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let self_dict = self.inner.data.bind_borrowed(py); + let result = PyDict::new(py); + for item in other.try_iter()? { + let item = item?; + if !self_dict.contains(&item)? { + result.set_item(&item, py.None())?; + } + } + Self::from_pydict(result)?.into_bound_py_any(py) + } + + fn __rxor__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { + self.__xor__(other) + } + + fn issubset(&self, other: &Bound) -> PyResult { + let py = other.py(); + let self_dict = self.inner.data.bind_borrowed(py); + if let Ok(other_fos) = other.cast::() { + let other_dict = other_fos.get().inner.data.bind_borrowed(py); + if self_dict.len() > other_dict.len() { + return Ok(false); + } + for key in self_dict.keys() { + if !other_dict.contains(&key)? { + return Ok(false); + } + } + } else { + let other_set = to_pyset(other)?; + for key in self_dict.keys() { + if !other_set.contains(&key)? { + return Ok(false); + } + } + } + Ok(true) + } + + fn issuperset(&self, other: &Bound) -> PyResult { + let py = other.py(); + let self_dict = self.inner.data.bind_borrowed(py); + for item in other.try_iter()? { + if !self_dict.contains(&item?)? { + return Ok(false); + } + } + Ok(true) + } + + fn isdisjoint(&self, other: &Bound) -> PyResult { + let py = other.py(); + let self_dict = self.inner.data.bind_borrowed(py); + for item in other.try_iter()? { + if self_dict.contains(&item?)? { + return Ok(false); + } + } + Ok(true) + } + + fn __bool__(slf: PyRef) -> bool { + !slf.inner.data.bind_borrowed(slf.py()).is_empty() + } + + pub fn __getnewargs__<'py>(slf: &Bound<'py, Self>) -> PyResult<(Bound<'py, PyList>,)> { + let py = slf.py(); + let keys = slf.get().inner.data.bind_borrowed(py).keys(); + Ok((keys,)) + } +} + +fn iter_to_pydict<'py>(iterable: &Bound<'py, PyAny>) -> PyResult> { + let py = iterable.py(); + let dict = PyDict::new(py); + for item in iterable.try_iter()? { + dict.set_item(item?, py.None())?; + } + Ok(dict) +} + +fn to_pyset<'py>(iterable: &Bound<'py, PyAny>) -> PyResult> { + let py = iterable.py(); + let set = PySet::empty(py)?; + for item in iterable.try_iter()? { + set.add(item?)?; + } + Ok(set) +} + +fn filter_keys<'py>( + fos: &FrozenOrderedSet, + py: Python<'py>, + pred: impl Fn(&Bound<'py, PyAny>) -> PyResult, +) -> PyResult> { + let result = PyDict::new(py); + for key in fos.inner.data.bind_borrowed(py).keys() { + if pred(&key)? { + result.set_item(&key, py.None())?; + } + } + FrozenOrderedSet::from_pydict(result)?.into_bound_py_any(py) +} + +impl FrozenOrderedSet { + fn from_iterable(iterable: Option<&Bound>) -> PyResult { + let Some(iterable) = iterable else { + return Python::attach(|py| Self::from_pydict(PyDict::new(py))); + }; + Self::from_pydict(iter_to_pydict(iterable)?) + } + + fn from_pydict(dict: Bound) -> PyResult { + Ok(Self { + inner: FrozenCollectionData::new_lazy(dict), + }) + } +} + +pub fn register(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + let abc = py.import("collections.abc")?; + let set_abc = abc.getattr("Set")?; + set_abc.call_method1("register", (FrozenOrderedSet::type_object(py),))?; + let hashable_abc = abc.getattr("Hashable")?; + hashable_abc.call_method1("register", (FrozenOrderedSet::type_object(py),))?; + Ok(()) +} diff --git a/src/rust/engine/src/externs/frozendict.rs b/src/rust/engine/src/externs/frozendict.rs index ede27802eb6..dcec70684d2 100644 --- a/src/rust/engine/src/externs/frozendict.rs +++ b/src/rust/engine/src/externs/frozendict.rs @@ -9,11 +9,12 @@ use pyo3::types::{ PyDict, PyIterator, PyList, PyMapping, PyNotImplemented, PySet, PyTuple, PyType, }; +use super::collection::FrozenCollectionData; + #[pyclass(subclass, frozen, mapping, generic)] #[derive(Debug)] pub struct FrozenDict { - data: Py, - hash: isize, + inner: FrozenCollectionData, } #[pymethods] @@ -97,31 +98,28 @@ impl FrozenDict { fn __repr__(slf: &Bound) -> PyResult { Ok(format!( "FrozenDict({})", - slf.get().data.bind_borrowed(slf.py()).repr()? + slf.get().inner.data.bind_borrowed(slf.py()).repr()? )) } - fn __hash__(&self) -> isize { - self.hash + fn __hash__(&self, py: Python) -> PyResult { + self.inner.get_hash(py, compute_frozendict_hash) } fn __iter__<'py>(slf: &Bound<'py, Self>) -> PyResult> { - slf.get().data.as_any().bind_borrowed(slf.py()).try_iter() + slf.get().inner.iter(slf.py()) } fn __eq__(&self, other: &Bound) -> PyResult { - self.data.bind_borrowed(other.py()).eq(other) + self.inner.data.bind_borrowed(other.py()).eq(other) } fn __len__(slf: PyRef) -> usize { - slf.data.bind_borrowed(slf.py()).len() + slf.inner.len(slf.py()) } fn __reversed__<'py>(slf: &Bound<'py, Self>) -> PyResult> { - let py = slf.py(); - let keys = slf.get().data.bind_borrowed(py).keys(); - keys.reverse()?; - keys.try_iter() + slf.get().inner.reversed(slf.py()) } fn __lt__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult { @@ -131,9 +129,9 @@ impl FrozenDict { )); } let py = other.py(); - let self_dict = self.data.bind_borrowed(py); + let self_dict = self.inner.data.bind_borrowed(py); let other_fd = other.cast::()?; - let other_dict = other_fd.get().data.bind_borrowed(py); + let other_dict = other_fd.get().inner.data.bind_borrowed(py); let self_len = self_dict.len(); let other_len = other_dict.len(); @@ -161,13 +159,13 @@ impl FrozenDict { } fn __contains__(&self, input: &Bound) -> PyResult { - self.data.bind_borrowed(input.py()).contains(input) + self.inner.contains(input) } fn __or__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { let py = other.py(); if let Some(other) = get_inner_or_mapping(&other)? { - let py_args = PyTuple::new(py, [self.data.bind_borrowed(py).bitor(other)?])?; + let py_args = PyTuple::new(py, [self.inner.data.bind_borrowed(py).bitor(other)?])?; Self::from_pyargs(&py_args, None)?.into_bound_py_any(py) } else { Ok(PyNotImplemented::get(py).into_bound_py_any(py)?) @@ -177,7 +175,7 @@ impl FrozenDict { fn __ror__<'py>(&self, other: Bound<'py, PyAny>) -> PyResult> { let py = other.py(); if let Some(other) = get_inner_or_mapping(&other)? { - let py_args = PyTuple::new(py, [other.bitor(self.data.bind_borrowed(py))?])?; + let py_args = PyTuple::new(py, [other.bitor(self.inner.data.bind_borrowed(py))?])?; Self::from_pyargs(&py_args, None)?.into_bound_py_any(py) } else { @@ -186,22 +184,22 @@ impl FrozenDict { } fn keys<'py>(slf: &Bound<'py, Self>) -> PyResult> { - let dict = slf.get().data.bind_borrowed(slf.py()); + let dict = slf.get().inner.data.bind_borrowed(slf.py()); dict.call_method0(intern!(slf.py(), "keys")) } fn values<'py>(slf: &Bound<'py, Self>) -> PyResult> { - let dict = slf.get().data.bind_borrowed(slf.py()); + let dict = slf.get().inner.data.bind_borrowed(slf.py()); dict.call_method0(intern!(slf.py(), "values")) } fn items<'py>(slf: &Bound<'py, Self>) -> PyResult> { - let dict = slf.get().data.bind_borrowed(slf.py()); + let dict = slf.get().inner.data.bind_borrowed(slf.py()); dict.call_method0(intern!(slf.py(), "items")) } pub fn __getnewargs__(&self) -> PyResult<(&Py,)> { - Ok((&self.data,)) + Ok((&self.inner.data,)) } } @@ -211,7 +209,7 @@ fn get_inner_or_mapping<'a, 'py>( let py = obj.py(); Ok(if obj.is_instance_of::() { let fd: &Bound = obj.cast::()?; - Some(fd.get().data.bind(py).cast::()?) + Some(fd.get().inner.data.bind(py).cast::()?) } else { obj.cast::().ok() }) @@ -296,15 +294,13 @@ impl FrozenDict { fn from_pydict(dict: Bound) -> PyResult { let hash = compute_frozendict_hash(&dict)?; - Ok(Self { - data: dict.unbind(), - hash, + inner: FrozenCollectionData::new(dict, hash), }) } pub fn iter(slf: Bound) -> PyResult { - KeyValueIter::new(slf.get().data.bind_borrowed(slf.py()).as_mapping()) + KeyValueIter::new(slf.get().inner.data.bind_borrowed(slf.py()).as_mapping()) } fn inner_get<'a, 'py>( @@ -312,7 +308,8 @@ impl FrozenDict { input: Borrowed<'a, 'py, PyAny>, default: Option>, ) -> PyResult>> { - self.data + self.inner + .data .bind_borrowed(input.py()) .get_item(input) .transpose() @@ -321,7 +318,7 @@ impl FrozenDict { } } -/// Compute a commutative hash for a dictionary +/// Compute a commutative hash for a dictionary (XOR of hash((k, v)) pairs). fn compute_frozendict_hash(dict: &Bound) -> PyResult { let mut h: isize = 0; for (k, v) in dict { diff --git a/src/rust/engine/src/externs/interface.rs b/src/rust/engine/src/externs/interface.rs index 3aad2a26fbd..b72df39ea06 100644 --- a/src/rust/engine/src/externs/interface.rs +++ b/src/rust/engine/src/externs/interface.rs @@ -80,6 +80,7 @@ fn native_engine(py: Python, m: &Bound<'_, PyModule>) -> PyO3Result<()> { externs::dep_inference::register(m)?; externs::unions::register(py, m)?; externs::frozendict::register(py, m)?; + externs::frozen_ordered_set::register(py, m)?; m.add("PollTimeout", py.get_type::())?; diff --git a/src/rust/engine/src/externs/mod.rs b/src/rust/engine/src/externs/mod.rs index edaf8593e95..13fb43b8aff 100644 --- a/src/rust/engine/src/externs/mod.rs +++ b/src/rust/engine/src/externs/mod.rs @@ -27,8 +27,10 @@ use crate::interning::Interns; use crate::python::{Failure, Key, TypeId, Value}; mod address; +mod collection; pub mod dep_inference; pub mod engine_aware; +mod frozen_ordered_set; mod frozendict; pub mod fs; mod interface;