diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java index 23ec04f5..a7b9564d 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/ListingNodes.java @@ -15,9 +15,11 @@ */ package org.pkl.core.stdlib.base; +import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.LoopNode; +import org.pkl.core.ast.PklNode; import org.pkl.core.ast.lambda.*; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.runtime.*; @@ -44,6 +46,23 @@ public final class ListingNodes { } } + public abstract static class lastIndex extends ExternalPropertyNode { + @Specialization + protected long eval(VmListing self) { + return self.getLength() - 1; + } + } + + public abstract static class getOrNull extends ExternalMethod1Node { + @Specialization + protected Object eval(VmListing self, long index) { + if (index < 0 || index >= self.getLength()) { + return VmNull.withoutDefault(); + } + return VmUtils.readMember(self, index); + } + } + public abstract static class isDistinct extends ExternalPropertyNode { @Specialization @TruffleBoundary @@ -89,6 +108,58 @@ public final class ListingNodes { } } + public abstract static class first extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + checkNonEmpty(self, this); + return VmUtils.readMember(self, 0L); + } + } + + public abstract static class firstOrNull extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + if (self.isEmpty()) { + return VmNull.withoutDefault(); + } + return VmUtils.readMember(self, 0L); + } + } + + public abstract static class last extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + checkNonEmpty(self, this); + return VmUtils.readMember(self, self.getLength() - 1L); + } + } + + public abstract static class lastOrNull extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + var length = self.getLength(); + return length == 0 ? VmNull.withoutDefault() : VmUtils.readMember(self, length - 1L); + } + } + + public abstract static class single extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + checkSingleton(self, this); + return VmUtils.readMember(self, 0L); + } + } + + public abstract static class singleOrNull extends ExternalPropertyNode { + @Specialization + protected Object eval(VmListing self) { + if (self.getLength() != 1) { + return VmNull.withoutDefault(); + } + return VmUtils.readMember(self, 0L); + } + } + public abstract static class distinctBy extends ExternalMethod1Node { @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); @@ -116,6 +187,59 @@ public final class ListingNodes { } } + public abstract static class every extends ExternalMethod1Node { + @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); + + @Specialization + protected boolean eval(VmListing self, VmFunction predicate) { + var result = new MutableBoolean(true); + self.iterateMemberValues( + (key, member, value) -> { + if (value == null) { + value = VmUtils.readMember(self, key); + } + result.set(applyNode.executeBoolean(predicate, value)); + return result.get(); + }); + return result.get(); + } + } + + public abstract static class any extends ExternalMethod1Node { + @Child private ApplyVmFunction1Node applyNode = ApplyVmFunction1Node.create(); + + @Specialization + protected boolean eval(VmListing self, VmFunction predicate) { + var result = new MutableBoolean(false); + self.iterateMemberValues( + (key, member, value) -> { + if (value == null) { + value = VmUtils.readMember(self, key); + } + result.set(applyNode.executeBoolean(predicate, value)); + return !result.get(); + }); + return result.get(); + } + } + + public abstract static class contains extends ExternalMethod1Node { + @Specialization + protected boolean eval(VmListing self, Object element) { + var result = new MutableBoolean(false); + self.iterateMemberValues( + (key, member, value) -> { + if (value == null) { + value = VmUtils.readMember(self, key); + } + result.set(element.equals(value)); + return !result.get(); + }); + LoopNode.reportLoopCount(this, self.getLength()); + return result.get(); + } + } + public abstract static class fold extends ExternalMethod2Node { @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); @@ -194,4 +318,24 @@ public final class ListingNodes { return builder.build(); } } + + private static void checkNonEmpty(VmListing self, PklNode node) { + if (self.isEmpty()) { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .evalError("expectedNonEmptyListing") + .withLocation(node) + .build(); + } + } + + private static void checkSingleton(VmListing self, PklNode node) { + if (self.getLength() != 1) { + CompilerDirectives.transferToInterpreter(); + throw new VmExceptionBuilder() + .evalError("expectedSingleElementListing") + .withLocation(node) + .build(); + } + } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java index 99e8aa73..ea00cbcc 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/MappingNodes.java @@ -19,6 +19,8 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.nodes.IndirectCallNode; import java.util.HashSet; +import org.pkl.core.ast.lambda.ApplyVmFunction2Node; +import org.pkl.core.ast.lambda.ApplyVmFunction2NodeGen; import org.pkl.core.ast.lambda.ApplyVmFunction3Node; import org.pkl.core.ast.lambda.ApplyVmFunction3NodeGen; import org.pkl.core.runtime.*; @@ -27,6 +29,7 @@ import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.ExternalMethod2Node; import org.pkl.core.stdlib.ExternalPropertyNode; import org.pkl.core.util.EconomicMaps; +import org.pkl.core.util.MutableBoolean; import org.pkl.core.util.MutableLong; import org.pkl.core.util.MutableReference; @@ -86,6 +89,22 @@ public final class MappingNodes { } } + public abstract static class containsValue extends ExternalMethod1Node { + @Specialization + protected boolean eval(VmMapping self, Object value) { + var foundValue = new MutableBoolean(false); + self.iterateMemberValues( + (key, member, memberValue) -> { + if (memberValue == null) { + memberValue = VmUtils.readMember(self, key); + } + foundValue.set(value.equals(memberValue)); + return !foundValue.get(); + }); + return foundValue.get(); + } + } + public abstract static class getOrNull extends ExternalMethod1Node { @Child private IndirectCallNode callNode = IndirectCallNode.create(); @@ -110,6 +129,42 @@ public final class MappingNodes { } } + public abstract static class every extends ExternalMethod1Node { + @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); + + @Specialization + protected boolean eval(VmMapping self, VmFunction function) { + var result = new MutableBoolean(true); + self.iterateMemberValues( + (key, member, value) -> { + if (value == null) { + value = VmUtils.readMember(self, key); + } + result.set(applyLambdaNode.executeBoolean(function, key, value)); + return result.get(); + }); + return result.get(); + } + } + + public abstract static class any extends ExternalMethod1Node { + @Child private ApplyVmFunction2Node applyLambdaNode = ApplyVmFunction2NodeGen.create(); + + @Specialization + protected boolean eval(VmMapping self, VmFunction function) { + var result = new MutableBoolean(false); + self.iterateMemberValues( + (key, member, value) -> { + if (value == null) { + value = VmUtils.readMember(self, key); + } + result.set(applyLambdaNode.executeBoolean(function, key, value)); + return !result.get(); + }); + return result.get(); + } + } + public abstract static class toMap extends ExternalMethod0Node { @Specialization protected VmMap eval(VmMapping self) { diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index 193045e2..06772f9c 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -518,6 +518,12 @@ Expected a single-element collection. cannotFlattenCollectionWithNonCollectionElement=\ Cannot flatten a collection containing a non-collection element. +expectedNonEmptyListing=\ +Expected a non-empty Listing. + +expectedSingleElementListing=\ +Expected a single-element Listing. + integerOverflow=\ Integer overflow. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/listing.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/listing.pkl index 7ca6e1aa..8ffc24e7 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/listing.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/listing.pkl @@ -23,6 +23,10 @@ local duplicate: Listing = (base) { new { name = "Elf Owl" } } +local altered: Listing = (base) { + [0] { name = "Wood Pigeon" } +} + facts { ["isEmpty"] { empty.isEmpty @@ -30,6 +34,14 @@ facts { !base.isEmpty !derived.isEmpty } + + ["lastIndex"] { + empty.lastIndex == -1 + empty2.lastIndex == -1 + base.lastIndex == 2 + derived.lastIndex == 4 + duplicate.lastIndex == 5 + } ["isDistinct"] { empty.isDistinct @@ -58,6 +70,72 @@ facts { !derived.isDistinctBy((it) -> it.getClass()) !duplicate.isDistinctBy((it) -> it.getClass()) } + + ["getOrNull"] { + empty.getOrNull(-1) == null + empty.getOrNull(0) == null + base.getOrNull(-1) == null + for (i, v in base) { + base.getOrNull(i) == v + } + base.getOrNull(base.length) == null + } + + ["first"] { + module.catch(() -> empty.first) == "Expected a non-empty Listing." + base.first == base[0] + derived.first == base[0] + } + + ["firstOrNull"] { + empty.firstOrNull == null + base.firstOrNull == base[0] + derived.firstOrNull == base[0] + } + + ["last"] { + module.catch(() -> empty.last) == "Expected a non-empty Listing." + base.last == base[2] + derived.last == derived[4] + } + + ["lastOrNull"] { + empty.lastOrNull == null + base.lastOrNull == base[2] + derived.lastOrNull == derived[4] + } + + ["single"] { + module.catch(() -> empty.single) == "Expected a single-element Listing." + module.catch(() -> base.single) == "Expected a single-element Listing." + new Listing { 42 }.single == 42 + } + + ["singleOrNull"] { + empty.singleOrNull == null + base.singleOrNull == null + new Listing { 42 }.singleOrNull == 42 + } + + ["every"] { + !base.every((it) -> it.name.contains("rot")) + base.every((it) -> !it.name.isBlank) + !((base) { new { name = "EEEEE" } }).every((it) -> it.name.contains("rot")) + } + + ["any"] { + base.any((it) -> it.name.contains("rot")) + !base.any((it) -> it.name.contains("inch")) + ((base) { new { name = "EEEEE" } }).any((it) -> it.name.contains("rot")) + } + + ["contains"] { + !empty.contains(0) + base.contains(base[1]) + derived.contains(base[1]) + derived.contains(derived[3]) + !altered.contains(base[0]) + } } examples { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/mapping.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/mapping.pkl index cef4b4da..fd69d589 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/mapping.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/mapping.pkl @@ -57,19 +57,26 @@ facts { !empty.containsKey("default") !empty2.containsKey("Pigeon") } - + + ["containsValue()"] { + !empty.containsValue("Any value") + for (_, v in derived) { + derived.containsValue(v) + } + } + ["length"] { empty.length == 0 base.length == 2 derived.length == 3 } - + ["keys (of type string)"] { empty.keys == Set() derived.keys == Set("Pigeon", "Parrot", "Barn Owl") base.keys == Set("Pigeon", "Parrot") } - + ["keys (of type object)"] { local base2 = new Mapping { [empty] = "one" @@ -82,6 +89,18 @@ facts { base2.keys == Set(empty, base) derived2.keys == Set(empty, base, derived) } + + ["every()"] { + empty.every((_, _) -> throw("unreachable code")) + base.every((k, v) -> k == "Parrot" || v.age > 30) + !base.every((k, _) -> k == "Pigeon") + } + + ["any()"] { + base.any((k, _) -> k.contains("rot")) + base.any((_, v) -> v.age > 40) + !base.any((k, _) -> k.contains("other")) + } } examples { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/listing.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/listing.pcf index 2b71114a..d958cbe2 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/listing.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/listing.pcf @@ -5,6 +5,13 @@ facts { true true } + ["lastIndex"] { + true + true + true + true + true + } ["isDistinct"] { true true @@ -29,6 +36,62 @@ facts { true true } + ["getOrNull"] { + true + true + true + true + true + true + true + } + ["first"] { + true + true + true + } + ["firstOrNull"] { + true + true + true + } + ["last"] { + true + true + true + } + ["lastOrNull"] { + true + true + true + } + ["single"] { + true + true + true + } + ["singleOrNull"] { + true + true + true + } + ["every"] { + true + true + true + } + ["any"] { + true + true + true + } + ["contains"] { + true + true + true + true + true + } } examples { ["length"] { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/mapping.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/mapping.pcf index 58e016ad..9d7f8fa1 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/mapping.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/mapping.pcf @@ -18,6 +18,12 @@ facts { true true } + ["containsValue()"] { + true + true + true + true + } ["length"] { true true @@ -32,6 +38,16 @@ facts { true true } + ["every()"] { + true + true + true + } + ["any()"] { + true + true + true + } } examples { ["getOrNull()"] { diff --git a/stdlib/base.pkl b/stdlib/base.pkl index cffae7cb..5dca25b0 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -1813,6 +1813,28 @@ class Listing extends Object { /// Tells if this listing is empty, that is, has zero elements. external isEmpty: Boolean + /// The index of the last element in this listing (same as `length - 1`). + /// + /// Returns `-1` for an empty list. + @Since { version = "0.27.0" } + external lastIndex: Int + + /// Returns the element at [index]. + /// + /// Returns [null] if [index] is outside the bounds of this listing. + /// + /// Facts: + /// ``` + /// new Listing { 3 ; 9 ; 6 }.getOrNull(0) == 3 + /// new Listing { 3 ; 9 ; 6 }.getOrNull(1) == 9 + /// new Listing { 3 ; 9 ; 6 }.getOrNull(2) == 6 + /// new Listing { 3 ; 9 ; 6 }.getOrNull(-1) == null + /// new Listing { 3 ; 9 ; 6 }.getOrNull(3) == null + /// new Listing { 3 ; 9 ; 6 }.getOrNull(99) == null + /// ``` + @Since { version = "0.27.0" } + external function getOrNull(index: Int): Element? + /// Tells if this listing has no duplicate elements. /// /// Facts: @@ -1843,6 +1865,55 @@ class Listing extends Object { @AlsoKnownAs { names { "unique" } } external distinct: Listing + /// The first element in this listing. + /// + /// Throws if this listing is empty. + /// + /// Facts: + /// ``` + /// new Listing { 1 ; 2 ; 3 }.first == 1 + /// import("pkl:test").catch(() -> new Listing {}.first) + /// ``` + @Since { version = "0.27.0" } + external first: Element + + /// Same as [first] but returns [null] if this listing is empty. + @Since { version = "0.27.0" } + external firstOrNull: Element? + + /// The last element in this listing. + /// + /// Throws if this listing is empty. + /// + /// Facts: + /// ``` + /// new Listing { 1 ; 2 ; 3 }.last == 3 + /// import("pkl:test").catch(() -> new Listing {}.last) + /// ``` + @Since { version = "0.27.0" } + external last: Element + + /// Same as [last] but returns [null] if this listing is empty. + @Since { version = "0.27.0" } + external lastOrNull: Element? + + /// The single element in this listing. + /// + /// Throws if this listing does not have exactly one element. + /// + /// Facts: + /// ``` + /// new Listing { 1 }.single == 1 + /// import("pkl:test").catch(() -> new Listing {}.single) + /// import("pkl:test").catch(() -> new Listing { 1 ; 2 ; 3 }.single) + /// ``` + @Since { version = "0.27.0" } + external single: Element + + /// Same as [single] but returns [null] if this collection is empty or has more than one element. + @Since { version = "0.27.0" } + external singleOrNull: Element? + /// Removes elements that are duplicates after applying [selector] from this listing, preserving the first occurrence. /// /// Facts: @@ -1853,6 +1924,30 @@ class Listing extends Object { @AlsoKnownAs { names { "uniqueBy" } } external function distinctBy(selector: (Element) -> Any): Listing + /// Tells if [predicate] holds for every element of this listing. + /// + /// Returns [true] for an empty listing. + @Since { version = "0.27.0" } + external function every(predicate: (Element) -> Boolean): Boolean + + /// Tells if [predicate] holds for at least one element of this listing. + /// + /// Returns [false] for an empty listing. + @Since { version = "0.27.0" } + external function any(predicate: (Element) -> Boolean): Boolean + + /// Tests if [element] is contained in this listing. + /// + /// Facts: + /// ``` + /// new Listing { 1 ; 2 ; 3 }.contains(1) + /// new Listing { 1 ; 2 ; 3 }.contains(2) + /// new Listing { 1 ; 2 ; 3 }.contains(3) + /// !new Listing { 1 ; 2 ; 3 }.contains(4) + /// ``` + @Since { version = "0.27.0" } + external function contains(element: Element): Boolean + /// Folds this listing in iteration order using [operator], starting with [initial]. external function fold(initial: Result, operator: (Result, Element) -> Result): Result @@ -1893,6 +1988,10 @@ class Mapping extends Object { /// Tells if this mapping contains [key]. external function containsKey(key: Any): Boolean + + /// Tells if this mapping contains an entry with the given [value]. + @Since { version = "0.27.0" } + external function containsValue(value: Any): Boolean /// Returns the value associated with [key] or [null] if this mapping does not contain [key]. /// @@ -1901,6 +2000,18 @@ class Mapping extends Object { /// Folds the entries of this mapping in iteration order using [operator], starting with [initial]. external function fold(initial: Result, operator: (Result, Key, Value) -> Result): Result + + /// Tells if [predicate] holds for every entry of this mapping. + /// + /// Returns [true] for an empty mapping. + @Since { version = "0.27.0" } + external function every(predicate: (Key, Value) -> Boolean): Boolean + + /// Tells if [predicate] holds for at least one entry of this mapping. + /// + /// Returns [false] for an empty mapping. + @Since { version = "0.27.0" } + external function any(predicate: (Key, Value) -> Boolean): Boolean /// Converts this mapping to a [Map]. external function toMap(): Map @@ -2035,7 +2146,7 @@ abstract external class Collection extends Any { /// Facts: /// ``` /// List(1, 2, 3).first == 1 - /// (import "pkl:test").catch(() -> List().first) + /// import("pkl:test").catch(() -> List().first) /// ``` @AlsoKnownAs { names { "head" } } abstract first: Element @@ -2051,7 +2162,7 @@ abstract external class Collection extends Any { /// Facts: /// ``` /// List(1, 2, 3).rest == List(2, 3) - /// (import "pkl:test").catch(() -> List().rest) + /// import("pkl:test").catch(() -> List().rest) /// ``` @AlsoKnownAs { names { "tail" } } abstract rest: Collection @@ -2067,7 +2178,7 @@ abstract external class Collection extends Any { /// Facts: /// ``` /// List(1, 2, 3).last == 3 - /// (import "pkl:test").catch(() -> List().last) + /// import("pkl:test").catch(() -> List().last) /// ``` abstract last: Element @@ -2140,7 +2251,7 @@ abstract external class Collection extends Any { /// ``` /// List(1, 2, 3).indexOf(2) == 1 /// List(1, 2, 2).indexOf(2) == 1 - /// (import "pkl:test").catch(() -> List(1, 2, 3).indexOf(4)) + /// import("pkl:test").catch(() -> List(1, 2, 3).indexOf(4)) /// ``` abstract function indexOf(element: Any): Int @@ -2155,7 +2266,7 @@ abstract external class Collection extends Any { /// ``` /// List(1, 2, 3).lastIndexOf(2) == 1 /// List(1, 2, 2).lastIndexOf(2) == 2 - /// (import "pkl:test").catch(() -> List(1, 2, 3).lastIndexOf(4)) + /// import("pkl:test").catch(() -> List(1, 2, 3).lastIndexOf(4)) /// ``` abstract function lastIndexOf(element: Any): Int @@ -2170,7 +2281,7 @@ abstract external class Collection extends Any { /// ``` /// List(5, 6, 7).find((n) -> n.isEven) == 6 /// List(4, 6, 7).find((n) -> n.isEven) == 4 - /// (import "pkl:test").catch(() -> List(5, 7, 9).find((n) -> n.isEven)) + /// import("pkl:test").catch(() -> List(5, 7, 9).find((n) -> n.isEven)) /// ``` abstract function find(predicate: (Element) -> Boolean): Element @@ -2200,7 +2311,7 @@ abstract external class Collection extends Any { /// ``` /// List(5, 6, 7).findIndex((n) -> n.isEven) == 1 /// List(4, 6, 8).findIndex((n) -> n.isEven) == 0 - /// (import "pkl:test").catch(() -> List(5, 7, 9).findLast((n) -> n.isEven)) + /// import("pkl:test").catch(() -> List(5, 7, 9).findLast((n) -> n.isEven)) /// ``` @AlsoKnownAs { names { "indexWhere" }} abstract function findIndex(predicate: (Element) -> Boolean): Int @@ -2217,7 +2328,7 @@ abstract external class Collection extends Any { /// ``` /// List(5, 6, 7).findLastIndex((n) -> n.isEven) == 1 /// List(4, 6, 8).findLastIndex((n) -> n.isEven) == 2 - /// (import "pkl:test").catch(() -> List(5, 7, 9).findLastIndex((n) -> n.isEven)) + /// import("pkl:test").catch(() -> List(5, 7, 9).findLastIndex((n) -> n.isEven)) /// ``` @AlsoKnownAs { names { "lastIndexWhere" }} abstract function findLastIndex(predicate: (Element) -> Boolean): Int