Skip to content

Commit ed93a88

Browse files
committed
[GR-12264] Report side-effects in KEY_INFO
PullRequest: graalpython/256
2 parents 62e55b6 + 5e8d5b2 commit ed93a88

File tree

15 files changed

+759
-106
lines changed

15 files changed

+759
-106
lines changed

doc/INTEROP.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ Returns true for None only.
145145

146146
###### HAS_SIZE
147147
According to the Truffle interop contract answering `true` to `HAS_SIZE` implies
148-
that indexed element access is available. Thus, we answer `true` here only for
149-
(sub-)instances of `tuple`, `list`, `array.array`, `bytearray`, `bytes`, `str`,
150-
and `range`.
148+
that indexed element access is available. However, we cannot fully guarantee
149+
this. We may answer `true` here when the object has both a `__len__` field and a
150+
`__getitem__` field. If the object's length is reported >0, we also try to read
151+
the item `0` and if that fails, we answer `false`. If the object reports it's
152+
empty, we cannot know if a read with an index will actually work, but we'll
153+
report `true`.
151154

152155
###### GET_SIZE
153156
Calls `__len__`. Just because `GET_SIZE` returns something positive does not
@@ -159,27 +162,35 @@ knowing what the `__getitem__` method does with an integer argument. Use
159162
Returns true for those values that can be unboxed using the `UNBOX` message.
160163

161164
###### KEY_INFO
162-
This will lookup the key using `READ`, assume it is readable and writable, and
163-
check `IS_EXECUTABLE`.
165+
This will lookup the key using the Python MRO. It will check if it's readable
166+
and writable, and also check if it has side-effects based on wether it is an
167+
inherited descriptor (i.e., an object with `__get__`, `__set__`, and/or
168+
`__delete__`). If the owner of the key is mutable (the owner being the class the
169+
key is inherited from or the object itself) then `REMOVABLE` and `MODIFABLE` are
170+
true. If the object itself is mutable, `INSERTABLE` will also be true. Finally,
171+
if the attribute is a function or it is *not* a descriptor and has a `__call__`,
172+
we declare it `INOCABLE`. We don't do this for descriptors, because we would
173+
have to run the `__get__` method and this message should not have side-effects.
164174

165175
###### HAS_KEYS
166-
Returns true for any boxed Python object, so small integers, booleans, or floats
167-
usually don't return true.
176+
Always returns true.
168177

169178
###### KEYS
170-
This returns the direct attributes of the receiver object, which would usually
171-
be available through `__getattribute__`.
172-
173-
The `KEYS` message requires the returned object to have only `java.lang.String`
174-
items. If the object responds to `keys`, `values`, `items`, and `__getitem__`,
175-
we assume it is Mapping, and we present the result of the `keys` method in
176-
combination with the attributes if, and only if, all keys are strings. This is
177-
roughly parallel to how `READ` and `WRITE` would be handled for string keys.
179+
This returns the all attributes of the receiver object that would usually be
180+
available through `__getattribute__`, i.e., both inherited and direct
181+
attributes.
182+
183+
If the object responds to `keys`, `values`, `items`, and `__getitem__`, we
184+
assume it is Mapping, and we present the String result of the `keys` method in
185+
combination with the attributes, prefixed with `[` if, and only if, the request
186+
asked for _internal_ keys also. The `KEYS` message requires the returned object
187+
to have only `java.lang.String` items, so inlo String keys are added to the
188+
result set. The `[` prefix ensures that in our handling of `READ` and `WRITE`
189+
messages we also treat them as mapping entries, not attributes.
178190

179191
It's still possible that none of the keys can be `READ`: the `READ` message uses
180192
Python semantics for lookup, which means that an inherited descriptor with a
181-
`__get__` method may still come before the object's keys and do anything
182-
(including raising an `AttributeError`).
193+
`__get__` method or the `__getitem__` method may still intercept actual access.
183194

184195
###### IS_POINTER
185196
Returns true if the object is a Python function defined in a Python C extension

graalpython/com.oracle.graal.python.cext/include/truffle.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ void *truffle_assign_managed(void *dst, void *managed);
5454
void *truffle_deref_handle_for_managed(void *managed);
5555
bool truffle_cannot_be_handle(void *nativeHandle);
5656

57+
// wrapping functions
58+
void *truffle_decorate_function(void *function, void *wrapper);
59+
5760
/*
5861
* All function below here are deprecated and will be removed in a future release.
5962
* Use the equivalent functions from <polyglot.h> instead.

graalpython/com.oracle.graal.python.tck/src/com/oracle/graal/python/tck/PythonProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public Collection<? extends Snippet> createExpressions(Context context) {
165165

166166
// dictionaries
167167
addExpressionSnippet(context, snippets, "dict", "lambda: { 'x': 4 }", OBJECT);
168-
addExpressionSnippet(context, snippets, "dict", "lambda: { 'a': 1, 'b': 2, 'c': 3 }", OBJECT, new PDictMemberVerifier(arr("a", "b", "c"), arr("x", "y", "z")));
168+
addExpressionSnippet(context, snippets, "dict", "lambda: { 'a': 1, 'b': 2, 'c': 3 }", OBJECT, new PDictMemberVerifier(arr("get", "keys", "update"), arr("a", "b", "c")));
169169

170170
// @formatter:on
171171
return snippets;

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/debug/PythonDebugTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
package com.oracle.graal.python.test.debug;
4242

4343
import static org.junit.Assert.assertEquals;
44+
import static org.junit.Assert.assertFalse;
4445
import static org.junit.Assert.assertNotNull;
46+
import static org.junit.Assert.assertTrue;
4547

4648
import java.util.Arrays;
4749
import java.util.HashMap;
@@ -326,6 +328,76 @@ public void testReenterArgumentsAndValues() throws Throwable {
326328
}
327329
}
328330

331+
@Test
332+
public void testGettersSetters() throws Throwable {
333+
final Source source = Source.newBuilder("python", "" +
334+
"class GetterOnly:\n" +
335+
" def __get__(self):\n" +
336+
" return 42\n" +
337+
"\n" +
338+
"class P:\n" +
339+
" def __init__(self):\n" +
340+
" self.__x = None\n" +
341+
" self.__y = None\n" +
342+
" self.__nx = 0\n" +
343+
" self.__ny = 0\n" +
344+
"\n" +
345+
" @property\n" +
346+
" def x(self):\n" +
347+
" self.__nx += 1\n" +
348+
" return self.__x\n" +
349+
"\n" +
350+
" @x.setter\n" +
351+
" def x(self, value):\n" +
352+
" self.__nx += 1\n" +
353+
" self.__x = value\n" +
354+
"\n" +
355+
" y = GetterOnly()\n" +
356+
"\n" +
357+
"p = P()\n" +
358+
"str(p)\n" +
359+
"\n", "testGettersSetters.py").buildLiteral();
360+
try (DebuggerSession session = tester.startSession()) {
361+
session.suspendNextExecution();
362+
tester.startEval(source);
363+
expectSuspended((SuspendedEvent event) -> {
364+
DebugStackFrame frame = event.getTopStackFrame();
365+
assertEquals(1, frame.getSourceSection().getStartLine());
366+
event.prepareStepOver(7);
367+
});
368+
expectSuspended((SuspendedEvent event) -> {
369+
DebugStackFrame frame = event.getTopStackFrame();
370+
assertEquals("p = P()", frame.getSourceSection().getCharacters().toString());
371+
event.prepareStepOver(1);
372+
});
373+
expectSuspended((SuspendedEvent event) -> {
374+
DebugValue p = session.getTopScope("python").getDeclaredValue("p");
375+
DebugValue x = p.getProperty("x");
376+
assertTrue(x.hasReadSideEffects());
377+
assertTrue(x.hasWriteSideEffects());
378+
assertTrue(x.isReadable());
379+
assertTrue(x.isWritable());
380+
DebugValue nx = p.getProperty("__nx");
381+
assertEquals(0, nx.as(Number.class).intValue());
382+
assertEquals("None", x.as(String.class));
383+
assertEquals(1, nx.as(Number.class).intValue());
384+
x.set(42);
385+
assertEquals(2, nx.as(Number.class).intValue());
386+
assertEquals("42", x.as(String.class));
387+
assertEquals(3, nx.as(Number.class).intValue());
388+
DebugValue y = p.getProperty("y");
389+
assertTrue(y.hasReadSideEffects());
390+
assertFalse(y.hasWriteSideEffects());
391+
assertTrue(y.isReadable());
392+
assertTrue(y.isWritable());
393+
DebugValue ny = p.getProperty("__ny");
394+
assertEquals(0, ny.as(Number.class).intValue());
395+
y.set(24);
396+
assertEquals("24", y.as(String.class));
397+
});
398+
}
399+
}
400+
329401
private void expectSuspended(SuspendedCallback callback) {
330402
tester.expectSuspended(callback);
331403
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646

4747
import java.io.ByteArrayOutputStream;
4848
import java.io.IOException;
49-
import java.util.Set;
49+
import java.util.Arrays;
50+
import java.util.List;
5051

5152
import org.graalvm.polyglot.Context;
5253
import org.graalvm.polyglot.Context.Builder;
@@ -227,14 +228,14 @@ public void accessSuitePy() throws IOException {
227228

228229
Value libraries = suite.getMember("libraries");
229230
assertNotNull("libraries found", libraries);
230-
final Set<String> suiteKeys = suite.getMemberKeys();
231+
final List<Object> suiteKeys = Arrays.asList(suite.invokeMember("keys").as(Object[].class));
231232
assertTrue("Libraries found among keys: " + suiteKeys, suiteKeys.contains("libraries"));
232233

233234
Value dacapo = null;
234-
for (String k : libraries.getMemberKeys()) {
235+
for (Object k : libraries.invokeMember("keys").as(List.class)) {
235236
System.err.println("k " + k);
236237
if ("DACAPO".equals(k)) {
237-
dacapo = libraries.getMember(k);
238+
dacapo = libraries.getMember((String) k);
238239
}
239240
}
240241
assertNotNull("Dacapo found", dacapo);

graalpython/com.oracle.graal.python.test/src/tests/test_interop.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def test_import():
4747
import python_cext
4848
assert imported_cext is python_cext
4949

50+
class GetterOnly():
51+
def __get__(self, instance, owner):
52+
pass
53+
5054
class CustomObject():
5155
field = 42
5256

@@ -56,6 +60,16 @@ def __getitem__(self, item):
5660
def __len__(self):
5761
return 21
5862

63+
getter = GetterOnly()
64+
65+
@property
66+
def setter(self):
67+
pass
68+
69+
@setter.setter
70+
def setter_setter(self):
71+
pass
72+
5973
class CustomMutable(CustomObject):
6074
_items = {}
6175

@@ -195,11 +209,31 @@ def test_has_keys():
195209

196210
def test_keys():
197211
o = CustomObject()
198-
assert len(polyglot.__keys__(o)) == 0
212+
inherited_keys = len(polyglot.__keys__(o))
199213
o.my_field = 1
200-
assert len(polyglot.__keys__(o)) == 1
214+
assert len(polyglot.__keys__(o)) == 1 + inherited_keys
201215
assert "my_field" in polyglot.__keys__(o)
202216

217+
def test_key_info():
218+
o = CustomObject()
219+
o.my_field = 1
220+
o.test_exec = lambda: False
221+
readable_invokable_insertable = polyglot.__key_info__(o, "__init__")
222+
223+
readable_invokable_modifiable_insertable = polyglot.__key_info__(o, "__len__")
224+
assert readable_invokable_modifiable_insertable == polyglot.__key_info__(o, "test_exec")
225+
226+
readable_modifiable_insertable = polyglot.__key_info__(o, "field")
227+
assert readable_modifiable_insertable == polyglot.__key_info__(o, "my_field")
228+
229+
assert readable_invokable_insertable != readable_modifiable_insertable
230+
assert readable_invokable_insertable != readable_invokable_modifiable_insertable
231+
assert readable_modifiable_insertable != readable_invokable_modifiable_insertable
232+
233+
sideeffects_get = polyglot.__key_info__(o, "getter")
234+
sideeffects_get_set = polyglot.__key_info__(o, "setter")
235+
assert sideeffects_get != sideeffects_get_set
236+
203237
def test_host_lookup():
204238
import java
205239
try:

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.oracle.graal.python.builtins.objects.PNone;
3939
import com.oracle.graal.python.builtins.objects.PythonAbstractObject;
4040
import com.oracle.graal.python.builtins.objects.code.PCode;
41+
import com.oracle.graal.python.builtins.objects.dict.PDict;
4142
import com.oracle.graal.python.builtins.objects.function.PArguments;
4243
import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction;
4344
import com.oracle.graal.python.builtins.objects.function.PFunction;
@@ -62,6 +63,7 @@
6263
import com.oracle.graal.python.runtime.PythonOptions;
6364
import com.oracle.graal.python.runtime.PythonParser.ParserMode;
6465
import com.oracle.graal.python.runtime.exception.PException;
66+
import com.oracle.graal.python.runtime.interop.InteropMap;
6567
import com.oracle.truffle.api.Assumption;
6668
import com.oracle.truffle.api.CallTarget;
6769
import com.oracle.truffle.api.CompilerAsserts;
@@ -338,6 +340,7 @@ protected boolean isVisible(PythonContext context, Object value) {
338340
}
339341

340342
@Override
343+
@TruffleBoundary
341344
protected Iterable<Scope> findLocalScopes(PythonContext context, Node node, Frame frame) {
342345
ArrayList<Scope> scopes = new ArrayList<>();
343346
for (Scope s : super.findLocalScopes(context, node, frame)) {
@@ -346,7 +349,7 @@ protected Iterable<Scope> findLocalScopes(PythonContext context, Node node, Fram
346349
if (frame != null) {
347350
PythonObject globals = PArguments.getGlobalsSafe(frame);
348351
if (globals != null) {
349-
scopes.add(Scope.newBuilder("globals()", globals).build());
352+
scopes.add(Scope.newBuilder("globals()", scopeFromObject(globals)).build());
350353
}
351354
Frame generatorFrame = PArguments.getGeneratorFrameSafe(frame);
352355
if (generatorFrame != null) {
@@ -358,13 +361,21 @@ protected Iterable<Scope> findLocalScopes(PythonContext context, Node node, Fram
358361
return scopes;
359362
}
360363

364+
private static InteropMap scopeFromObject(PythonObject globals) {
365+
if (globals instanceof PDict) {
366+
return InteropMap.fromPDict((PDict) globals);
367+
} else {
368+
return InteropMap.fromPythonObject(globals);
369+
}
370+
}
371+
361372
@Override
362373
protected Iterable<Scope> findTopScopes(PythonContext context) {
363374
ArrayList<Scope> scopes = new ArrayList<>();
364375
if (context.getBuiltins() != null) {
365376
// false during initialization
366-
scopes.add(Scope.newBuilder("__main__", context.getMainModule()).build());
367-
scopes.add(Scope.newBuilder("builtins", context.getBuiltins()).build());
377+
scopes.add(Scope.newBuilder("__main__", scopeFromObject(context.getMainModule())).build());
378+
scopes.add(Scope.newBuilder("builtins", scopeFromObject(context.getBuiltins())).build());
368379
}
369380
return scopes;
370381
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/InteropModuleBuiltins.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,14 @@
5454
import com.oracle.graal.python.builtins.PythonBuiltins;
5555
import com.oracle.graal.python.builtins.objects.PNone;
5656
import com.oracle.graal.python.builtins.objects.function.PythonCallable;
57+
import com.oracle.graal.python.nodes.SpecialAttributeNames;
5758
import com.oracle.graal.python.nodes.function.PythonBuiltinNode;
59+
import com.oracle.graal.python.runtime.PythonContext;
60+
import com.oracle.graal.python.runtime.PythonCore;
61+
import com.oracle.graal.python.runtime.PythonOptions;
5862
import com.oracle.graal.python.runtime.exception.PythonErrorType;
5963
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
64+
import com.oracle.truffle.api.TruffleFile;
6065
import com.oracle.truffle.api.TruffleLanguage.Env;
6166
import com.oracle.truffle.api.dsl.Cached;
6267
import com.oracle.truffle.api.dsl.Fallback;
@@ -84,6 +89,23 @@ protected List<com.oracle.truffle.api.dsl.NodeFactory<? extends PythonBuiltinNod
8489
return InteropModuleBuiltinsFactory.getFactories();
8590
}
8691

92+
@Override
93+
public void initialize(PythonCore core) {
94+
super.initialize(core);
95+
96+
PythonContext context = core.getContext();
97+
Env env = context.getEnv();
98+
String coreHome = PythonOptions.getOption(context, PythonOptions.CoreHome);
99+
try {
100+
TruffleFile coreDir = env.getTruffleFile(coreHome);
101+
TruffleFile docDir = coreDir.resolveSibling("doc");
102+
if (docDir.exists() || (docDir = coreDir.getParent().resolveSibling("doc")).exists()) {
103+
builtinConstants.put(SpecialAttributeNames.__DOC__, new String(docDir.resolve("INTEROP.md").readAllBytes()));
104+
}
105+
} catch (SecurityException | IOException e) {
106+
}
107+
}
108+
87109
@Builtin(name = "import_value", minNumOfPositionalArgs = 1, keywordArguments = {"name"})
88110
@GenerateNodeFactory
89111
public abstract static class ImportNode extends PythonBuiltinNode {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,14 @@ public void initialize(PythonCore core) {
138138
true, // dont_write_bytecode
139139
false, // hash_randomization
140140
false, // ignore_environment
141-
PythonOptions.getOption(core.getContext(), PythonOptions.InspectFlag).booleanValue(), // inspect
142-
PythonOptions.getOption(core.getContext(), PythonOptions.InspectFlag).booleanValue(), // interactive
141+
PythonOptions.getFlag(core.getContext(), PythonOptions.InspectFlag), // inspect
142+
PythonOptions.getFlag(core.getContext(), PythonOptions.InspectFlag), // interactive
143143
false, // isolated
144-
PythonOptions.getOption(core.getContext(), PythonOptions.NoSiteFlag).booleanValue(), // no_site
145-
PythonOptions.getOption(core.getContext(), PythonOptions.NoUserSiteFlag).booleanValue(), // no_user_site
144+
PythonOptions.getFlag(core.getContext(), PythonOptions.NoSiteFlag), // no_site
145+
PythonOptions.getFlag(core.getContext(), PythonOptions.NoUserSiteFlag), // no_user_site
146146
false, // optimize
147-
PythonOptions.getOption(core.getContext(), PythonOptions.QuietFlag).booleanValue(), // quiet
148-
PythonOptions.getOption(core.getContext(), PythonOptions.VerboseFlag).booleanValue(), // verbose
147+
PythonOptions.getFlag(core.getContext(), PythonOptions.QuietFlag), // quiet
148+
PythonOptions.getFlag(core.getContext(), PythonOptions.VerboseFlag), // verbose
149149
}));
150150
builtinConstants.put("graal_python_core_home", PythonOptions.getOption(core.getContext(), PythonOptions.CoreHome));
151151
builtinConstants.put("graal_python_stdlib_home", PythonOptions.getOption(core.getContext(), PythonOptions.StdLibHome));

0 commit comments

Comments
 (0)