Skip to content

Commit 937fcd9

Browse files
committed
[GR-70898] Fix stack walking in Bytecode DSL.
PullRequest: graalpython/4053
2 parents 116def4 + 9194fdc commit 937fcd9

File tree

220 files changed

+5591
-3044
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

220 files changed

+5591
-3044
lines changed

graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/interop/JavaInteropTest.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.HashMap;
5656
import java.util.List;
5757
import java.util.Map;
58+
import java.util.stream.Collectors;
5859

5960
import org.graalvm.polyglot.Context;
6061
import org.graalvm.polyglot.Context.Builder;
@@ -219,9 +220,13 @@ public void testHostException() {
219220
context.eval(createSource("import java; java.math.BigInteger.ONE.divide(java.math.BigInteger.ZERO)"));
220221
fail();
221222
} catch (PolyglotException e) {
222-
Assert.assertTrue(e.isHostException());
223-
Assert.assertTrue(e.asHostException() instanceof ArithmeticException);
224-
Assert.assertTrue(e.getMessage(), e.getMessage().contains("divide by zero"));
223+
try {
224+
Assert.assertTrue(e.isHostException());
225+
Assert.assertTrue(e.asHostException() instanceof ArithmeticException);
226+
Assert.assertTrue(e.getMessage(), e.getMessage().contains("divide by zero"));
227+
} catch (AssertionError assertionError) {
228+
throw new AssertionError(getAssertionMessage(e), assertionError);
229+
}
225230
}
226231

227232
String outString = getOutString();
@@ -230,6 +235,18 @@ public void testHostException() {
230235
Assert.assertTrue(errString, errString.isEmpty());
231236
}
232237

238+
private static String getAssertionMessage(PolyglotException ex) {
239+
var builder = new StringBuilder(ex.getClass().getSimpleName() + "\n");
240+
try {
241+
builder.append(ex.getMessage());
242+
builder.append('\n');
243+
builder.append(Arrays.stream(ex.getStackTrace()).map(StackTraceElement::toString).collect(Collectors.joining("\n")));
244+
} catch (Throwable ignore) {
245+
// pass
246+
}
247+
return builder.toString();
248+
}
249+
233250
@Test
234251
public void javaArraySet() {
235252
String source = "import java\n" +
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
import sys
40+
import os
41+
42+
from tests.cpyext import CPyExtTestCase, CPyExtType
43+
44+
# synchronize with Java implementation of __graalpython__.indirect_call_tester
45+
TYPE_INDIRECT_BOUNDARY = 1
46+
TYPE_INDIRECT_INTEROP_CALL = 2
47+
TYPE_INDIRECT_INTEROP = 3
48+
TYPE_INDIRECT_BOUNDARY_UNCACHED_INTEROP = 4
49+
50+
# We want the code to get compiled, so that we also test compiled version of the code
51+
NUM_ITERATIONS = 1000000 if sys.implementation.name == "graalpy" else 5
52+
53+
# Because of splitting, the code may not stabilize after first iteration and we may
54+
# see some stack walks during first few iterations. With default runtime or splitting
55+
# disabled, these tests should pass with STABILIZES_AT=1
56+
STABILIZES_AT = 1 if sys.implementation.name != "graalpy" or __graalpython__.truffle_runtime == 'Interpreted' else 10
57+
STABILIZES_AT = int(os.environ.get('GRAALPY_TEST_INDIRECT_CALL_STABILIZES_AT', STABILIZES_AT))
58+
59+
def was_stack_walk(new_value):
60+
if sys.implementation.name == "graalpy":
61+
return __graalpython__.was_stack_walk(new_value)
62+
return False
63+
64+
65+
IndirectCApiCallTester = CPyExtType(
66+
'IndirectCApiCallTester',
67+
code='''
68+
static PyObject* IndirectCApiCallTester_call(PyObject* self, PyObject *callable) {
69+
return PyObject_CallNoArgs(callable);
70+
}
71+
''',
72+
tp_methods='''{"call", (PyCFunction)IndirectCApiCallTester_call, METH_O, ""}''',
73+
)
74+
75+
# === capturing frame
76+
77+
def check_get_frame_no_deopt(forwarding_call, *args):
78+
def callee():
79+
return sys._getframe(1)
80+
81+
def fun(x):
82+
known_local = x
83+
return forwarding_call(callee, *args)
84+
85+
for i in range(NUM_ITERATIONS):
86+
assert fun(i * 2).f_locals['known_local'] == i * 2
87+
if i <= STABILIZES_AT:
88+
# just reset the flag
89+
was_stack_walk(False)
90+
else:
91+
assert not was_stack_walk(False), f"{i=}"
92+
93+
def test_capi_get_frame():
94+
check_get_frame_no_deopt(IndirectCApiCallTester().call)
95+
96+
def check_get_frame_indirect_call_tester(indirect_call_type):
97+
if sys.implementation.name == "graalpy":
98+
check_get_frame_no_deopt(__graalpython__.indirect_call_tester, indirect_call_type)
99+
else:
100+
check_get_frame_no_deopt(IndirectCApiCallTester().call)
101+
102+
def test_truffle_boundary_call_get_frame():
103+
check_get_frame_indirect_call_tester(TYPE_INDIRECT_BOUNDARY)
104+
105+
def test_interop_call_get_frame():
106+
check_get_frame_indirect_call_tester(TYPE_INDIRECT_INTEROP_CALL)
107+
108+
def test_interop_get_frame():
109+
check_get_frame_indirect_call_tester(TYPE_INDIRECT_INTEROP)
110+
111+
def test_truffle_boundary_interop_call_get_frame():
112+
check_get_frame_indirect_call_tester(TYPE_INDIRECT_BOUNDARY_UNCACHED_INTEROP)
113+
114+
class Var:
115+
def __init__(self, name):
116+
self.name = name
117+
def __str__(self):
118+
self.escape_frame = sys._getframe(1)
119+
return str(self.escape_frame.f_locals[self.name])
120+
121+
def test_format_with_get_frame_in_str():
122+
secret_var = 42
123+
for i in range(NUM_ITERATIONS):
124+
assert "{}".format(Var("secret_var")) == "42"
125+
if i <= STABILIZES_AT:
126+
# just reset the flag
127+
was_stack_walk(False)
128+
else:
129+
assert not was_stack_walk(False), f"{i=}"
130+
131+
escape_frame = None
132+
class AttrGetter:
133+
def __getattribute__(self, name):
134+
global escape_frame
135+
escape_frame = sys._getframe(1)
136+
return escape_frame.f_locals[name]
137+
138+
139+
def test_obj_with_get_frame_in_getattribute():
140+
secret_var = 42
141+
for i in range(NUM_ITERATIONS):
142+
assert AttrGetter().secret_var == 42
143+
if i <= STABILIZES_AT:
144+
# just reset the flag
145+
was_stack_walk(False)
146+
else:
147+
assert not was_stack_walk(False), f"{i=}"
148+
149+
# === exception state
150+
151+
def check_get_ex_no_deopt(forwarding_call, *args):
152+
def callee1():
153+
return sys.exc_info()[1]
154+
155+
def callee2():
156+
return callee1()
157+
158+
def fun(msg):
159+
try:
160+
raise IndexError(str(msg))
161+
except IndexError as e:
162+
return forwarding_call(callee2, *args)
163+
164+
def check_ex(msg):
165+
ex = fun(str(msg))
166+
assert type(ex) == IndexError, f"{ex=}, {msg=}"
167+
assert str(ex) == str(msg), f"{ex=}, {msg=}"
168+
169+
for i in range(NUM_ITERATIONS):
170+
check_ex(i*2)
171+
if i <= max(STABILIZES_AT, 2):
172+
# uncached interpreter always passes exception down, so on first call we do not walk the stack
173+
# and do not invalidate the assumptions. We can proactively walk the stack just to invalidate
174+
# the assumptions if we ever find that this is a problem.
175+
was_stack_walk(False)
176+
else:
177+
assert not was_stack_walk(False)
178+
179+
def test_capi_get_ex():
180+
check_get_ex_no_deopt(IndirectCApiCallTester().call)
181+
182+
def check_get_ex_indirect_call_tester(indirect_call_type):
183+
pass
184+
if sys.implementation.name == "graalpy":
185+
check_get_ex_no_deopt(__graalpython__.indirect_call_tester, indirect_call_type)
186+
else:
187+
check_get_ex_no_deopt(IndirectCApiCallTester().call)
188+
189+
def test_truffle_boundary_call_get_ex():
190+
check_get_ex_indirect_call_tester(TYPE_INDIRECT_BOUNDARY)
191+
192+
def test_interop_call_get_ex():
193+
check_get_ex_indirect_call_tester(TYPE_INDIRECT_INTEROP_CALL)
194+
195+
def test_truffle_boundary_interop_call_get_ex():
196+
check_get_ex_indirect_call_tester(TYPE_INDIRECT_BOUNDARY_UNCACHED_INTEROP)
197+
198+
class ExStr:
199+
def __str__(self):
200+
self.escape_ex = sys.exc_info()[1]
201+
return str(self.escape_ex)
202+
203+
def test_format_with_get_ex_in_str():
204+
for i in range(NUM_ITERATIONS):
205+
try:
206+
raise IndexError(str(i))
207+
except:
208+
assert "{}".format(ExStr()) == str(i)
209+
if i <= max(STABILIZES_AT, 2):
210+
# see similar code above
211+
was_stack_walk(False)
212+
else:
213+
assert not was_stack_walk(False)
214+
215+
escape_ex = None
216+
class AttrGetterFromEx:
217+
def __getattribute__(self, name):
218+
global escape_ex
219+
escape_ex = sys.exc_info()[1]
220+
return str(escape_ex)
221+
222+
223+
def test_obj_with_get_ex_in_getattribute():
224+
for i in range(NUM_ITERATIONS):
225+
try:
226+
raise IndexError(str(i))
227+
except:
228+
assert AttrGetterFromEx().dummy_attribute == str(i)
229+
if i <= max(STABILIZES_AT, 2):
230+
# see similar code above
231+
was_stack_walk(False)
232+
else:
233+
assert not was_stack_walk(False)

0 commit comments

Comments
 (0)