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
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2026, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or
* data (collectively the "Software"), free of charge and under any and all
* copyright rights in the Software, and any and all patent rights owned or
* freely licensable by each licensor hereunder covering either (i) the
* unmodified Software as contributed to or provided by such licensor, or (ii)
* the Larger Works (as defined below), to deal in both
*
* (a) the Software, and
*
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
*
* The above copyright notice and either this complete permission notice or at a
* minimum a reference to the UPL must be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oracle.graal.python.test.util;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.oracle.graal.python.util.LazyCyclicAssumption;
import com.oracle.truffle.api.Assumption;

public class LazyCyclicAssumptionTest {
@Test
public void testRenewalAndExhaustion() {
LazyCyclicAssumption cyclic = new LazyCyclicAssumption("test");
Assumption first = cyclic.getAssumption();
assertTrue(first.isValid());
assertSame(first, cyclic.getAssumption());
cyclic.invalidate(2);
assertFalse(first.isValid());
// Invalidations without an intervening get must not exhaust the remaining budget.
cyclic.invalidate(2);
cyclic.invalidate(2);
Assumption second = cyclic.getAssumption();
assertTrue(second.isValid());
assertNotSame(first, second);
cyclic.invalidate(2);
assertFalse(second.isValid());
assertSame(Assumption.NEVER_VALID, cyclic.getAssumption());
cyclic.invalidate(Integer.MAX_VALUE);
assertSame(Assumption.NEVER_VALID, cyclic.getAssumption());
}

@Test
public void testInvalidationBeforeFirstGet() {
LazyCyclicAssumption cyclic = new LazyCyclicAssumption("test");
cyclic.invalidate(2);
cyclic.invalidate(2);
Assumption first = cyclic.getAssumption();
assertTrue(first.isValid());
cyclic.invalidate(2);
assertFalse(first.isValid());
Assumption second = cyclic.getAssumption();
assertTrue(second.isValid());
cyclic.invalidate(2);
assertFalse(second.isValid());
assertSame(Assumption.NEVER_VALID, cyclic.getAssumption());
}

@Test
public void testChangingLimit() {
LazyCyclicAssumption cyclic = new LazyCyclicAssumption("test");
cyclic.invalidate(10);
Assumption first = cyclic.getAssumption();
cyclic.invalidate(3);
assertFalse(first.isValid());
assertTrue(cyclic.getAssumption().isValid());
cyclic.invalidate(2);
assertSame(Assumption.NEVER_VALID, cyclic.getAssumption());
}

@Test
public void testNonPositiveLimit() {
for (int limit : new int[]{0, -1, Integer.MIN_VALUE}) {
LazyCyclicAssumption cyclic = new LazyCyclicAssumption("test");
Assumption first = cyclic.getAssumption();
cyclic.invalidate(limit);
assertFalse(first.isValid());
assertSame(Assumption.NEVER_VALID, cyclic.getAssumption());
LazyCyclicAssumption unused = new LazyCyclicAssumption("unused");
unused.invalidate(limit);
assertSame(Assumption.NEVER_VALID, unused.getAssumption());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,24 @@ def do_lookup(obj):
assert bool(x) is True


def test_pytype_modified_on_python_type_invalidates_instance_attribute_fast_path():
class PythonType:
pass

x = PythonType()
x.value = "instance"

def do_lookup(obj):
return obj.value

for i in range(10):
assert do_lookup(x) == "instance"

TypeAttrHelper.changeAttr(PythonType, "value", property(lambda self: "descriptor"))
for i in range(10):
assert do_lookup(x) == "descriptor"


def test_pytype_modified_after_deleting_special_method_invalidates_slot_lookup():
class PythonBase:
def __len__(self):
Expand Down
195 changes: 195 additions & 0 deletions graalpython/com.oracle.graal.python.test/src/tests/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,201 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import gc
import warnings


def test_cached_instance_load_after_descriptor_added():
class C:
pass

def read(obj):
return obj.attr

obj = C()
obj.attr = "instance"
for _ in range(20):
assert read(obj) == "instance"

C.attr = property(lambda self: "descriptor")
assert read(obj) == "descriptor"


def test_cached_instance_int_load_after_inherited_descriptor_added():
class Base:
pass

class C(Base):
pass

def read(obj):
return obj.attr + 1

obj = C()
obj.attr = 41
for _ in range(20):
assert read(obj) == 42

Base.attr = property(lambda self: 99)
assert read(obj) == 100


def test_cached_instance_load_after_getattribute_added():
class C:
pass

def read(obj):
return obj.attr

obj = C()
obj.attr = "instance"
for _ in range(20):
assert read(obj) == "instance"

C.__getattribute__ = lambda self, name: "override"
assert read(obj) == "override"


def test_cached_instance_store_after_descriptor_replaced():
class C:
def attr(self):
pass

def write(obj, value):
obj.attr = value

obj = C()
for value in range(20):
write(obj, value)
assert obj.attr == 19

writes = []
C.attr = property(lambda self: "descriptor", lambda self, value: writes.append(value))
write(obj, 42)
assert writes == [42]
assert obj.__dict__["attr"] == 19


def test_cached_instance_store_after_inherited_setattr_added():
class Base:
pass

class C(Base):
pass

def write(obj, value):
obj.attr = value

obj = C()
for value in range(20):
write(obj, value)
assert obj.attr == 19

writes = []
Base.__setattr__ = lambda self, name, value: writes.append((name, value))
write(obj, 42)
assert writes == [("attr", 42)]
assert obj.__dict__["attr"] == 19


def _change_base_while_warming_attribute_access(new_base, warmup):
class Base:
pass

class C(Base):
pass

obj = C()
obj.attr = 41
armed = False

class Meta(type):
def mro(cls):
if armed:
# C's MRO has already changed and invalidated its lookup caches,
# but its slots still come from Base. Specializing here must not
# leave cached attribute accesses valid after the slots change.
warmup(obj)
return super().mro()

class Child(C, metaclass=Meta):
pass

armed = True
C.__bases__ = (new_base,)
return obj


def test_cached_instance_load_during_bases_change():
class Override:
def __getattribute__(self, name):
return 99

def read(obj):
return obj.attr

def warmup(obj):
for _ in range(20):
read(obj)

obj = _change_base_while_warming_attribute_access(Override, warmup)
assert getattr(obj, "attr") == 99
assert read(obj) == 99


def test_cached_instance_int_load_during_bases_change():
class Override:
def __getattribute__(self, name):
return 99

def read(obj):
return obj.attr + 1

def warmup(obj):
for _ in range(20):
read(obj)

obj = _change_base_while_warming_attribute_access(Override, warmup)
assert getattr(obj, "attr") == 99
assert read(obj) == 100


def test_cached_instance_store_during_bases_change():
writes = []

class Override:
def __setattr__(self, name, value):
writes.append((name, value))

def write(obj, value):
obj.attr = value

def warmup(obj):
for value in range(20):
write(obj, value)

obj = _change_base_while_warming_attribute_access(Override, warmup)
writes.clear()
value_before = obj.attr
write(obj, 42)
assert writes == [("attr", 42)]
assert obj.attr == value_before


def test_instance_store_with_non_string_class_dict_key():
# Such a namespace requires a dictionary that cannot be probed by the
# side-effect-free descriptor lookup used when specializing STORE_ATTR.
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
C = type("C", (), {1: None})

def write(obj, value):
obj.attr = value

obj = C()
for value in range(20):
write(obj, value)
assert obj.attr == value


def test_evil_getattribute():
# Variation of a CPython test from test_descr.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
import com.oracle.graal.python.builtins.objects.getsetdescriptor.GetSetDescriptor;
import com.oracle.graal.python.builtins.objects.object.PythonBuiltinObject;
import com.oracle.graal.python.builtins.objects.type.PythonAbstractClass;
import com.oracle.graal.python.builtins.objects.type.PythonClass;
import com.oracle.graal.python.builtins.objects.type.PythonManagedClass;
import com.oracle.graal.python.builtins.objects.type.TpSlots;
import com.oracle.graal.python.builtins.objects.type.TypeNodes;
Expand Down Expand Up @@ -259,14 +260,17 @@ static Object doIt(PythonAbstractClass object,
public static void invalidateMroLookup(PythonAbstractClass klass) {
// Note: PyType_Modified should not recompute the slots, just invalidate lookup caches
PythonAbstractClass[] allSubclasses = GetSubclassesAsArrayNode.executeRecursiveUncached(klass);
PythonClass.invalidateTypeStableAssumption(klass);
TpSlots.updateSlotWrappersLookups(klass, allSubclasses);
MroSequenceStorage mroStorage = TypeNodes.GetMroStorageNode.executeUncached(klass);
mroStorage.lookupChanged();
for (PythonAbstractClass subclass : allSubclasses) {
PythonClass.invalidateTypeStableAssumption(subclass);
MroSequenceStorage subClassMroStorage = TypeNodes.GetMroStorageNode.executeUncached(subclass);
subClassMroStorage.lookupChanged();
}
}

}

@CApiBuiltin(ret = Int, args = {Pointer}, call = Ignored)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,17 @@ private static int incrementLen(DynamicObjectStorage self, ReadAttributeFromPyth
@GenerateUncached
@GenerateInline
@GenerateCached(false)
abstract static class GetItemNode extends Node {
public abstract static class GetItemNode extends Node {
/**
* For builtin strings the {@code keyHash} value is ignored and can be garbage. If the
* {@code keyHash} is equal to {@code -1} it will be computed for non-string keys.
*/
public abstract Object execute(Frame frame, Node inliningTarget, DynamicObjectStorage self, Object key, long keyHash);

public static Object executeSlowPath(DynamicObjectStorage storage, TruffleString key, Object defaultValue) {
return DynamicObject.GetNode.getUncached().execute(storage.store, key, defaultValue);
}

@Specialization
static Object string(Node inliningTarget, DynamicObjectStorage self, TruffleString key, @SuppressWarnings("unused") long keyHash,
@Shared @Cached ReadAttributeFromPythonObjectNode readKey,
Expand Down
Loading
Loading