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
37 changes: 18 additions & 19 deletions truffle/docs/DynamicObjectModel.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ permalink: /graalvm-as-a-platform/language-implementation-framework/DynamicObjec
---
# Dynamic Object Model

This guide demonstrates how to get started with using the [DynamicObject](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/DynamicObject.html) and [DynamicObjectLibrary](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/DynamicObjectLibrary.html) APIs introduced with GraalVM 20.2.0.
The full documentation can be found in the [Javadoc](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/DynamicObjectLibrary.html).
This guide demonstrates how to use the [`DynamicObject`](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/DynamicObject.html) and node APIs introduced with GraalVM 25.1.
The full documentation can be found in the [Javadoc](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/DynamicObject.html).

### Motivation

Expand Down Expand Up @@ -61,7 +61,7 @@ public class Array extends BasicObject {
}
```

Dynamic object members can be accessed using the `DynamicObjectLibrary`, which can be obtained using the `@CachedLibrary` annotation of the Truffle DSL and `DynamicObjectLibrary.getFactory()` + `getUncached()`, `create(DynamicObject)`, and `createDispatched(int)`.
You can access dynamic object members through `DynamicObject` access nodes. To obtain these nodes, cache them using the `@Cached` annotation provided in the Truffle DSL.
Here is an example of how it could be used to implement `InteropLibrary` messages:
```java
@ExportLibrary(InteropLibrary.class)
Expand All @@ -78,9 +78,9 @@ public class SimpleObject extends BasicObject {

@ExportMessage
Object readMember(String name,
@CachedLibrary("this") DynamicObjectLibrary objectLibrary)
@Cached DynamicObject.GetNode getNode)
throws UnknownIdentifierException {
Object result = objectLibrary.getOrDefault(this, name, null);
Object result = getNode.execute(this, name, null);
if (result == null) {
/* Property does not exist. */
throw UnknownIdentifierException.create(name);
Expand All @@ -90,14 +90,14 @@ public class SimpleObject extends BasicObject {

@ExportMessage
void writeMember(String name, Object value,
@CachedLibrary("this") DynamicObjectLibrary objectLibrary) {
objectLibrary.put(this, name, value);
@Cached DynamicObject.PutNode putNode) {
putNode.execute(this, name, value);
}

@ExportMessage
boolean isMemberReadable(String member,
@CachedLibrary("this") DynamicObjectLibrary objectLibrary) {
return objectLibrary.containsKey(this, member);
@Cached DynamicObject.ContainsKeyNode containsKeyNode) {
return containsKeyNode.execute(this, member);
}
// ...
}
Expand All @@ -106,7 +106,7 @@ public class SimpleObject extends BasicObject {
In order to construct instances of these objects, you first need a `Shape` that you can pass to the `DynamicObject` constructor.
This shape is created using [`Shape.newBuilder().build()`](https://www.graalvm.org/truffle/javadoc/com/oracle/truffle/api/object/Shape.Builder.html).
The returned shape describes the initial shape of the object and forms the root of a new shape tree.
As you are adding new properties with `DynamicObjectLibrary#put`, the object will mutate into other shapes in this shape tree.
As you are adding new properties with `DynamicObject.PutNode#execute`, the object will mutate into other shapes in this shape tree.

Note: You should reuse the same initial shapes because shapes are internally cached per root shape.
It is recommended that you store the initial shapes in the `TruffleLanguage` instance, so they can be shared across contexts of the same engine.
Expand Down Expand Up @@ -137,7 +137,7 @@ public final class MyLanguage extends TruffleLanguage<MyContext> {

You can extend the default object layout with extra _dynamic fields_ that you hand over to the dynamic object model by adding `@DynamicField`-annotated field declarations of type `Object` or `long` in your subclasses, and specifying the _layout class_ with `Shape.newBuilder().layout(ExtendedObject.class).build();`.
Dynamic fields declared in this class and its superclasses will then automatically be used to store dynamic object properties and allow faster access to properties that fit into this reserved space.
Note: You must not access dynamic fields directly. Always use `DynamicObjectLibrary` for this purpose.
Note: You must not access dynamic fields directly. Always use `DynamicObject` nodes for this purpose.

```java
@ExportLibrary(InteropLibrary.class)
Expand All @@ -158,21 +158,20 @@ public class ExtendedObject extends SimpleObject {

## Caching Considerations

In order to ensure optimal caching, avoid reusing the same cached `DynamicObjectLibrary` for multiple, independent operations (`get`, `put`, etc.).
Try to minimize the number of different shapes and property keys seen by each cached library instance.
When the property keys are known statically (compilation-final), always use a separate `DynamicObjectLibrary` for each property key.
Use dispatched libraries (`@CachedLibrary(limit=...)`) when putting multiple properties in succession.
To ensure optimal caching, avoid reusing the same cached `DynamicObject` node (`GetNode`, `PutNode`, etc.) for multiple independent operations.
Try to minimize the number of different shapes and property keys that each cached node instance encounters.
When the property keys are known statically (compilation-final), always use a separate `DynamicObject` node for each property key.
For example:
```java
public abstract class MakePairNode extends BinaryExpressionNode {
@Specialization
Object makePair(Object left, Object right,
@CachedLanguage MyLanguage language,
@CachedLibrary(limit = "3") DynamicObjectLibrary putLeft,
@CachedLibrary(limit = "3") DynamicObjectLibrary putRight) {
@Cached DynamicObject.PutNode putLeft,
@Cached DynamicObject.PutNode putRight) {
MyObject obj = language.createObject();
putLeft.put(obj, "left", left);
putRight.put(obj, "right", right);
putLeft.execute(obj, "left", left);
putRight.execute(obj, "right", right);
return obj;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ public void testPropertyFlags() {
Assert.assertEquals(f3, uncachedGetProperty(o1, k1).getFlags());

Shape before = o1.getShape();
assertTrue(lib.setPropertyFlags(o1, k1, f3));
assertEquals(!(run == TestRun.CACHED_NODES || run == TestRun.UNCACHED_NODES), lib.setPropertyFlags(o1, k1, f3));
assertFalse(updatePropertyFlags(lib, o1, k1, f -> f | f2));
assertEquals(f3, lib.getPropertyFlagsOrDefault(o1, k1, -1));
Assert.assertEquals(f3, uncachedGetProperty(o1, k1).getFlags());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
Expand Down Expand Up @@ -133,6 +133,13 @@ private DynamicObject.PutConstantNode createPutConstantNode() {
};
}

private DynamicObject.PutAllNode createPutAllNode() {
return switch (run) {
case CACHED -> DynamicObject.PutAllNode.create();
case UNCACHED -> DynamicObject.PutAllNode.getUncached();
};
}

private DynamicObject.CopyPropertiesNode createCopyPropertiesNode() {
return switch (run) {
case CACHED -> DynamicObject.CopyPropertiesNode.create();
Expand Down Expand Up @@ -238,13 +245,6 @@ private DynamicObject.GetPropertyNode createGetPropertyNode() {
};
}

private DynamicObject.GetPropertyNode createGetPropertyNodeForKey(@SuppressWarnings("unused") Object k) {
return switch (run) {
case CACHED -> DynamicObject.GetPropertyNode.create();
case UNCACHED -> DynamicObject.GetPropertyNode.getUncached();
};
}

private DynamicObject.GetKeyArrayNode createGetKeyArrayNode() {
return switch (run) {
case CACHED -> DynamicObject.GetKeyArrayNode.create();
Expand Down Expand Up @@ -686,19 +686,19 @@ public void testHasAddShapeFlags() {
assertTrue(hasShapeFlags(getShapeFlagsNode, o1, flags));
assertTrue(hasShapeFlags(getShapeFlagsNode, o1, 0b10));
assertFalse(hasShapeFlags(getShapeFlagsNode, o1, 0b11));
addShapeFlags(getShapeFlagsNode, setShapeFlagsNode, o1, 0b1);
setShapeFlagsNode.executeAdd(o1, 0b1);
assertTrue(hasShapeFlags(getShapeFlagsNode, o1, 0b11));
assertEquals(flags | 0b1, getShapeFlagsNode.execute(o1));
setShapeFlagsNode.executeRemove(o1, 0b1);
assertEquals(flags, getShapeFlagsNode.execute(o1));
setShapeFlagsNode.executeRemoveAndAdd(o1, 0b1111, 0b0101);
assertEquals(0b100101, getShapeFlagsNode.execute(o1));
}

static boolean hasShapeFlags(DynamicObject.GetShapeFlagsNode getShapeFlagsNode, DynamicObject obj, int flags) {
return (getShapeFlagsNode.execute(obj) & flags) == flags;
}

static boolean addShapeFlags(DynamicObject.GetShapeFlagsNode getShapeFlagsNode, DynamicObject.SetShapeFlagsNode setShapeFlagsNode, DynamicObject obj, int flags) {
return setShapeFlagsNode.execute(obj, (getShapeFlagsNode.execute(obj) | flags));
}

@Test
public void testMakeShared() {
String key = "key";
Expand Down Expand Up @@ -728,20 +728,21 @@ public void testPropertyFlags() {

DynamicObject.SetPropertyFlagsNode setPropertyFlagsNode = createSetPropertyFlagsNodeForKey(k1);
DynamicObject.GetPropertyFlagsNode getPropertyFlagsNode = createGetPropertyFlagsNodeForKey(k1);
DynamicObject.GetPropertyNode getPropertyNode = createGetPropertyNodeForKey(k1);

DynamicObject o1 = createEmpty();
uncachedPut(o1, k1, v1, 0);
assertTrue(setPropertyFlagsNode.execute(o1, k1, f1));
assertEquals(f1, getPropertyFlagsNode.execute(o1, k1, -1));
assertEquals(f1, uncachedGetProperty(o1, k1).getFlags());
assertTrue(updatePropertyFlags(getPropertyNode, setPropertyFlagsNode, o1, k1, f -> f | f2));
assertTrue(setPropertyFlagsNode.executeAdd(o1, k1, f2));
assertEquals(f3, getPropertyFlagsNode.execute(o1, k1, -1));
assertFalse(setPropertyFlagsNode.executeAdd(o1, k1, f2));
assertEquals(f3, uncachedGetProperty(o1, k1).getFlags());

Shape before = o1.getShape();
assertTrue(setPropertyFlagsNode.execute(o1, k1, f3));
assertFalse(updatePropertyFlags(getPropertyNode, setPropertyFlagsNode, o1, k1, f -> f | f2));
assertFalse(setPropertyFlagsNode.execute(o1, k1, f3));
assertFalse(setPropertyFlagsNode.executeAdd(o1, k1, f2));
assertFalse(setPropertyFlagsNode.executeRemove(o1, k1, 0x100));
assertEquals(f3, getPropertyFlagsNode.execute(o1, k1, -1));
assertEquals(f3, uncachedGetProperty(o1, k1).getFlags());
assertSame(before, o1.getShape());
Expand All @@ -750,31 +751,21 @@ public void testPropertyFlags() {
uncachedPut(o2, k1, v2, 0);
assertTrue(setPropertyFlagsNode.execute(o2, k1, f1));
assertEquals(f1, getPropertyFlagsNode.execute(o2, k1, -1));
assertTrue(updatePropertyFlags(getPropertyNode, setPropertyFlagsNode, o2, k1, f -> f | f2));
assertTrue(setPropertyFlagsNode.executeAdd(o2, k1, f2));
assertEquals(f3, getPropertyFlagsNode.execute(o2, k1, -1));
assertSame(o1.getShape(), o2.getShape());
assertTrue(setPropertyFlagsNode.executeRemove(o2, k1, f1));
assertEquals(f2, getPropertyFlagsNode.execute(o2, k1, -1));
assertTrue(setPropertyFlagsNode.executeRemoveAndAdd(o2, k1, f3, f1));
assertEquals(f1, getPropertyFlagsNode.execute(o2, k1, -1));
assertFalse(setPropertyFlagsNode.executeRemoveAndAdd(o2, k1, f1, f1));
assertFalse(setPropertyFlagsNode.executeRemoveAndAdd(o2, k1, f3, f1));
assertEquals(f1, getPropertyFlagsNode.execute(o2, k1, -1));

DynamicObject o3 = createEmpty();
assertFalse(setPropertyFlagsNode.execute(o3, k1, f1));
}

private static boolean updatePropertyFlags(DynamicObject.GetPropertyNode getPropertyNode,
DynamicObject.SetPropertyFlagsNode setPropertyFlagsNode,
DynamicObject obj,
String key,
IntUnaryOperator updateFunction) {
Property property = getPropertyNode.execute(obj, key);
if (property == null) {
return false;
}
int oldFlags = property.getFlags();
int newFlags = updateFunction.applyAsInt(oldFlags);
if (oldFlags == newFlags) {
return false;
}
return setPropertyFlagsNode.execute(obj, key, newFlags);
}

@Test
public void testRemove() {
int v1 = 42;
Expand Down Expand Up @@ -827,10 +818,18 @@ public void testResetShape() {
Shape emptyShape = o1.getShape();
uncachedPut(o1, "key1", v1, 0);
uncachedPut(o1, "key2", v2, 0);
Shape nonEmptyShape = o1.getShape();
resetShapeNode.execute(o1, emptyShape);
assertSame(emptyShape, o1.getShape());

assumeTrue("new layout only", isNewLayout());
try {
resetShapeNode.execute(o1, nonEmptyShape);
fail("should have thrown IllegalArgumentException");
} catch (IllegalArgumentException e) {
// expected
assertSame(emptyShape, o1.getShape());
}

int flags = 0xf;
DynamicObject o2 = createEmpty();
Shape newEmptyShape = Shape.newBuilder().shapeFlags(flags).build();
Expand Down Expand Up @@ -971,9 +970,7 @@ public void testPutConstant2() {
assertEquals(v1, uncachedGet(o1, k1));

putNode.executeWithFlags(o1, k1, v2, flags);
if (isNewLayout()) {
assertFalse(o1.getShape().getProperty(k1).getLocation().isConstant());
}
assertFalse(o1.getShape().getProperty(k1).getLocation().isConstant());
assertEquals(flags, o1.getShape().getProperty(k1).getFlags());
assertEquals(v2, uncachedGet(o1, k1));
}
Expand Down Expand Up @@ -1014,6 +1011,46 @@ public void testPropertyAndShapeFlags() {
assertEquals(1, getNode.execute(o3, "k13", null));
}

@Test
public void testPutAll() {
DynamicObject o1 = createEmpty();
DynamicObject o2 = createEmpty();
DynamicObject o3 = createEmpty();
DynamicObject o4 = createEmpty();
String k1 = "key1";
String k2 = "key2";
String k3 = "key3";
String k4 = "key4";
int v1 = 42;
int v2 = 43;
String v3 = "asdf";
String v4 = "qwer";
String[] keys = {k1, k2, k3};
Object[] values1 = {v1, v2, v3};
Object[] values2 = {v1, v4, v3};
uncachedPutAll(o1, keys, values1);
DynamicObject.PutAllNode putAllNode = createPutAllNode();
putAllNode.execute(o2, keys, values1);
assertSame(o1.getShape(), o2.getShape());
putAllNode.execute(o3, keys, values1);
putAllNode.execute(o3, keys, values2);
putAllNode.execute(o4, keys, values1);
String[] keys4 = {k4};
DynamicObject.PutAllNode putAllNode4 = createPutAllNode();
putAllNode4.executeIfPresent(o3, keys4, new Object[]{v4});
assertFalse(uncachedContainsKey(o3, k4));
assertNull(uncachedGet(o4, k4));
putAllNode4.executeIfAbsent(o4, keys4, new Object[]{v4});
assertTrue(uncachedContainsKey(o4, k4));
assertEquals(v4, uncachedGet(o4, k4));
for (int i = 0; i < keys.length; i++) {
assertEquals(values1[i], uncachedGet(o1, keys[i]));
assertEquals(values1[i], uncachedGet(o2, keys[i]));
assertEquals(values2[i], uncachedGet(o3, keys[i]));
assertEquals(values1[i], uncachedGet(o4, keys[i]));
}
}

private void fillObjectWithProperties(DynamicObject obj, boolean b) {
DynamicObject.PutNode putNode = createPutNode();

Expand Down Expand Up @@ -1078,6 +1115,14 @@ private static int uncachedGetPropertyFlags(DynamicObject obj, Object key, int d
return DynamicObject.GetPropertyFlagsNode.getUncached().execute(obj, key, defaultValue);
}

private static boolean uncachedContainsKey(DynamicObject obj, Object key) {
return DynamicObject.ContainsKeyNode.getUncached().execute(obj, key);
}

private static void uncachedPutAll(DynamicObject obj, Object[] keys, Object[] values) {
DynamicObject.PutAllNode.getUncached().execute(obj, keys, values);
}

private static Object newObjectType() {
return new Object() {
};
Expand All @@ -1087,10 +1132,6 @@ private static List<Object> getKeyList(DynamicObject obj) {
return Arrays.asList(DynamicObject.GetKeyArrayNode.getUncached().execute(obj));
}

private static boolean isNewLayout() {
return true;
}

@GenerateInline(false)
public abstract static class TestGet extends Node {
public abstract Object execute(DynamicObject obj);
Expand Down
Loading