Skip to content

Commit 2e456d1

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 2e456d1

File tree

6 files changed

+820
-5
lines changed

6 files changed

+820
-5
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 com.oracle.truffle.api.frame.VirtualFrame;
22+
import org.pkl.core.ast.member.ObjectMember;
23+
import org.pkl.core.runtime.*;
24+
import org.pkl.core.stdlib.ExternalMethod0Node;
25+
26+
public final class ObjectNodes {
27+
private ObjectNodes() {}
28+
29+
public abstract static class toMixin extends ExternalMethod0Node {
30+
@Specialization
31+
protected VmFunction eval(VmObject self) {
32+
CompilerDirectives.transferToInterpreterAndInvalidate();
33+
var rootNode = new ObjectToMixinRootNode(self, new FrameDescriptor());
34+
return new VmFunction(
35+
VmUtils.createEmptyMaterializedFrame(),
36+
null,
37+
1,
38+
rootNode,
39+
null);
40+
}
41+
}
42+
43+
private static final class ObjectToMixinRootNode extends org.pkl.core.ast.PklRootNode {
44+
private final VmObject sourceObject;
45+
46+
public ObjectToMixinRootNode(VmObject sourceObject, FrameDescriptor descriptor) {
47+
super(null, descriptor);
48+
this.sourceObject = sourceObject;
49+
}
50+
51+
@Override
52+
public com.oracle.truffle.api.source.SourceSection getSourceSection() {
53+
return VmUtils.unavailableSourceSection();
54+
}
55+
56+
@Override
57+
public String getName() {
58+
return "toMixin";
59+
}
60+
61+
@Override
62+
protected Object executeImpl(VirtualFrame frame) {
63+
var arguments = frame.getArguments();
64+
if (arguments.length != 3) {
65+
CompilerDirectives.transferToInterpreter();
66+
throw new VmExceptionBuilder()
67+
.evalError("wrongFunctionArgumentCount", 1, arguments.length - 2)
68+
.build();
69+
}
70+
71+
var targetObject = arguments[2];
72+
73+
if (!(targetObject instanceof VmObject)) {
74+
CompilerDirectives.transferToInterpreter();
75+
throw new VmExceptionBuilder()
76+
.typeMismatch(targetObject, BaseModule.getDynamicClass())
77+
.build();
78+
}
79+
80+
var parent = (VmObject) targetObject;
81+
var parentLength = (parent instanceof VmDynamic) ? ((VmDynamic) parent).getLength() : 0;
82+
var sourceLength = (sourceObject instanceof VmDynamic) ? ((VmDynamic) sourceObject).getLength() : 0;
83+
var adjustedMembers = adjustMemberIndices(sourceObject.getMembers(), parentLength);
84+
85+
return new VmDynamic(
86+
sourceObject.getEnclosingFrame(),
87+
parent,
88+
adjustedMembers,
89+
parentLength + sourceLength);
90+
}
91+
92+
// Adjust element indices in the members map by offsetting them by parentLength
93+
@CompilerDirectives.TruffleBoundary
94+
private static org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> adjustMemberIndices(
95+
org.graalvm.collections.UnmodifiableEconomicMap<Object, ObjectMember> members,
96+
long parentLength) {
97+
if (parentLength == 0) {
98+
return members;
99+
}
100+
101+
var result = org.pkl.core.util.EconomicMaps.<Object, ObjectMember>create(
102+
org.pkl.core.util.EconomicMaps.size(members));
103+
104+
var cursor = members.getEntries();
105+
while (cursor.advance()) {
106+
var key = cursor.getKey();
107+
var member = cursor.getValue();
108+
109+
// If this is an element (not an entry with an Int key), offset the index
110+
if (member.isElement()) {
111+
// Elements always have Long keys
112+
var newKey = (Long) key + parentLength;
113+
org.pkl.core.util.EconomicMaps.put(result, newKey, member);
114+
} else {
115+
// Properties and entries are not offset
116+
org.pkl.core.util.EconomicMaps.put(result, key, member);
117+
}
118+
}
119+
120+
return result;
121+
}
122+
}
123+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
amends "../snippetTest.pkl"
2+
3+
local class Person {
4+
name: String
5+
age: Int = 0
6+
}
7+
8+
open local class Animal {
9+
name: String
10+
age: Int
11+
}
12+
13+
local class Dog extends Animal {
14+
breed: String = "Unknown"
15+
}
16+
17+
local class Config {
18+
port: Int(isBetween(1, 65535))
19+
}
20+
21+
local class ServerConfig {
22+
host: String
23+
port: Int
24+
}
25+
26+
local class Settings {
27+
enabled: Boolean = true
28+
timeout: Int = 30
29+
}
30+
31+
examples {
32+
["basic conversion"] {
33+
// Convert Dynamic with properties to Mixin
34+
local dynamic1 = new Dynamic {
35+
name = "Pigeon"
36+
age = 42
37+
}
38+
local mixin1 = dynamic1.toMixin()
39+
new Dynamic { name = "Original" } |> mixin1
40+
}
41+
42+
["empty Dynamic"] {
43+
// Empty Dynamic should create identity mixin
44+
local emptyDynamic = new Dynamic {}
45+
local emptyMixin = emptyDynamic.toMixin()
46+
new Dynamic { existing = "value" } |> emptyMixin
47+
}
48+
49+
["with elements"] {
50+
// Elements should be appended
51+
local dynamicWithElements = new Dynamic {
52+
"element1"
53+
"element2"
54+
}
55+
local mixinWithElements = dynamicWithElements.toMixin()
56+
new Dynamic { "base" } |> mixinWithElements
57+
}
58+
59+
["with entries"] {
60+
// Entries should be merged
61+
local dynamicWithEntries = new Dynamic {
62+
["key1"] = "value1"
63+
["key2"] = "value2"
64+
}
65+
local mixinWithEntries = dynamicWithEntries.toMixin()
66+
new Dynamic { ["baseKey"] = "baseValue" } |> mixinWithEntries
67+
}
68+
69+
["with mixed members"] {
70+
// Properties, elements, and entries should all be merged
71+
local dynamicMixed = new Dynamic {
72+
prop = "property"
73+
"element"
74+
["entry"] = "entryValue"
75+
}
76+
local mixinMixed = dynamicMixed.toMixin()
77+
new Dynamic { baseProp = "base" } |> mixinMixed
78+
}
79+
80+
["applying to Typed object"] {
81+
// Mixin should work with typed objects
82+
local dynamicPerson = new Dynamic {
83+
name = "Modified"
84+
age = 100
85+
}
86+
local mixinPerson = dynamicPerson.toMixin()
87+
new Person { name = "Original"; age = 20 } |> mixinPerson
88+
}
89+
90+
["chaining mixins"] {
91+
// Multiple mixins can be chained
92+
local mixin1 = new Dynamic { extra = "value" }.toMixin()
93+
local mixin2 = new Dynamic { another = "field" }.toMixin()
94+
new Dynamic { base = "start" } |> mixin1 |> mixin2
95+
}
96+
97+
["reusable mixin"] {
98+
// Mixin can be applied to multiple objects
99+
local reusableMixin = new Dynamic { shared = "config" }.toMixin()
100+
new {
101+
first = new Dynamic { id = 1 } |> reusableMixin
102+
second = new Dynamic { id = 2 } |> reusableMixin
103+
}
104+
}
105+
106+
["overriding properties"] {
107+
// Properties from mixin should override base properties
108+
local overrideDynamic = new Dynamic {
109+
name = "Override"
110+
value = 999
111+
}
112+
local overrideMixin = overrideDynamic.toMixin()
113+
new Dynamic { name = "Original"; value = 1; other = "keep" } |> overrideMixin
114+
}
115+
116+
["replacement vs merge for nested objects"] {
117+
// Test replacement vs merge semantics
118+
local base = new {
119+
a1 {
120+
b1 = 2
121+
}
122+
a2 {
123+
b1 = 2
124+
}
125+
}
126+
local overrideValue = new Dynamic {
127+
a1 = new Dynamic {
128+
b2 = 2
129+
}
130+
a2 {
131+
b2 = 2
132+
}
133+
}
134+
(base) |> overrideValue.toMixin()
135+
}
136+
137+
["integer entry keys vs elements"] {
138+
// CRITICAL: Test that integer entry keys are NOT offset like elements
139+
// This tests the fix for using member.isElement() instead of key instanceof Long
140+
local mixinWithIntKeys = new Dynamic {
141+
[5] = "entry at 5"
142+
[10] = "entry at 10"
143+
"element0"
144+
"element1"
145+
}.toMixin()
146+
new Dynamic {
147+
"base0"
148+
"base1"
149+
[99] = "base entry at 99"
150+
} |> mixinWithIntKeys
151+
}
152+
153+
["class hierarchy"] {
154+
// Test with class inheritance
155+
local mixin = new Dynamic {
156+
age = 5
157+
breed = "Labrador"
158+
}.toMixin()
159+
new Dog { name = "Buddy"; age = 3 } |> mixin
160+
}
161+
162+
["constraints are preserved"] {
163+
// Mixin should preserve constraints from base object
164+
local mixin = new Dynamic { port = 8080 }.toMixin()
165+
new Config { port = 3000 } |> mixin
166+
}
167+
168+
["local members are not exposed"] {
169+
// Local members in mixin source should not appear in result
170+
local source = new Dynamic {
171+
local helper = "should not appear"
172+
visible = "should appear"
173+
}
174+
new Dynamic { base = "value" } |> source.toMixin()
175+
}
176+
177+
["nested amendment semantics"] {
178+
// Deep nesting with amendments should work correctly
179+
local mixin = new Dynamic {
180+
outerVal {
181+
inner = new Dynamic {
182+
deep = "value"
183+
}
184+
}
185+
}.toMixin()
186+
new Dynamic {
187+
outerVal {
188+
inner = new Dynamic {
189+
shallow = "base"
190+
}
191+
sibling = "data"
192+
}
193+
} |> mixin
194+
}
195+
196+
["typed object to mixin"] {
197+
// Non-Dynamic typed objects can also be converted to mixins
198+
local config = new ServerConfig {
199+
host = "localhost"
200+
port = 8080
201+
}
202+
new Dynamic { existing = "field" } |> config.toMixin()
203+
}
204+
205+
["mixin from typed class with defaults"] {
206+
// Test that default values work correctly
207+
local mixin = new Settings { enabled = false }.toMixin()
208+
new Dynamic { custom = "value" } |> mixin
209+
}
210+
211+
["element index offset with gaps"] {
212+
// Elements should be offset correctly even with gaps in parent
213+
local mixin = new Dynamic {
214+
"mixinElement0"
215+
"mixinElement1"
216+
}.toMixin()
217+
new Dynamic {
218+
"base0"
219+
"base1"
220+
"base2"
221+
} |> mixin
222+
}
223+
224+
["empty parent with mixin elements"] {
225+
// Elements should start at 0 when parent has no elements
226+
local mixin = new Dynamic {
227+
"elem0"
228+
"elem1"
229+
}.toMixin()
230+
new Dynamic { prop = "value" } |> mixin
231+
}
232+
233+
["mixin self application"] {
234+
// Applying mixin derived from an object to itself
235+
local obj = new Dynamic { count = 1 }
236+
local mixin = obj.toMixin()
237+
obj |> mixin
238+
}
239+
}

0 commit comments

Comments
 (0)