diff --git a/pkl-core/src/main/java/org/pkl/core/ast/ObjectToMixinNode.java b/pkl-core/src/main/java/org/pkl/core/ast/ObjectToMixinNode.java new file mode 100644 index 000000000..be1df4444 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ast/ObjectToMixinNode.java @@ -0,0 +1,150 @@ +/* + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.ast; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.source.SourceSection; +import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.runtime.*; + +public final class ObjectToMixinNode extends PklRootNode { + private final VmObject sourceObject; + + public ObjectToMixinNode(VmObject sourceObject, FrameDescriptor descriptor) { + super(null, descriptor); + this.sourceObject = sourceObject; + } + + @Override + public 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]; + + if (!(targetObject instanceof VmObject)) { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .typeMismatch(targetObject, BaseModule.getDynamicClass()) + .build(); + } + + var parent = (VmObject) targetObject; + var parentLength = getObjectLength(parent); + var sourceLength = getObjectLength(sourceObject); + var allSourceMembers = collectAllMembers(sourceObject); + var adjustedMembers = adjustMemberIndices(allSourceMembers, parentLength); + + return new VmDynamic( + sourceObject.getEnclosingFrame(), + parent, + adjustedMembers, + (int) (parentLength + sourceLength)); + } + + // Get the length of an object (number of elements) + private static long getObjectLength(VmObject obj) { + if (obj instanceof VmDynamic) { + return ((VmDynamic) obj).getLength(); + } else if (obj instanceof VmListing) { + return ((VmListing) obj).getLength(); + } else if (obj instanceof VmMapping) { + return ((VmMapping) obj).getLength(); + } + return 0; + } + + // Collect all members from the source object and its entire parent chain (including prototypes) + @CompilerDirectives.TruffleBoundary + private static org.graalvm.collections.UnmodifiableEconomicMap collectAllMembers( + VmObject sourceObject) { + var result = org.pkl.core.util.EconomicMaps.create(); + + // Build list of objects from source to root (including all prototypes) + var chain = new java.util.ArrayList(); + var current = sourceObject; + while (current != null) { + chain.add(current); + current = current.getParent(); + } + + // Iterate in reverse order (from root down to source) + // This ensures parent members appear first, but child members override parents + for (int i = chain.size() - 1; i >= 0; i--) { + var obj = chain.get(i); + var entries = obj.getMembers().getEntries(); + while (entries.advance()) { + var key = entries.getKey(); + var member = entries.getValue(); + if (member.isLocalOrExternalOrHidden()) continue; + // Skip undefined members (required properties with no default value) + if (member.isUndefined()) continue; + // Always put the member - later objects in the chain override earlier ones + org.pkl.core.util.EconomicMaps.put(result, key, member); + } + } + + return result; + } + + // Adjust element indices in the members map by offsetting them by parentLength + @CompilerDirectives.TruffleBoundary + private static org.graalvm.collections.UnmodifiableEconomicMap adjustMemberIndices( + org.graalvm.collections.UnmodifiableEconomicMap members, + long parentLength) { + if (parentLength == 0) { + return members; + } + + var result = org.pkl.core.util.EconomicMaps.create( + org.pkl.core.util.EconomicMaps.size(members)); + + var cursor = members.getEntries(); + while (cursor.advance()) { + var key = cursor.getKey(); + var member = cursor.getValue(); + + // If this is an element (not an entry with an Int key), offset the index + if (member.isElement()) { + // Elements always have Long keys + 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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ObjectNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ObjectNodes.java new file mode 100644 index 000000000..b6776609b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ObjectNodes.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.stdlib.base; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.FrameDescriptor; +import org.pkl.core.ast.ObjectToMixinNode; +import org.pkl.core.runtime.*; +import org.pkl.core.stdlib.ExternalMethod0Node; + +public final class ObjectNodes { + private ObjectNodes() {} + + public abstract static class toMixin extends ExternalMethod0Node { + @Specialization + protected VmFunction eval(VmObject self) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + var rootNode = new ObjectToMixinNode(self, new FrameDescriptor()); + return new VmFunction( + VmUtils.createEmptyMaterializedFrame(), + null, + 1, + rootNode, + null); + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/objectToMixin.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/objectToMixin.pkl new file mode 100644 index 000000000..bd2bfd7ed --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/objectToMixin.pkl @@ -0,0 +1,339 @@ +amends "../snippetTest.pkl" + +local class Person { + name: String + age: Int = 0 +} + +open local class Animal { + name: String + age: Int +} + +local class Dog extends Animal { + breed: String = "Unknown" +} + +local class Config { + port: Int(isBetween(1, 65535)) +} + +local class ServerConfig { + host: String + port: Int +} + +open local class Settings { + enabled: Boolean = true + timeout: Int = 30 +} + +local class ColorSettings extends Settings { + color: String = "blue" +} + +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() + } + + ["integer entry keys vs elements"] { + // CRITICAL: Test that integer entry keys are NOT offset like elements + // This tests the fix for using member.isElement() instead of key instanceof Long + local mixinWithIntKeys = new Dynamic { + [5] = "entry at 5" + [10] = "entry at 10" + "element0" + "element1" + }.toMixin() + new Dynamic { + "base0" + "base1" + [99] = "base entry at 99" + } |> mixinWithIntKeys + } + + ["class hierarchy"] { + // Test with class inheritance + local mixin = new Dynamic { + age = 5 + breed = "Labrador" + }.toMixin() + new Dog { name = "Buddy"; age = 3 } |> mixin + } + + ["constraints are preserved"] { + // Mixin should preserve constraints from base object + local mixin = new Dynamic { port = 8080 }.toMixin() + new Config { port = 3000 } |> mixin + } + + ["local members are not exposed"] { + // Local members in mixin source should not appear in result + local source = new Dynamic { + local helper = "should not appear" + visible = "should appear" + } + new Dynamic { base = "value" } |> source.toMixin() + } + + ["nested amendment semantics"] { + // Deep nesting with amendments should work correctly + local mixin = new Dynamic { + outerVal { + inner = new Dynamic { + deep = "value" + } + } + }.toMixin() + new Dynamic { + outerVal { + inner = new Dynamic { + shallow = "base" + } + sibling = "data" + } + } |> mixin + } + + ["typed object to mixin"] { + // Non-Dynamic typed objects can also be converted to mixins + local config = new ServerConfig { + host = "localhost" + port = 8080 + } + new Dynamic { existing = "field" } |> config.toMixin() + } + + ["mixin from typed class with defaults"] { + // Test that default values work correctly + local mixin = new Settings { enabled = false }.toMixin() + new Dynamic { custom = "value" } |> mixin + } + + ["element index offset with gaps"] { + // Elements should be offset correctly even with gaps in parent + local mixin = new Dynamic { + "mixinElement0" + "mixinElement1" + }.toMixin() + new Dynamic { + "base0" + "base1" + "base2" + } |> mixin + } + + ["empty parent with mixin elements"] { + // Elements should start at 0 when parent has no elements + local mixin = new Dynamic { + "elem0" + "elem1" + }.toMixin() + new Dynamic { prop = "value" } |> mixin + } + + ["mixin self application"] { + // Applying mixin derived from an object to itself + local obj = new Dynamic { count = 1 } + local mixin = obj.toMixin() + obj |> mixin + } + + ["applying elements-only mixin to Listing"] { + // Elements-only mixin should work with Listing + local elementsMixin = new Dynamic { + "new1" + "new2" + }.toMixin() + new Listing { + "base1" + "base2" + } |> elementsMixin + } + + ["applying properties mixin to Listing"] { + // Properties can be added to Listings via mixins + new Listing { + "item1" + "item2" + } |> new Dynamic { + customProp = "properties work on Listings" + }.toMixin() + } + + ["applying entries mixin to Listing"] { + // Entries can be added to Listings via mixins + new Listing { + "item1" + "item2" + } |> new Dynamic { + ["key"] = "entries work on Listings" + }.toMixin() + } + + ["applying Listing mixin to Listing"] { + // Listing mixin should work with Listing + local listingSource = new Listing { + "fromListing1" + "fromListing2" + } + new Listing { + "base1" + } |> listingSource.toMixin() + } + + ["applying Mapping mixin to Mapping"] { + // Mapping mixin should work with Mapping + local mappingSource = new Mapping { + ["item1"] = "value override" + ["item3"] = "value 3" + } + new Mapping { + ["item1"] = "value 1" + ["item2"] = "value 2" + } |> mappingSource.toMixin() + } + + ["applying mixin to itself"] { + local obj = new Dynamic { + name = "Override" + value = 999 + } + obj |> obj.toMixin() + } + + ["amended dynamic mixin"] { + local foo = new Dynamic { + a = 1 + } + local bar = (foo) { + b = 2 + } + new Dynamic {} |> bar.toMixin() + } + + ["amended class mixin"] { + local foo = new ServerConfig { + host = "value" + } + local bar = (foo) { + host = "replace" + } + new Dynamic {} |> bar.toMixin() + } + + ["undefined property as mixin"] { + new ServerConfig { + host = "value" + port = 123 + } |> new ServerConfig{}.toMixin() + } + + ["default property values as mixin"] { + new Settings { + timeout = 100 + } |> new ColorSettings{}.toMixin() + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/objectToMixin.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/objectToMixin.pcf new file mode 100644 index 000000000..a783c9df3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/objectToMixin.pcf @@ -0,0 +1,218 @@ +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 + } + } + } + ["integer entry keys vs elements"] { + new { + [99] = "base entry at 99" + "base0" + "base1" + [5] = "entry at 5" + [10] = "entry at 10" + "element0" + "element1" + } + } + ["class hierarchy"] { + new { + name = "Buddy" + age = 5 + breed = "Labrador" + } + } + ["constraints are preserved"] { + new { + port = 8080 + } + } + ["local members are not exposed"] { + new { + base = "value" + visible = "should appear" + } + } + ["nested amendment semantics"] { + new { + outerVal { + inner { + deep = "value" + } + sibling = "data" + } + } + } + ["typed object to mixin"] { + new { + existing = "field" + host = "localhost" + port = 8080 + } + } + ["mixin from typed class with defaults"] { + new { + custom = "value" + enabled = false + timeout = 30 + } + } + ["element index offset with gaps"] { + new { + "base0" + "base1" + "base2" + "mixinElement0" + "mixinElement1" + } + } + ["empty parent with mixin elements"] { + new { + prop = "value" + "elem0" + "elem1" + } + } + ["mixin self application"] { + new { + count = 1 + } + } + ["applying elements-only mixin to Listing"] { + new { + "base1" + "base2" + "new1" + "new2" + } + } + ["applying properties mixin to Listing"] { + new { + "item1" + "item2" + customProp = "properties work on Listings" + } + } + ["applying entries mixin to Listing"] { + new { + "item1" + "item2" + ["key"] = "entries work on Listings" + } + } + ["applying Listing mixin to Listing"] { + new { + "base1" + "fromListing1" + "fromListing2" + } + } + ["applying Mapping mixin to Mapping"] { + new { + ["item1"] = "value override" + ["item2"] = "value 2" + ["item3"] = "value 3" + } + } + ["applying mixin to itself"] { + new { + name = "Override" + value = 999 + } + } + ["amended dynamic mixin"] { + new { + a = 1 + b = 2 + } + } + ["amended class mixin"] { + new { + host = "replace" + } + } + ["undefined property as mixin"] { + new { + host = "value" + port = 123 + } + } + ["default property values as mixin"] { + new { + enabled = true + timeout = 30 + color = "blue" + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/reflectedDeclaration.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/reflectedDeclaration.pcf index fde86b4d7..cb932471b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/reflectedDeclaration.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/reflectedDeclaration.pcf @@ -3532,7 +3532,33 @@ rec { } properties = Map() allProperties = Map() - methods = Map() + methods = Map("toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() + }) allMethods = Map("getClass", new { location { line = XX @@ -3583,6 +3609,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }) } supertype { @@ -3920,7 +3972,33 @@ rec { } properties = Map() allProperties = Map() - methods = Map() + methods = Map("toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() + }) allMethods = Map("getClass", new { location { line = XX @@ -3971,6 +4049,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }) } typeArguments = List() @@ -4102,6 +4206,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }, "hasProperty", new { location { line = XXXX @@ -4529,7 +4659,33 @@ rec { } properties = Map() allProperties = Map() - methods = Map() + methods = Map("toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() + }) allMethods = Map("getClass", new { location { line = XX @@ -4580,6 +4736,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }) } supertype { @@ -4917,7 +5099,33 @@ rec { } properties = Map() allProperties = Map() - methods = Map() + methods = Map("toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() + }) allMethods = Map("getClass", new { location { line = XX @@ -4968,6 +5176,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }) } typeArguments = List() @@ -5099,6 +5333,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "https://github.com/apple/pkl/blob/$commitId/stdlib/base.pkl#LXXXX" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }, "hasProperty", new { location { line = XXXX @@ -5254,6 +5514,32 @@ rec { parameters = Map("transform", new { name = "transform" }) + }, "toMixin", new { + location { + line = XXXX + column = 3 + displayUri = "pkl:base" + } + docComment = """ + Converts this 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 object. + + Example: + ``` + obj = new { name = "Pigeon"; age = 42 } + person = new Person { name = "Original" } |> obj.toMixin() + // person.name == "Pigeon", person.age == 42 + ``` + """ + annotations = List(new { + version = "0.31.0" + }) + modifiers = Set() + name = "toMixin" + typeParameters = List() + parameters = Map() }, "hasProperty", new { location { line = XXXX diff --git a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt index 7b9c21a8c..49c223399 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/ReplServerTest.kt @@ -67,6 +67,7 @@ class ReplServerTest { "bar", "toList()", "toMap()", + "toMixin()", "getProperty(", "getPropertyOrNull(", "hasProperty(", @@ -115,6 +116,7 @@ class ReplServerTest { "output", "toDynamic()", "toMap()", + "toMixin()", "f()", "x", "ifNonNull(", diff --git a/stdlib/base.pkl b/stdlib/base.pkl index 5e52f9f12..feb4287a9 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -1861,7 +1861,21 @@ external class DataSize extends Any { /// manipulated = pigeon.toMap().mapKeys((key, value) -> key.reverse()) /// manipulated.toDynamic() // new { eman = "Pigeon"; ega = 42 } /// ``` -abstract external class Object extends Any +abstract external class Object extends Any { + /// Converts this 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 object. + /// + /// Example: + /// ``` + /// obj = new { name = "Pigeon"; age = 42 } + /// person = new Person { name = "Original" } |> obj.toMixin() + /// // person.name == "Pigeon", person.age == 42 + /// ``` + @Since { version = "0.31.0" } + external function toMixin(): Mixin +} /// Base class for objects whose members are described by a class definition. ///