Skip to content

Commit 84e495a

Browse files
committed
Add Object.toMixin() method
Adds a toMixin() method to the Object class that converts an Object into a Mixin function applicable via the pipe operator (|>). A generic toMixin() method cannot be implemented in user land, so this implementation provides a native method that properly handles: - Property merging and overriding - Element appending with correct index offsetting - Entry merging with proper key handling - Nested object replacement vs amendment semantics Implementation uses the source Object's enclosing frame to ensure proper module context for type resolution during member evaluation.
1 parent 445d94c commit 84e495a

7 files changed

Lines changed: 1055 additions & 5 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.pkl.core.ast;
17+
18+
import com.oracle.truffle.api.CompilerDirectives;
19+
import com.oracle.truffle.api.frame.FrameDescriptor;
20+
import com.oracle.truffle.api.frame.VirtualFrame;
21+
import com.oracle.truffle.api.source.SourceSection;
22+
import org.pkl.core.ast.member.ObjectMember;
23+
import org.pkl.core.runtime.*;
24+
25+
public final class ObjectToMixinNode extends PklRootNode {
26+
private final VmObject sourceObject;
27+
28+
public ObjectToMixinNode(VmObject sourceObject, FrameDescriptor descriptor) {
29+
super(null, descriptor);
30+
this.sourceObject = sourceObject;
31+
}
32+
33+
@Override
34+
public SourceSection getSourceSection() {
35+
return VmUtils.unavailableSourceSection();
36+
}
37+
38+
@Override
39+
public String getName() {
40+
return "toMixin";
41+
}
42+
43+
@Override
44+
protected Object executeImpl(VirtualFrame frame) {
45+
var arguments = frame.getArguments();
46+
if (arguments.length != 3) {
47+
CompilerDirectives.transferToInterpreter();
48+
throw new VmExceptionBuilder()
49+
.evalError("wrongFunctionArgumentCount", 1, arguments.length - 2)
50+
.build();
51+
}
52+
53+
var targetObject = arguments[2];
54+
55+
if (!(targetObject instanceof VmObject)) {
56+
CompilerDirectives.transferToInterpreter();
57+
throw new VmExceptionBuilder()
58+
.typeMismatch(targetObject, BaseModule.getDynamicClass())
59+
.build();
60+
}
61+
62+
var parent = (VmObject) targetObject;
63+
var parentLength = getObjectLength(parent);
64+
var sourceLength = getObjectLength(sourceObject);
65+
var allSourceMembers = collectAllMembers(sourceObject);
66+
var adjustedMembers = adjustMemberIndices(allSourceMembers, parentLength);
67+
68+
return new VmDynamic(
69+
sourceObject.getEnclosingFrame(),
70+
parent,
71+
adjustedMembers,
72+
(int) (parentLength + sourceLength));
73+
}
74+
75+
// Get the length of an object (number of elements)
76+
private static long getObjectLength(VmObject obj) {
77+
if (obj instanceof VmDynamic) {
78+
return ((VmDynamic) obj).getLength();
79+
} else if (obj instanceof VmListing) {
80+
return ((VmListing) obj).getLength();
81+
} else if (obj instanceof VmMapping) {
82+
return ((VmMapping) obj).getLength();
83+
}
84+
return 0;
85+
}
86+
87+
// Collect all members from the source object and its entire parent chain (including prototypes)
88+
@CompilerDirectives.TruffleBoundary
89+
private static org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> collectAllMembers(
90+
VmObject sourceObject) {
91+
var result = org.pkl.core.util.EconomicMaps.<Object, ObjectMember>create();
92+
93+
// Build list of objects from source to root (including all prototypes)
94+
var chain = new java.util.ArrayList<VmObject>();
95+
var current = sourceObject;
96+
while (current != null) {
97+
chain.add(current);
98+
current = current.getParent();
99+
}
100+
101+
// Iterate in reverse order (from root down to source)
102+
// This ensures parent members appear first, but child members override parents
103+
for (int i = chain.size() - 1; i >= 0; i--) {
104+
var obj = chain.get(i);
105+
var entries = obj.getMembers().getEntries();
106+
while (entries.advance()) {
107+
var key = entries.getKey();
108+
var member = entries.getValue();
109+
if (member.isLocalOrExternalOrHidden()) continue;
110+
// Skip undefined members (required properties with no default value)
111+
if (member.isUndefined()) continue;
112+
// Always put the member - later objects in the chain override earlier ones
113+
org.pkl.core.util.EconomicMaps.put(result, key, member);
114+
}
115+
}
116+
117+
return result;
118+
}
119+
120+
// Adjust element indices in the members map by offsetting them by parentLength
121+
@CompilerDirectives.TruffleBoundary
122+
private static org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> adjustMemberIndices(
123+
org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> members,
124+
long parentLength) {
125+
if (parentLength == 0) {
126+
return members;
127+
}
128+
129+
var result = org.pkl.core.util.EconomicMaps.<Object, ObjectMember>create(
130+
org.pkl.core.util.EconomicMaps.size(members));
131+
132+
var cursor = members.getEntries();
133+
while (cursor.advance()) {
134+
var key = cursor.getKey();
135+
var member = cursor.getValue();
136+
137+
// If this is an element (not an entry with an Int key), offset the index
138+
if (member.isElement()) {
139+
// Elements always have Long keys
140+
var newKey = (Long) key + parentLength;
141+
org.pkl.core.util.EconomicMaps.put(result, newKey, member);
142+
} else {
143+
// Properties and entries are not offset
144+
org.pkl.core.util.EconomicMaps.put(result, key, member);
145+
}
146+
}
147+
148+
return result;
149+
}
150+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.pkl.core.stdlib.base;
17+
18+
import com.oracle.truffle.api.CompilerDirectives;
19+
import com.oracle.truffle.api.dsl.Specialization;
20+
import com.oracle.truffle.api.frame.FrameDescriptor;
21+
import org.pkl.core.ast.ObjectToMixinNode;
22+
import org.pkl.core.runtime.*;
23+
import org.pkl.core.stdlib.ExternalMethod0Node;
24+
25+
public final class ObjectNodes {
26+
private ObjectNodes() {}
27+
28+
public abstract static class toMixin extends ExternalMethod0Node {
29+
@Specialization
30+
protected VmFunction eval(VmObject self) {
31+
CompilerDirectives.transferToInterpreterAndInvalidate();
32+
var rootNode = new ObjectToMixinNode(self, new FrameDescriptor());
33+
return new VmFunction(
34+
VmUtils.createEmptyMaterializedFrame(),
35+
null,
36+
1,
37+
rootNode,
38+
null);
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)