diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java index 43c1130dec..f4eb2173d3 100644 --- a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java @@ -91,6 +91,56 @@ public void dispose() { tester.close(); } + @Test + public void testSuperSourceSections() throws Throwable { + assertSuperSourceSections("super().value", "super(Derived, self).method(value)"); + } + + @Test + public void testGenericSuperSourceSections() throws Throwable { + assertSuperSourceSections("alias(Derived, self).value", "alias(Derived, self).method(value)"); + } + + private void assertSuperSourceSections(String valueLookup, String methodCall) throws Throwable { + Source source = Source.newBuilder("python", """ + class Base: + @property + def value(self): + return 21 + def method(self, value): + return value * 2 + class Derived(Base): + def run(self): + value = %s + return %s + alias = super + for _ in range(10): + Derived().run() + """.formatted(valueLookup, methodCall), "super_sections.py").buildLiteral(); + try (DebuggerSession session = tester.startSession()) { + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(4).build()); + session.install(Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(6).build()); + tester.startEval(source); + // The default session instruments statements, so caller locations cover the enclosing + // assignment/return. Check both uncached and cached execution, as for ordinary calls. + for (int i = 0; i < 10; i++) { + expectSuspended(event -> { + var frames = event.getStackFrames().iterator(); + assertEquals(4, frames.next().getSourceSection().getStartLine()); + assertEquals("value = " + valueLookup, frames.next().getSourceSection().getCharacters().toString()); + event.prepareContinue(); + }); + expectSuspended(event -> { + var frames = event.getStackFrames().iterator(); + assertEquals(6, frames.next().getSourceSection().getStartLine()); + assertEquals("return " + methodCall, frames.next().getSourceSection().getCharacters().toString()); + event.prepareContinue(); + }); + } + tester.expectDone(); + } + } + @Test public void testSteppingAsExpected() throws Throwable { // test that various elements step as expected, including generators, statement level atomic diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py index fca6a2660d..4174330f4f 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_graalpy_opts.py @@ -40,6 +40,7 @@ import sys import types import unittest +import builtins if sys.implementation.name == "graalpy" and not __graalpython__.is_forced_uncached_interpreter: diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_super.py b/graalpython/com.oracle.graal.python.test/src/tests/test_super.py index 79c1e4f890..508585a8f2 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_super.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_super.py @@ -37,6 +37,161 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import sys +import traceback +import weakref + +from tests import util + + +class A: + def f(self): + return "a" + + +class B(A): + def f(self): + return super().f() + "b" + + +class C(A): + def f(self): + yield super().f() + "c" + + +def test_super_without_arguments(): + assert B().f() == "ab" + + +def test_super_in_generator(): + assert list(C().f()) == ["ac"] + + +def test_super_with_none_first_parameter_is_unbound(): + class Base: + value = 42 + + class Derived(Base): + def direct(self): + return super().__thisclass__, super().__self__, super().__self_class__ + + def saved(self): + proxy = super() + return proxy.__thisclass__, proxy.__self__, proxy.__self_class__ + + def bind(self, instance): + return super().__get__(instance, Derived) + + def missing(self): + return super().value + + assert Derived.saved(None) == (Derived, None, None) + assert Derived.direct(None) == Derived.saved(None) + instance = Derived() + bound = Derived.bind(None, instance) + assert bound.__thisclass__ is Derived + assert bound.__self__ is instance + assert bound.value == 42 + try: + Derived.missing(None) + except AttributeError: + pass + else: + assert False + + +def test_super_uses_current_first_parameter(): + class Base: + def identity(self): + return self + + class Derived(Base): + def reassigned(self, other): + self = other + return super().identity() + + def deleted(self): + del self + return super().identity() + + first = Derived() + second = Derived() + assert first.reassigned(second) is second + try: + first.deleted() + except (RuntimeError, UnboundLocalError) as caught: + assert "deleted" in str(caught) or "self" in str(caught) + else: + assert False + + +def test_super_in_nested_function_uses_nested_first_parameter(): + class Base: + def identity(self): + return self + + class Derived(Base): + def make(self): + def nested(other): + return super().identity() + return nested + + first = Derived() + second = Derived() + assert first.make()(second) is second + + +def test_replacement_precedes_implicit_context_errors(): + class Base: + value = "base" + + class Derived(Base): + def read_after_delete(self): + del self + return super().value + + namespace = globals() + previous = namespace.get("super") + had_previous = "super" in namespace + replacement = type("Replacement", (), {"value": "replacement"})() + try: + namespace["super"] = lambda: replacement + if sys.implementation.name == "graalpy": + assert Derived().read_after_delete() == "replacement" + else: + try: + Derived().read_after_delete() + except UnboundLocalError: + pass + else: + assert False + finally: + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) + + +def test_generator_keeps_selected_super_method_across_yield(): + class Base: + def method(self, value): + return "old", self, value + + class Derived(Base): + def run(self): + return super().method((yield "ready")) + + obj = Derived() + generator = obj.run() + assert next(generator) == "ready" + Base.method = lambda self, value: ("new", self, value) + try: + generator.send(42) + except StopIteration as stopped: + assert stopped.value == ("old", obj, 42) + else: + assert False + def test_super(): class A: @@ -58,6 +213,24 @@ def test_super_requires_type_arg(): assert False +def test_direct_super_invalid_type_with_none(): + def attribute(cls): + return super(cls, None).__self__ + + def method(cls): + return super(cls, None).__repr__() + + for operation in (attribute, method): + operation(object) + try: + operation(42) + except TypeError as error: + assert "type" in str(error) + else: + assert False + operation(object) + + def test_super_subclass_descr_get_invokes_subclass_type(): class MySuper(super): news = [] @@ -99,6 +272,619 @@ class B(A): assert MySuper.calls == [()] +def test_direct_super_lookup_and_binding(): + descriptor_calls = [] + + class Descriptor: + def __get__(self, instance, owner): + descriptor_calls.append((instance, owner)) + return lambda value: (instance, owner, value) + + class A: + value = 40 + descriptor = Descriptor() + + def method(self, value): + return self, value + + @staticmethod + def static(value): + return "static", value + + @classmethod + def classmethod_(cls, value): + return cls, value + + @property + def property_(self): + return self.value + 2 + + class B(A): + def read(self): + return super().value, super(B, self).property_ + + def call(self): + return super().method(2) + + def call_descriptor(self): + return super().descriptor(3) + + def call_static(self): + return super().static(4) + + def call_classmethod(self): + return super().classmethod_(5) + + @classmethod + def unbound(cls, obj): + return super(B, B).method(obj, 6) + + obj = B() + obj.value = -1 + assert obj.read() == (40, 1) + assert obj.call() == (obj, 2) + assert obj.call_descriptor() == (obj, B, 3) + assert descriptor_calls == [(obj, B)] + assert obj.call_static() == ("static", 4) + assert obj.call_classmethod() == (B, 5) + assert B.unbound(obj) == (obj, 6) + + +def test_direct_super_observes_mutation_after_warmup(): + class Root: + pass + + class A(Root): + value = 1 + + def method(self): + return 1 + + class Other(Root): + value = 10 + + def method(self): + return 10 + + class B(A): + def read(self): + return super().value + + def call(self): + return super().method() + + obj = B() + for _ in range(5): + assert (obj.read(), obj.call()) == (1, 1) + A.value = 2 + A.method = lambda self: 2 + assert (obj.read(), obj.call()) == (2, 2) + B.__bases__ = (Other,) + assert (obj.read(), obj.call()) == (10, 10) + + +def test_direct_super_descriptor_errors_are_not_missing(): + class Raising: + def __init__(self, exception): + self.exception = exception + + def __get__(self, instance, owner): + raise self.exception("from descriptor") + + class A: + attr_error = Raising(AttributeError) + value_error = Raising(ValueError) + + class B(A): + def attr(self): + return super().attr_error + + def value(self): + return super().value_error + + for function, exception in ((B().attr, AttributeError), (B().value, ValueError)): + try: + function() + except exception as caught: + assert "from descriptor" in str(caught) + else: + assert False + + +def test_direct_super_replacement_and_ordering(): + events = [] + + class Result: + def __getattribute__(self, name): + events.append(("lookup", name)) + if name == "method": + return lambda value: events.append(("invoke", value)) or value + return object.__getattribute__(self, name) + + def replacement(type_arg, object_arg): + events.append(("replacement", type_arg, object_arg)) + return Result() + + class A: + pass + + class B(A): + def run(self): + def type_arg(): + events.append("type") + return B + + def object_arg(): + events.append("object") + return self + + def outer_arg(): + events.append("outer") + return 42 + + return super(type_arg(), object_arg()).method(outer_arg()) + + namespace = globals() + previous = namespace.get("super") + had_previous = "super" in namespace + try: + namespace["super"] = replacement + obj = B() + assert obj.run() == 42 + assert events == ["type", "object", ("replacement", B, obj), ("lookup", "method"), "outer", ("invoke", 42)] + finally: + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) + + +def test_direct_super_replacement_profile_events(): + events = [] + + class User: + def get(self, key): + return super().get(key) + + def missing(self): + return super().missing + + def fail(self): + return super().value + + def profile(frame, event, arg): + if event.startswith("c_") and (arg is globals or arg is len): + events.append((event, arg)) + + namespace = globals() + previous = namespace.get("super") + had_previous = "super" in namespace + key = "__super_profile_events" + try: + namespace[key] = events + namespace["super"] = globals + for _ in range(5): + assert User().get(key) is events + + sys.setprofile(profile) + assert User().get(key) is events + try: + User().missing() + except AttributeError: + pass + else: + assert False + namespace["super"] = len + try: + User().fail() + except TypeError: + pass + else: + assert False + sys.setprofile(None) + + assert events == [ + ("c_call", globals), + ("c_return", globals), + ("c_call", globals), + ("c_return", globals), + ("c_call", len), + ("c_exception", len), + ] + finally: + sys.setprofile(None) + namespace.pop(key, None) + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) + + +def test_direct_super_profile_nested_calls(): + events = [] + + class Base: + @property + def value(self): + return [len(())] + + def number(self): + return -1 + + @property + def broken(self): + len(()) + raise ValueError("descriptor") + + class Derived(Base): + def zero(self): + return len(super().value), abs(super().number()) + + def explicit(self): + return len(super(Derived, self).value), abs(super(Derived, self).number()) + + def failing(self): + return abs(super().broken) + + def invalid(self): + return abs(super(42, None).number()) + + def nested(self): + return len(super(type(super().number()), self).value) + + def profile(frame, event, arg): + if event.startswith("c_") and (arg is len or arg is abs): + events.append((event, arg)) + + obj = Derived() + for operation in (obj.zero, obj.explicit): + assert operation() == (1, 1) + events.clear() + try: + sys.setprofile(profile) + assert operation() == (1, 1) + finally: + sys.setprofile(None) + assert events == [ + ("c_call", len), ("c_return", len), + ("c_call", len), ("c_return", len), + ("c_call", abs), ("c_return", abs), + ] + + for operation, error, expected in ( + (obj.failing, ValueError, [("c_call", len), ("c_return", len)]), + (obj.invalid, TypeError, []), + (obj.nested, TypeError, [])): + events.clear() + try: + sys.setprofile(profile) + try: + operation() + except error: + pass + else: + assert False + finally: + sys.setprofile(None) + assert events == expected + + +def test_direct_super_method_does_not_retain_receiver_after_exception(): + class Marker(Exception): + pass + + class A: + dynamic = int.real + + def method(self, value): + return value + + class B(A): + @staticmethod + def outer_argument_case(refs): + def make_receiver(): + receiver = B() + refs.append(weakref.ref(receiver)) + return receiver + + def argument(raises): + if raises: + raise Marker("argument") + return 41 + + for raises in (True, False): + try: + result = super(B, make_receiver()).method(argument(raises)) + except Marker as caught: + assert raises + caught.__traceback__ = None + caught = None + util.gc_collect(lambda: refs[-1]() is not None) + assert refs[-1]() is None + else: + assert not raises + assert result == 41 + + @staticmethod + def descriptor_case(refs): + def make_receiver(): + receiver = B() + refs.append(weakref.ref(receiver)) + return receiver + + for raises in (True, False): + A.dynamic = int.real if raises else lambda self: 42 + try: + result = super(B, make_receiver()).dynamic() + except TypeError as caught: + assert raises + caught.__traceback__ = None + caught = None + if sys.implementation.name != "graalpy": + # TODO: temporary locals remain live until this frame returns + # fix with StackValues when we have multi-return in Bytecode DSL + util.gc_collect(lambda: refs[-1]() is not None) + assert refs[-1]() is None + else: + assert not raises + assert result == 42 + + refs = [] + B.outer_argument_case(refs) + B.descriptor_case(refs) + util.gc_collect(lambda: any(ref() is not None for ref in refs)) + assert all(ref() is None for ref in refs) + + +def test_direct_super_replacement_releases_constructor_operands(): + class Result: + def method(self, value): + return value + + class Replacement: + def __call__(self, cls, obj): + return Result() + + refs = [] + + def make_type(): + cls = type("Temporary", (), {}) + refs.append(weakref.ref(cls)) + return cls + + def make_receiver(): + obj = Result() + refs.append(weakref.ref(obj)) + return obj + + def run(): + return super(make_type(), make_receiver()).method((yield "ready")) + + namespace = globals() + had_previous = "super" in namespace + previous = namespace.get("super") + replacement = Replacement() + refs.append(weakref.ref(replacement)) + namespace["super"] = replacement + del replacement + generator = run() + try: + assert next(generator) == "ready" + finally: + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) + util.gc_collect(lambda: any(ref() is not None for ref in refs)) + assert all(ref() is None for ref in refs) + try: + generator.send(42) + except StopIteration as stopped: + assert stopped.value == 42 + else: + assert False + + +def test_direct_super_method_lookup_traceback_span(): + for constructor in ("super()", "super(B, self)", "super(42, self)"): + for attribute in ("missing", "broken"): + expression = f"{constructor}.{attribute}" + line = f" return {expression}(3)" + source = ("class A:\n" + " @property\n" + " def broken(self):\n" + " raise ValueError('descriptor')\n" + "class B(A):\n" + " def run(self):\n" + line + "\n") + namespace = {} + exec(compile(source, "", "exec"), namespace) + try: + namespace["B"]().run() + except (TypeError, AttributeError, ValueError) as error: + entry = next(entry for entry in traceback.extract_tb(error.__traceback__) + if entry.name == "run") + assert entry.lineno == 7 + assert entry.colno == line.index(expression) + assert entry.end_colno == line.index(expression) + len(expression) + else: + assert False + + +def test_direct_super_proxy_class_lookup_once(): + events = [] + + class Base: + value = 42 + + def method(self): + return self + + class Derived(Base): + pass + + class Proxy: + @property + def __class__(self): + events.append("class") + return Derived + + proxy = Proxy() + for _ in range(5): + events.clear() + assert super(Derived, proxy).value == 42 + assert events == ["class"] + events.clear() + assert super(Derived, proxy).method() is proxy + assert events == ["class"] + + +def test_direct_super_fallback_after_warmup(): + events = [] + + class Base: + @property + def value(self): + return self + + def method(self): + return self + + class Proxy: + @property + def __class__(self): + events.append("class") + return Derived + + for constructor in ("super()", "super(Derived, self)"): + for attribute in ("value", "method()"): + for use_proxy in (False, True): + # Give each scenario its own bytecode so an earlier bailout cannot hide a + # transition from the fast path to the generic specialization. + namespace = {"Base": Base} + exec("class Derived(Base):\n" + " def lookup(self):\n" + f" return {constructor}.{attribute}\n", namespace) + Derived = namespace["Derived"] + lookup = Derived.lookup + obj = Derived() + for _ in range(10): + assert lookup(obj) is obj + + events.clear() + if use_proxy: + proxy = Proxy() + assert lookup(proxy) is proxy + assert events == ["class"] + else: + try: + lookup(object()) + except TypeError: + pass + else: + assert False, "super() accepted an unrelated receiver" + + # Once attribute lookup falls back, subsequent calls must keep supplying real + # super objects rather than returning the fast-path placeholder again. + for _ in range(10): + assert lookup(obj) is obj + assert events == (["class"] if use_proxy else []) + + +def test_direct_super_bound_descriptor_releases_receiver_after_call(): + refs = [] + + class Base: + @staticmethod + def method(value): + return value + + class Derived(Base): + pass + + def make(): + obj = Derived() + refs.append(weakref.ref(obj)) + return obj + + def argument(): + # A staticmethod does not retain its receiver. It may already be collected, + # but the selected callable must remain valid while arguments are evaluated. + util.gc_collect() + return 42 + + for _ in range(5): + assert super(Derived, make()).method(argument()) == 42 + util.gc_collect(lambda: refs[-1]() is not None) + assert refs[-1]() is None + + alias = super + assert alias(Derived, make()).method(argument()) == 42 + util.gc_collect(lambda: refs[-1]() is not None) + assert refs[-1]() is None + + class Caller: + def call(self): + return super(Caller, self).method(argument()) + + namespace = globals() + previous = namespace.get("super") + had_previous = "super" in namespace + try: + namespace["super"] = lambda *args: make() + assert Caller().call() == 42 + finally: + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) + util.gc_collect(lambda: refs[-1]() is not None) + assert refs[-1]() is None + + +def test_direct_super_missing_lookup_key_equality(): + events = [] + + class Key(str): + def __hash__(self): + return hash("missing") + + def __eq__(self, other): + events.append(other) + return False + + Base = type("Base", (), {Key("collision"): 42}) + + class Derived(Base): + pass + + obj = Derived() + alias = super + + def direct(): + return super(Derived, obj).missing + + def generic(): + return alias(Derived, obj).missing + + # GraalPy may cache negative lookups and omit subsequent __eq__ calls. This is an + # intentionally accepted difference from CPython, which currently calls __eq__ on + # every negative lookup, even after warmup. Keep checking that behavior on CPython + # so a change in a future version makes this test fail and prompts us to reconsider + # the GraalPy behavior. + for _ in range(10): + for lookup in (generic, direct): + events.clear() + try: + lookup() + except AttributeError: + pass + else: + assert False + assert all(event == "missing" for event in events) + if sys.implementation.name == "cpython": + assert events, "CPython no longer calls __eq__ on every negative super lookup" + + def test_super_lookup_mro_change_during_dict_key_equality(): calls = [] @@ -345,3 +1131,70 @@ def read(self): for _ in range(10): assert C.value == "A" assert obj.read() == "B" + + + def test_direct_super_optimized_patterns(): + class A: + value = 42 + + def method(self, *args): + return self, args + + class B(A): + def zero_attr(self): + return super().value + + def explicit_attr(self): + return super(B, self).value + + def zero_method(self): + return super().method(1) + + def explicit_method(self): + return super(B, self).method(1) + + def starred_method(self, args): + return super().method(*args) + + def keyword_method(self): + return super().method(value=1) + + obj = B() + assert obj.zero_attr() == obj.explicit_attr() == 42 + assert obj.zero_method() == obj.explicit_method() == (obj, (1,)) + assert obj.starred_method((1, 2)) == (obj, (1, 2)) + try: + obj.keyword_method() + except TypeError: + pass + else: + assert False + + + def test_direct_super_runtime_rebinding(): + class A: + value = "builtin" + + class B(A): + def value(self): + return super().value + + obj = B() + for _ in range(5): + assert obj.value() == "builtin" + assert_contains_bytecode(B.value, "SuperGetAttribute") + + sentinel = type("Replacement", (), {"value": "replacement"})() + namespace = globals() + previous = namespace.get("super") + had_previous = "super" in namespace + try: + namespace["super"] = lambda: sentinel + assert obj.value() == "replacement" + namespace.pop("super") + assert obj.value() == "builtin" + finally: + if had_previous: + namespace["super"] = previous + else: + namespace.pop("super", None) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_super_without_arguments.py b/graalpython/com.oracle.graal.python.test/src/tests/test_super_without_arguments.py deleted file mode 100644 index fda8c1f7ae..0000000000 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_super_without_arguments.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2018, 2023, 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. - -class A(): - def f(self): - return "a" - - -class B(A): - def f(self): - return super().f() + "b" - - -class C(A): - def f(self): - yield super().f() + "c" - - -def test_super(): - assert B().f() == "ab" - - -def test_super_in_generator(): - assert list(C().f()) == ["ac"] diff --git a/graalpython/com.oracle.graal.python.test/src/tests/util.py b/graalpython/com.oracle.graal.python.test/src/tests/util.py index 7408ffc823..7b01298ec6 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/util.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/util.py @@ -44,10 +44,33 @@ import unittest IS_BYTECODE_DSL = sys.implementation.name == 'graalpy' +is_graalpy = sys.implementation.name == "graalpy" TRANSIENT_GRAALPY_STARTUP_BLOCKING_IO = "ERROR: BlockingIOError: [Errno 11] Resource temporarily unavailable" GRAALPY_SUBPROCESS_TEST_ENV = "GRAALPY_TEST_SUBPROCESS" +# gc_collect copied from graalpy patch of stdlib test support +def gc_collect(until = None): + import gc + gc.collect() + if is_graalpy: + time.sleep(0.1) + gc.collect() + if until: + i = 0 + while until(): + if is_graalpy: + time.sleep(0.1) + gc.collect() + i += 1 + if i > 1000: + print("WARNING: timeout while waiting for GC") + if is_graalpy and hasattr(__graalpython__, 'dump_heap'): + dump_path = __graalpython__.dump_heap() + print(f"HEAP DUMP: {dump_path}") + break + + def run_in_graalpy_subprocess(test): @functools.wraps(test) def wrapper(*args, **kwargs): diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cell/CellBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cell/CellBuiltins.java index d5bb915ae7..6bf3bc747b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cell/CellBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cell/CellBuiltins.java @@ -48,10 +48,10 @@ import java.util.List; import com.oracle.graal.python.PythonLanguage; +import com.oracle.graal.python.annotations.Builtin; import com.oracle.graal.python.annotations.Slot; import com.oracle.graal.python.annotations.Slot.SlotKind; import com.oracle.graal.python.annotations.Slot.SlotSignature; -import com.oracle.graal.python.annotations.Builtin; import com.oracle.graal.python.builtins.CoreFunctions; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; @@ -82,6 +82,7 @@ import com.oracle.truffle.api.dsl.GenerateCached; import com.oracle.truffle.api.dsl.GenerateInline; import com.oracle.truffle.api.dsl.GenerateNodeFactory; +import com.oracle.truffle.api.dsl.GenerateUncached; import com.oracle.truffle.api.dsl.NeverDefault; import com.oracle.truffle.api.dsl.NodeFactory; import com.oracle.truffle.api.dsl.Specialization; @@ -221,6 +222,7 @@ static Object set(PCell self, Object ref) { @GenerateInline @GenerateCached(false) + @GenerateUncached public abstract static class GetRefNode extends PNodeWithContext { public abstract Object execute(Node inliningTarget, PCell self); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/BytecodeDSLCompiler.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/BytecodeDSLCompiler.java index ad9e3804ef..8ff6d0ae4d 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/BytecodeDSLCompiler.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/BytecodeDSLCompiler.java @@ -49,16 +49,16 @@ import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.compiler.AstOptimizer; import com.oracle.graal.python.compiler.ParserCallbacksImpl; -import com.oracle.graal.python.pegparser.AbstractParser; import com.oracle.graal.python.nodes.bytecode_dsl.BytecodeDSLCodeUnit; import com.oracle.graal.python.nodes.bytecode_dsl.PBytecodeDSLRootNode; +import com.oracle.graal.python.pegparser.AbstractParser; import com.oracle.graal.python.pegparser.FutureFeature; import com.oracle.graal.python.pegparser.InputType; import com.oracle.graal.python.pegparser.Parser; -import com.oracle.graal.python.pegparser.scope.Scope; -import com.oracle.graal.python.pegparser.scope.ScopeEnvironment; import com.oracle.graal.python.pegparser.ParserCallbacks; import com.oracle.graal.python.pegparser.ParserCallbacks.ErrorType; +import com.oracle.graal.python.pegparser.scope.Scope; +import com.oracle.graal.python.pegparser.scope.ScopeEnvironment; import com.oracle.graal.python.pegparser.sst.AliasTy; import com.oracle.graal.python.pegparser.sst.ConstantValue; import com.oracle.graal.python.pegparser.sst.ExprTy; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java index 4f6ef210db..05c0b9d684 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/bytecode_dsl/RootNodeCompiler.java @@ -59,6 +59,7 @@ import static com.oracle.graal.python.compiler.bytecode_dsl.BytecodeDSLCompilerUtils.hasDefaultKwargs; import static com.oracle.graal.python.compiler.bytecode_dsl.BytecodeDSLCompilerUtils.len; import static com.oracle.graal.python.nodes.BuiltinNames.J_BREAKPOINT; +import static com.oracle.graal.python.nodes.BuiltinNames.J_SUPER; import static com.oracle.graal.python.nodes.SpecialAttributeNames.J___CLASS__; import static com.oracle.graal.python.nodes.SpecialAttributeNames.J___FIRSTLINENO__; import static com.oracle.graal.python.nodes.SpecialAttributeNames.J___STATIC_ATTRIBUTES__; @@ -2095,9 +2096,17 @@ public Void visit(ExprTy.Attribute node) { beginSourceSection(node, b); beginTraceLineChecked(b); - beginGetAttribute(node.attr, b); - node.value.accept(this); - b.endGetAttribute(); + ExprTy.Call superCall = getOptimizableSuperCall(node); + if (superCall != null) { + b.beginBlock(); + StackValue[] values = emitSuperLookup(superCall, node.attr, false); + b.emitLoadStackValue(values[0]); + b.endBlock(); + } else { + beginGetAttribute(node.attr, b); + node.value.accept(this); + b.endGetAttribute(); + } endTraceLineChecked(node, b); endSourceSection(b); @@ -2105,6 +2114,59 @@ public Void visit(ExprTy.Attribute node) { return null; } + private void emitArgOrNull(ExprTy[] args, int index) { + if (index < args.length) { + args[index].accept(this); + } else { + b.emitLoadNull(); + } + } + + /** + * Emits a direct super lookup and returns its callable and, for method lookups, receiver. + * The caller must own a block that keeps the returned stack values alive. + */ + private StackValue[] emitSuperLookup(ExprTy.Call superCall, String attribute, boolean method) { + // @formatter:off + BytecodeLocal typeArg = beginTemporaryLocal(); + BytecodeLocal valueArg = beginTemporaryLocal(); + b.beginBindStackValue(); + // SuperGetAttribute handles the placeholder from SuperCall or a real fallback result. + b.beginSuperGetAttribute(toTruffleStringUncached(maybeMangleAndAddName(attribute)), method, valueArg); + enterProfileCEventCall(); + b.beginInstrumentCallReturn(); + // SuperCall returns a placeholder for the builtin, otherwise the real call result. + b.beginSuperCall(typeArg, valueArg); + b.beginInstrumentCallable(); + superCall.func.accept(this); + b.endInstrumentCallable(); + assert superCall.args.length == 0 || superCall.args.length == 2; + emitArgOrNull(superCall.args, 0); + // The last argument is instrumented with the call event. + b.beginInstrumentCall(); + beginTraceLineChecked(b); + emitArgOrNull(superCall.args, 1); + endTraceLineChecked(superCall.func, b); + b.endInstrumentCall(); + b.endSuperCall(); + b.endInstrumentCallReturn(); + exitProfileCEventCall(); + b.emitLoadLocal(typeArg); + b.emitLoadLocal(valueArg); + b.endSuperGetAttribute(); + StackValue callable = b.endBindStackValue(); + endTemporaryLocal(typeArg); + if (method) { + b.beginBindStackValue(); + loadAndEndTemporaryLocal(valueArg); + StackValue receiver = b.endBindStackValue(); + return new StackValue[]{callable, receiver}; + } + endTemporaryLocal(valueArg); + return new StackValue[]{callable, null}; + // @formatter:on + } + @Override public Void visit(ExprTy.Await node) { // TODO if !IS_TOP_LEVEL_AWAIT @@ -2278,6 +2340,54 @@ private static boolean isAttributeLoad(ExprTy node) { return node instanceof ExprTy.Attribute && ((ExprTy.Attribute) node).context == ExprContextTy.Load; } + /** + * Returns the "super" call node if the expression is of form {@code super().attr} or {@code super(X, self).attr}. + * Otherwise, returns {@code null}. + *

+ * For zero-argument {@code super()}, this only checks that a {@code __class__} closure cell + * exists. {@link PBytecodeDSLRootNode.SuperCall} reads and validates the cell's current value + * at runtime, so changes to the cell are observed by the optimized path. + */ + private ExprTy.Call getOptimizableSuperCall(ExprTy.Attribute attr) { + assert attr.context == ExprContextTy.Load; + if (J___CLASS__.equals(attr.attr)) { + return null; + } + if (!(attr.value instanceof ExprTy.Call call) || !(call.func instanceof ExprTy.Name name) || !J_SUPER.equals(name.id) || // + call.keywords.length != 0 || (call.args.length != 0 && call.args.length != 2)) { + return null; + } + assert name.context == ExprContextTy.Load; + for (ExprTy arg : call.args) { + if (arg instanceof ExprTy.Starred) { + return null; + } + } + EnumSet uses = scope.getUseOfName(J_SUPER); + if (!uses.contains(DefUse.GlobalImplicit)) { + return null; + } + if (call.args.length == 0 && (!hasPositionalFirstParameter() || !freevars.containsKey(J___CLASS__))) { + // we need __class__ and first positional param for "super()" + return null; + } + return call; + } + + private boolean hasPositionalFirstParameter() { + ArgumentsTy args; + if (startNode instanceof FunctionDef functionDef) { + args = functionDef.args; + } else if (startNode instanceof AsyncFunctionDef functionDef) { + args = functionDef.args; + } else if (startNode instanceof Lambda lambda) { + args = lambda.args; + } else { + return false; + } + return args.posOnlyArgs.length != 0 || args.args.length != 0; + } + private static final int NUM_ARGS_MAX_FIXED = 4; private void enterProfileCEventCall() { @@ -2388,14 +2498,24 @@ private void emitCall(ExprTy func, ExprTy[] args, KeywordTy[] keywords) { } StackValue receiver = null; + StackValue directSuperCallable = null; if (isMethodCall) { // Reserve a stack value for the receiver. b.beginBlock(); - b.beginBindStackValue(); assert isAttributeLoad(func); ExprTy.Attribute attrAccess = (ExprTy.Attribute) func; - attrAccess.value.accept(this); - receiver = b.endBindStackValue(); + ExprTy.Call superCall = getOptimizableSuperCall(attrAccess); + if (superCall != null) { + beginSourceSection(attrAccess, b); + StackValue[] values = emitSuperLookup(superCall, attrAccess.attr, true); + endSourceSection(b); + directSuperCallable = values[0]; + receiver = values[1]; + } else { + b.beginBindStackValue(); + attrAccess.value.accept(this); + receiver = b.endBindStackValue(); + } } // @formatter:off @@ -2411,7 +2531,11 @@ private void emitCall(ExprTy func, ExprTy[] args, KeywordTy[] keywords) { // The receiver is needed for method lookup and for the first argument. if (useVariadic) { b.beginInstrumentCallable(); - emitGetMethod(func, receiver); + if (directSuperCallable != null) { + b.emitLoadStackValue(directSuperCallable); + } else { + emitGetMethod(func, receiver); + } b.endInstrumentCallable(); b.beginCollectToObjectArray(); @@ -2425,7 +2549,11 @@ private void emitCall(ExprTy func, ExprTy[] args, KeywordTy[] keywords) { assert len(keywords) == 0; b.beginInstrumentCallable(); - emitGetMethod(func, receiver); + if (directSuperCallable != null) { + b.emitLoadStackValue(directSuperCallable); + } else { + emitGetMethod(func, receiver); + } b.endInstrumentCallable(); if (numArgs == 1) { b.beginInstrumentCall(); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java index 5e59c32c42..0fe698dac1 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/bytecode_dsl/PBytecodeDSLRootNode.java @@ -73,6 +73,7 @@ import com.oracle.graal.python.builtins.objects.PythonAbstractObject; import com.oracle.graal.python.builtins.objects.asyncio.GetAwaitableNode; import com.oracle.graal.python.builtins.objects.asyncio.PAsyncGenWrappedValue; +import com.oracle.graal.python.builtins.objects.cell.CellBuiltins; import com.oracle.graal.python.builtins.objects.cell.PCell; import com.oracle.graal.python.builtins.objects.code.PCode; import com.oracle.graal.python.builtins.objects.common.DynamicObjectStorage; @@ -115,13 +116,16 @@ import com.oracle.graal.python.builtins.objects.set.PFrozenSet; import com.oracle.graal.python.builtins.objects.set.PSet; import com.oracle.graal.python.builtins.objects.set.SetNodes; +import com.oracle.graal.python.builtins.objects.superobject.SuperObject; import com.oracle.graal.python.builtins.objects.tuple.PTuple; import com.oracle.graal.python.builtins.objects.type.PythonBuiltinClass; 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.TpSlots.GetObjectSlotsNode; +import com.oracle.graal.python.builtins.objects.type.TypeNodes; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.CallSlotDescrGet; +import com.oracle.graal.python.builtins.objects.type.slots.TpSlotGetAttr.CallSlotGetAttrNode; import com.oracle.graal.python.builtins.objects.type.slots.TpSlotIterNext.CallSlotTpIterNextNode; import com.oracle.graal.python.builtins.objects.typing.PTypeAliasType; import com.oracle.graal.python.compiler.MakeTypeParamKind; @@ -237,6 +241,7 @@ import com.oracle.graal.python.nodes.frame.ReadGlobalOrBuiltinNode; import com.oracle.graal.python.nodes.frame.WriteGlobalNode; import com.oracle.graal.python.nodes.frame.WriteNameNode; +import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinClassExactProfile; import com.oracle.graal.python.nodes.object.BuiltinClassProfiles.IsBuiltinObjectProfile; import com.oracle.graal.python.nodes.object.GetClassNode; import com.oracle.graal.python.nodes.object.GetClassNode.GetPythonObjectClassNode; @@ -1933,6 +1938,157 @@ public static boolean doIterator(VirtualFrame frame, LocalAccessor output, Objec } } + /** + * Specialized instruction for {@code super(T, o)} calls where T and o may be null, which means + * the arguments were not provided. This operation either produces the super object, or returns + * placeholder {@link #BUILTIN_SUPER} that indicates that the following operation + * SuperGetAttribute can take the fast-path. + *

+ * TODO: when Bytecode DSL supports multi-value return operations, we can return the super + * object/placeholder and {@code actualType} and {@code actualObject} directly instead of taking + * LocalAccessors. + */ + @Operation(storeBytecodeIndex = false) + @ConstantOperand(type = LocalAccessor.class) + @ConstantOperand(type = LocalAccessor.class) + @ImportStatic(PythonBuiltinClassType.class) + public static final class SuperCall { + static final Object BUILTIN_SUPER = new Object(); + + @ForceQuickening + @Specialization(guards = "isBuiltinSuperProfile.profileClass(inliningTarget, callable, Super)", // + limit = "1", rewriteOn = FastPathBailoutException.class, excludeForUncached = false) + public static Object check(VirtualFrame frame, + LocalAccessor actualType, LocalAccessor actualObject, Object callable, Object explicitType, Object explicitObject, + @Bind Node inliningTarget, + @Bind BytecodeNode location, + @Bind PBytecodeDSLRootNode root, + @Cached IsBuiltinClassExactProfile isBuiltinSuperProfile, + @Cached CellBuiltins.GetRefNode getRefNode, + @Cached TypeNodes.IsTypeNode isTypeNode, + @Cached GetClassNode getClassNode, + @Cached IsSubtypeNode isSubtypeNode, + @Cached IsForeignObjectNode isForeignObjectNode) throws FastPathBailoutException { + Object type = explicitType; + Object object = explicitObject; + if (type == null) { + PCell classCell = root.readClassCell(frame, location); + type = classCell == null ? null : getRefNode.execute(inliningTarget, classCell); + object = root.readSelf(frame, location); + } + /* + * Keep all observable validation, including proxy __class__ lookup and error creation, + * on the ordinary super constructor path. The fast path only accepts cases that can be + * validated without executing Python code. + */ + if (type == null || object == null || object instanceof PNone || isForeignObjectNode.execute(inliningTarget, object) || !isTypeNode.execute(inliningTarget, type)) { + throw FastPathBailoutException.INSTANCE; + } + if (!isTypeNode.execute(inliningTarget, object) || !isSubtypeNode.execute(object, type)) { + Object objectType = getClassNode.execute(inliningTarget, object); + if (!isSubtypeNode.execute(objectType, type)) { + throw FastPathBailoutException.INSTANCE; + } + } + actualType.setObject(location, frame, type); + actualObject.setObject(location, frame, object); + return BUILTIN_SUPER; + } + + @InliningCutoff + @StoreBytecodeIndex + @Specialization(replaces = "check") + public static Object doSuper(VirtualFrame frame, + LocalAccessor actualType, LocalAccessor actualObject, Object callable, Object explicitType, Object explicitObject, + @Bind BytecodeNode location, + @Cached CallNode callNode) { + // SuperGetAttribute still consumes both locals on the generic path. + actualType.setObject(location, frame, PNone.NO_VALUE); + actualObject.setObject(location, frame, PNone.NO_VALUE); + if (explicitType == null) { + return callNode.execute(frame, callable); + } + return callNode.execute(frame, callable, explicitType, explicitObject); + } + + } + + /** + * Specialized instruction for super attribute accesses of form {@code super(T, o).foo}. The + * first argument is either the {@link SuperCall#BUILTIN_SUPER} placeholder or the actual super + * object. If the first argument is the placeholder, then the following two arguments are the + * {@code T} and {@code o} from the {@code super} call that wasn't actually performed - this + * indicates that we can take a fast-path for what would be a builtin super object. + */ + @Operation(storeBytecodeIndex = true) + @ConstantOperand(type = TruffleString.class) + @ConstantOperand(type = boolean.class) + @ConstantOperand(type = LocalAccessor.class) + @ImportStatic(SuperCall.class) + public static final class SuperGetAttribute { + @ForceQuickening + @Specialization(guards = "superObject == BUILTIN_SUPER") + public static Object doFastPath(VirtualFrame frame, + TruffleString key, boolean method, @SuppressWarnings("unused") LocalAccessor receiver, + Object superObject, Object type, Object object, + @Bind Node inliningTarget, + @Bind BytecodeNode location, + @Bind PBytecodeDSLRootNode root, + @Cached TypeNodes.IsTypeNode isTypeNode, + @Cached GetClassNode getClassNode, + @Cached IsSubtypeNode isSubtypeNode, + @Cached LookupAttributeInMRONode.Super lookupNode, + @Cached GetObjectSlotsNode getSlotsNode, + @Cached CallSlotDescrGet callGetSlotNode, + @Cached InlinedConditionProfile hasDescrGetProfile, + @Cached InlinedConditionProfile classBindingProfile, + @Cached CallSlotGetAttrNode getAttrNode) { + Object objectType; + if (isTypeNode.execute(inliningTarget, object) && isSubtypeNode.execute(object, type)) { + objectType = object; + } else { + objectType = getClassNode.execute(inliningTarget, object); + } + Object result = lookupNode.execute(type, objectType, key); + if (result == PNone.NO_VALUE || result == LookupAttributeInMRONode.Super.NO_MRO_SUFFIX) { + /* + * Search only attributes of the proxy itself. Re-entering super.__getattribute__ + * would repeat an MRO lookup, including arbitrary dictionary-key equality. + */ + SuperObject proxy = PFactory.createSuperObject(PythonLanguage.get(root)); + proxy.init(type, objectType, object); + result = getAttrNode.execute(frame, inliningTarget, ObjectBuiltins.SLOTS, proxy, key); + return method ? new BoundDescriptor(result) : result; + } + boolean classBinding = classBindingProfile.profile(inliningTarget, object == objectType); + if (method && !classBinding && MaybeBindDescriptorNode.isMethodDescriptor(result)) { + return result; + } + TpSlots slots = getSlotsNode.execute(inliningTarget, result); + if (hasDescrGetProfile.profile(inliningTarget, slots.tp_descr_get() != null)) { + Object instance = classBinding ? PNone.NO_VALUE : object; + result = callGetSlotNode.execute(frame, inliningTarget, slots.tp_descr_get(), result, instance, objectType); + } + return method ? new BoundDescriptor(result) : result; + } + + @ForceQuickening + @Specialization(replaces = "doFastPath") + public static Object doGeneric(VirtualFrame frame, + TruffleString key, boolean method, LocalAccessor receiver, + Object superObject, @SuppressWarnings("unused") Object type, @SuppressWarnings("unused") Object object, + @Bind Node inliningTarget, + @Bind BytecodeNode location, + @Cached PyObjectGetAttr getAttributeNode) { + Object result = getAttributeNode.execute(frame, inliningTarget, superObject, key); + if (method) { + receiver.setObject(location, frame, superObject); + return new BoundDescriptor(result); + } + return result; + } + } + @Operation(storeBytecodeIndex = false) @ConstantOperand(type = TruffleString.class) @ImportStatic({PythonUtils.class, PGuards.class, GetAttribute.class}) @@ -2005,12 +2161,12 @@ public static Object doFastPathBuiltin(VirtualFrame frame, TruffleString name, P @Specialization(guards = { /* static checks: */ "!canBeSpecial", "!hasMaterializedDict(cachedShape)", "isBuiltinModule(cachedShape)", "getter != null", // /* dynamic checks: */ "getter.accepts(obj)"}, // - rewriteOn = GetAttribute.FastPathBailoutException.class, // + rewriteOn = FastPathBailoutException.class, // limit = "2", excludeForUncached = true) public static Object doModuleFastPath(VirtualFrame frame, TruffleString name, PythonModule obj, @Cached("obj.getShape()") Shape cachedShape, @Cached("canBeSpecialMethod(name)") boolean canBeSpecial, - @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter getter) throws GetAttribute.FastPathBailoutException { + @Cached("getPropertyGetterWithFinalAssumption(cachedShape, name)") PropertyGetter getter) throws FastPathBailoutException { return new BoundDescriptor(GetAttribute.getValue(getter, obj)); } @@ -2089,14 +2245,6 @@ public static int getIntValue(PropertyGetter getter, PythonObject obj) throws Fa } } - public static final class FastPathBailoutException extends SlowPathException { - private static final long serialVersionUID = 8275772171120097556L; - private static final FastPathBailoutException INSTANCE = new FastPathBailoutException(); - - private FastPathBailoutException() { - } - } - @Specialization(guards = { "!canBeSpecialMethod(cachedKey)", // "!hasMaterializedDict(cachedShape)", "isBuiltinModule(cachedShape)", // @@ -4722,4 +4870,12 @@ public static boolean makePythonSingle(VirtualFrame frame, return false; } } + + public static final class FastPathBailoutException extends SlowPathException { + private static final long serialVersionUID = 8275772171120097556L; + private static final FastPathBailoutException INSTANCE = new FastPathBailoutException(); + + private FastPathBailoutException() { + } + } }