Skip to content
Open
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
101 changes: 101 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/stdlib/base/DynamicNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.runtime.*;
import org.pkl.core.stdlib.ExternalMethod0Node;
import org.pkl.core.stdlib.ExternalMethod1Node;
Expand Down Expand Up @@ -113,4 +116,102 @@ protected VmObjectLike eval(VmDynamic self, VmClass clazz) {
return result;
}
}

public abstract static class toMixin extends ExternalMethod0Node {
@Specialization
protected VmFunction eval(VmDynamic self) {
var rootNode = new DynamicToMixinRootNode(self);
return new VmFunction(
VmUtils.createEmptyMaterializedFrame(),
null,
1,
rootNode,
null);
}
}

private static final class DynamicToMixinRootNode extends org.pkl.core.ast.PklRootNode {
private final VmDynamic sourceDynamic;

public DynamicToMixinRootNode(VmDynamic sourceDynamic) {
super(null, new FrameDescriptor());
this.sourceDynamic = sourceDynamic;
}

@Override
public com.oracle.truffle.api.source.SourceSection getSourceSection() {
return VmUtils.unavailableSourceSection();
}

@Override
public String getName() {
return "toMixin";
}

@Override
protected Object executeImpl(VirtualFrame frame) {
var arguments = frame.getArguments();
if (arguments.length != 3) {
CompilerDirectives.transferToInterpreter();
throw new VmExceptionBuilder()
.evalError("wrongFunctionArgumentCount", 1, arguments.length - 2)
.build();
}

var targetObject = arguments[2];

// Create a new VmDynamic with targetObject as parent
// and sourceDynamic's members
if (!(targetObject instanceof VmObject)) {
CompilerDirectives.transferToInterpreter();
throw new VmExceptionBuilder()
.typeMismatch(targetObject, BaseModule.getDynamicClass())
.build();
}

var parent = (VmObject) targetObject;
// Get parent length - only VmDynamic and VmListing have elements
var parentLength = (parent instanceof VmDynamic) ? ((VmDynamic) parent).getLength() : 0;

// Create adjusted members map with offset element indices
var adjustedMembers = adjustMemberIndices(sourceDynamic.getMembers(), parentLength);

var result = new VmDynamic(
sourceDynamic.getEnclosingFrame(),
parent,
adjustedMembers,
parentLength + sourceDynamic.getLength());

return result;
}

// Adjust element indices in the members map by offsetting them by parentLength
private static org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> adjustMemberIndices(
org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> members,
long parentLength) {
if (parentLength == 0) {
return members;
}

var result = org.pkl.core.util.EconomicMaps.<Object, ObjectMember>create(
org.pkl.core.util.EconomicMaps.size(members));

var cursor = members.getEntries();
while (cursor.advance()) {
var key = cursor.getKey();
var member = cursor.getValue();

// If the key is a Long (element index), offset it by parentLength
if (key instanceof Long) {
Comment on lines +204 to +205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a sound assumption to make. Mappings and Dynamics can have entries with Int (in Pkl, Long in Java) keys. Probably better to check member.isElement() here instead (and you can then assume the key is a Long).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the internal field representation, not the userland defined keys. I will see if I can get the key to be the right type at compile time and replace this check

var newKey = (Long) key + parentLength;
org.pkl.core.util.EconomicMaps.put(result, newKey, member);
} else {
// Properties and entries are not offset
org.pkl.core.util.EconomicMaps.put(result, key, member);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
amends "../snippetTest.pkl"

local class Person {
name: String
age: Int = 0
}

examples {
["basic conversion"] {
// Convert Dynamic with properties to Mixin
local dynamic1 = new Dynamic {
name = "Pigeon"
age = 42
}
local mixin1 = dynamic1.toMixin()
new Dynamic { name = "Original" } |> mixin1
}

["empty Dynamic"] {
// Empty Dynamic should create identity mixin
local emptyDynamic = new Dynamic {}
local emptyMixin = emptyDynamic.toMixin()
new Dynamic { existing = "value" } |> emptyMixin
}

["with elements"] {
// Elements should be appended
local dynamicWithElements = new Dynamic {
"element1"
"element2"
}
local mixinWithElements = dynamicWithElements.toMixin()
new Dynamic { "base" } |> mixinWithElements
}

["with entries"] {
// Entries should be merged
local dynamicWithEntries = new Dynamic {
["key1"] = "value1"
["key2"] = "value2"
}
local mixinWithEntries = dynamicWithEntries.toMixin()
new Dynamic { ["baseKey"] = "baseValue" } |> mixinWithEntries
}

["with mixed members"] {
// Properties, elements, and entries should all be merged
local dynamicMixed = new Dynamic {
prop = "property"
"element"
["entry"] = "entryValue"
}
local mixinMixed = dynamicMixed.toMixin()
new Dynamic { baseProp = "base" } |> mixinMixed
}

["applying to Typed object"] {
// Mixin should work with typed objects
local dynamicPerson = new Dynamic {
name = "Modified"
age = 100
}
local mixinPerson = dynamicPerson.toMixin()
new Person { name = "Original"; age = 20 } |> mixinPerson
}

["chaining mixins"] {
// Multiple mixins can be chained
local mixin1 = new Dynamic { extra = "value" }.toMixin()
local mixin2 = new Dynamic { another = "field" }.toMixin()
new Dynamic { base = "start" } |> mixin1 |> mixin2
}

["reusable mixin"] {
// Mixin can be applied to multiple objects
local reusableMixin = new Dynamic { shared = "config" }.toMixin()
new {
first = new Dynamic { id = 1 } |> reusableMixin
second = new Dynamic { id = 2 } |> reusableMixin
}
}

["overriding properties"] {
// Properties from mixin should override base properties
local overrideDynamic = new Dynamic {
name = "Override"
value = 999
}
local overrideMixin = overrideDynamic.toMixin()
new Dynamic { name = "Original"; value = 1; other = "keep" } |> overrideMixin
}

["replacement vs merge for nested objects"] {
// Test replacement vs merge semantics
local base = new {
a1 {
b1 = 2
}
a2 {
b1 = 2
}
}
local overrideValue = new Dynamic {
a1 = new Dynamic {
b2 = 2
}
a2 {
b2 = 2
}
}
(base) |> overrideValue.toMixin()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
examples {
["basic conversion"] {
new {
name = "Pigeon"
age = 42
}
}
["empty Dynamic"] {
new {
existing = "value"
}
}
["with elements"] {
new {
"base"
"element1"
"element2"
}
}
["with entries"] {
new {
["baseKey"] = "baseValue"
["key1"] = "value1"
["key2"] = "value2"
}
}
["with mixed members"] {
new {
baseProp = "base"
prop = "property"
["entry"] = "entryValue"
"element"
}
}
["applying to Typed object"] {
new {
name = "Modified"
age = 100
}
}
["chaining mixins"] {
new {
base = "start"
extra = "value"
another = "field"
}
}
["reusable mixin"] {
new {
first {
id = 1
shared = "config"
}
second {
id = 2
shared = "config"
}
}
}
["overriding properties"] {
new {
name = "Override"
value = 999
other = "keep"
}
}
["replacement vs merge for nested objects"] {
new {
a1 {
b2 = 2
}
a2 {
b1 = 2
b2 = 2
}
}
}
}
1 change: 1 addition & 0 deletions pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ReplServerTest {
"bar",
"toList()",
"toMap()",
"toMixin()",
"getProperty(",
"getPropertyOrNull(",
"hasProperty(",
Expand Down
14 changes: 14 additions & 0 deletions stdlib/base.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,20 @@ class Dynamic extends Object {
///
/// Throws if [clazz] is abstract or not a subclass of [Typed].
external function toTyped<Type>(clazz: Class<Type>): Type(this is Typed)

/// Converts this dynamic object to a [Mixin] function.
///
/// The resulting mixin can be applied to any object using the `|>` operator
/// to amend it with the properties, elements, and entries from this dynamic object.
///
/// Example:
/// ```
/// dynamic = new Dynamic { name = "Pigeon"; age = 42 }
/// mixin = dynamic.toMixin()
/// person = new Person { name = "Original" } |> mixin
/// // person.name == "Pigeon", person.age == 42
/// ```
external function toMixin(): Mixin
}

/// An object containing an ordered sequence of elements.
Expand Down