diff --git a/docs/modules/language-reference/pages/index.adoc b/docs/modules/language-reference/pages/index.adoc index 170044188..022fce48e 100644 --- a/docs/modules/language-reference/pages/index.adoc +++ b/docs/modules/language-reference/pages/index.adoc @@ -3178,6 +3178,7 @@ pkl: TRACE: num1 * num2 = 672 (at file:///some/module.pkl, line 42) This section discusses language features that are generally more relevant to template and library authors than template consumers. <> + +<> + <> + <> + <> + @@ -3357,6 +3358,45 @@ swiftHatchlings = typedProperty.listHatchlings(new { "Poppy"; "Chirpy" }) // <8> <7> Admending the property default value `new Listing { new Bird { name = "Osprey" } }`; the result contains both birds. <8> Error: Cannot tell which parent to amend. +[[member-deletions]] +=== Deleting members with `delete` + +Members can be deleted when amending the object containing them. +This is done using the `delete` keyword. +By itself, `delete` is not an expression; it can be used only in specific places, namely in a member +assignment. +These are all the positions where `delete` can be used: + +[source,pkl] +---- +foo { + bar = delete // <1> + ["baz"] = delete // <2> + [42] = delete // <3> + [[qux == 1337]] = delete // <4> +} +---- +<1> Property deletion. +<2> Entry deletion. +<3> Element deletion. +<4> Member predicate deletion (can include both entries and elements). + +Properties can only be deleted from `Dynamic`s, because a `Typed` must have precisely the properties defined in its class. +Since elements are contiguously indices, deleting elements effectively renames other elements. +This means that the indices used in the definition of an object can differ from those used in subscripting. +In the following, `result = true`: + +[source,pkl] +---- +result = (new Listing { + /* [0] */ "foo" + /* [1] */ "bar" + /* [2] */ "baz" +}) { + [1] = delete +}[1] == "baz" +---- + [[let-expressions]] === Let Expressions diff --git a/pkl-core/src/main/antlr/PklParser.g4 b/pkl-core/src/main/antlr/PklParser.g4 index a784a0852..b823912be 100644 --- a/pkl-core/src/main/antlr/PklParser.g4 +++ b/pkl-core/src/main/antlr/PklParser.g4 @@ -217,14 +217,14 @@ objectBody ; objectMember - : modifier* Identifier (typeAnnotation? '=' expr | objectBody+) # objectProperty - | methodHeader '=' expr # objectMethod - | t='[[' k=expr err1=']'? err2=']'? ('=' v=expr | objectBody+) # memberPredicate - | t='[' k=expr err1=']'? err2=']'? ('=' v=expr | objectBody+) # objectEntry - | expr # objectElement - | ('...' | '...?') expr # objectSpread - | 'when' '(' e=expr err=')'? (b1=objectBody ('else' b2=objectBody)?) # whenGenerator - | 'for' '(' t1=parameter (',' t2=parameter)? 'in' e=expr err=')'? objectBody # forGenerator + : modifier* Identifier (typeAnnotation? '=' (v=expr | d='delete') | objectBody+) # objectProperty + | methodHeader '=' expr # objectMethod + | t='[[' k=expr err1=']'? err2=']'? ('=' (v=expr | d='delete') | objectBody+) # memberPredicate + | t='[' k=expr err1=']'? err2=']'? ('=' (v=expr | d='delete') | objectBody+) # objectEntry + | expr # objectElement + | ('...' | '...?') expr # objectSpread + | 'when' '(' e=expr err=')'? (b1=objectBody ('else' b2=objectBody)?) # whenGenerator + | 'for' '(' t1=parameter (',' t2=parameter)? 'in' e=expr err=')'? objectBody # forGenerator ; stringConstant @@ -247,7 +247,6 @@ reservedKeyword : 'protected' | 'override' | 'record' - | 'delete' | 'case' | 'switch' | 'vararg' diff --git a/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java index 6181cbde0..04e9317a2 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/VmModifier.java @@ -55,6 +55,8 @@ private VmModifier() {} public static final int GLOB = 0x1000; + public static final int DELETE = 0x2000; + // modifier sets public static final int NONE = 0; @@ -134,16 +136,20 @@ public static boolean isEntry(int modifiers) { return (modifiers & ENTRY) != 0; } + public static boolean isDelete(int modifiers) { + return (modifiers & DELETE) != 0; + } + public static boolean isType(int modifiers) { - return (modifiers & (CLASS | TYPE_ALIAS | IMPORT)) != 0 && (modifiers & GLOB) == 0; + return (modifiers & (CLASS | TYPE_ALIAS | IMPORT)) != 0 && (modifiers & (GLOB | DELETE)) == 0; } public static boolean isLocalOrExternalOrHidden(int modifiers) { return (modifiers & (LOCAL | EXTERNAL | HIDDEN)) != 0; } - public static boolean isLocalOrExternalOrAbstract(int modifiers) { - return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0; + public static boolean isLocalOrExternalOrAbstractOrDelete(int modifiers) { + return (modifiers & (LOCAL | EXTERNAL | ABSTRACT | DELETE)) != 0; } public static boolean isConstOrFixed(int modifiers) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java index a4acd001c..c2bace6da 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -651,7 +651,13 @@ private VmException missingLocalPropertyValue(TypeAnnotationContext typeAnnCtx) private ObjectMember doVisitObjectProperty(ObjectPropertyContext ctx) { return doVisitObjectProperty( - ctx, ctx.modifier(), ctx.Identifier(), ctx.typeAnnotation(), ctx.expr(), ctx.objectBody()); + ctx, + ctx.modifier(), + ctx.Identifier(), + ctx.typeAnnotation(), + ctx.v, + ctx.d, + ctx.objectBody()); } private ObjectMember doVisitObjectMethod(ObjectMethodContext ctx) { @@ -720,6 +726,7 @@ private ObjectMember doVisitObjectProperty( TerminalNode propertyName, @Nullable TypeAnnotationContext typeAnnCtx, @Nullable ExprContext exprCtx, + @Nullable Token deleteToken, @Nullable List bodyCtx) { return doVisitObjectProperty( @@ -730,18 +737,21 @@ private ObjectMember doVisitObjectProperty( propertyName.getText(), typeAnnCtx, exprCtx, + deleteToken, bodyCtx); } private ObjectMember doVisitObjectProperty( SourceSection sourceSection, SourceSection headerSection, - int modifiers, + int propertyModifiers, String propertyName, @Nullable TypeAnnotationContext typeAnnCtx, @Nullable ExprContext exprCtx, + @Nullable Token deleteToken, @Nullable List bodyCtx) { + var modifiers = propertyModifiers | (deleteToken == null ? 0 : VmModifier.DELETE); var isLocal = VmModifier.isLocal(modifiers); var identifier = Identifier.property(propertyName, isLocal); @@ -783,6 +793,15 @@ private ObjectMember doVisitObjectProperty( // 2. if in a const scope (i.e. `const bar = new { foo { ... } }`), // `super.foo` does not reference something outside the scope. false)); + } else if (deleteToken != null) { + var deleteSourceSection = createSourceSection(deleteToken); + if (isLocal) { + throw exceptionBuilder() + .evalError("cannotDeleteLocalProperty") + .withSourceSection(deleteSourceSection) + .build(); + } + bodyNode = VmUtils.DELETE_MARKER; } else { // foo = ... assert exprCtx != null; bodyNode = visitExpr(exprCtx); @@ -1218,7 +1237,8 @@ private Pair doVisitMemberPredicate(MemberPredicat var keyNode = symbolTable.enterCustomThisScope(scope -> visitExpr(ctx.k)); return symbolTable.enterEntry( - keyNode, objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.objectBody())); + keyNode, + objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.d, ctx.objectBody())); } private Pair doVisitObjectEntry(ObjectEntryContext ctx) { @@ -1232,20 +1252,22 @@ private Pair doVisitObjectEntry(ObjectEntryContext var keyNode = visitExpr(ctx.k); return symbolTable.enterEntry( - keyNode, objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.objectBody())); + keyNode, + objectMemberInserter(createSourceSection(ctx), keyNode, ctx.v, ctx.d, ctx.objectBody())); } private Function> objectMemberInserter( SourceSection sourceSection, ExpressionNode keyNode, @Nullable ExprContext valueCtx, + @Nullable Token deleteToken, List objectBodyCtxs) { return scope -> { var member = new ObjectMember( sourceSection, keyNode.getSourceSection(), - VmModifier.ENTRY, + VmModifier.ENTRY | (deleteToken == null ? 0 : VmModifier.DELETE), null, scope.getQualifiedName()); @@ -2610,6 +2632,7 @@ private EconomicMap doVisitModuleProperties( ctx.Identifier(), ctx.typeAnnotation(), ctx.expr(), + null, ctx.objectBody()); if (moduleInfo.isAmend() && !member.isLocal() && ctx.typeAnnotation() != null) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java index 8d6aff71c..6fb54a216 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/Member.java @@ -119,6 +119,10 @@ public final boolean isType() { return VmModifier.isType(modifiers); } + public final boolean isDelete() { + return VmModifier.isDelete(modifiers); + } + public final boolean isLocalOrExternalOrHidden() { return VmModifier.isLocalOrExternalOrHidden(modifiers); } @@ -127,7 +131,7 @@ public final boolean isConstOrFixed() { return VmModifier.isConstOrFixed(modifiers); } - public final boolean isLocalOrExternalOrAbstract() { - return VmModifier.isLocalOrExternalOrAbstract(modifiers); + public final boolean isLocalOrExternalOrAbstractOrDelete() { + return VmModifier.isLocalOrExternalOrAbstractOrDelete(modifiers); } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java index 82957bcc7..ed266569a 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -247,12 +247,13 @@ private void doRunAndValidateExamples( return true; } if (examples.getCachedValue(groupKey) == null) { + var key = String.valueOf(groupKey); allGroupsSucceeded.set(false); results - .newResult(String.valueOf(groupKey)) + .newResult(key) .addFailure( Failure.buildExamplePropertyMismatchFailure( - getDisplayUri(groupMember), String.valueOf(groupKey), false)); + getDisplayUri(groupMember), key, false)); } return true; }); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java index f4780bb61..c06d06b00 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmDynamic.java @@ -62,7 +62,7 @@ public int getLength() { return length; } - /** Tells whether this object has any elements. */ + @Override public boolean hasElements() { return length != 0; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java index 621a9b511..c907f66c6 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmFunction.java @@ -19,7 +19,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.frame.MaterializedFrame; -import java.util.function.BiFunction; import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.PklRootNode; import org.pkl.core.ast.member.ObjectMember; @@ -142,7 +141,7 @@ public boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer consum } @Override - public boolean iterateMembers(BiFunction consumer) { + public boolean iterateMembers(MemberConsumer consumer) { return true; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java index 804c8c449..25f973444 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmListing.java @@ -77,6 +77,11 @@ public int getLength() { return length; } + @Override + public boolean hasElements() { + return !isEmpty(); + } + public boolean isEmpty() { return length == 0; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java index cdf157f0e..f61caa65a 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmMapping.java @@ -79,6 +79,11 @@ public VmClass getVmClass() { return BaseModule.getMappingClass(); } + @Override + public boolean hasElements() { + return false; + } + @TruffleBoundary public VmSet getAllKeys() { synchronized (this) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java index 3c00e1375..1c136ed7b 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObject.java @@ -19,7 +19,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; import java.util.*; -import java.util.function.BiFunction; import org.graalvm.collections.EconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.member.ObjectMember; @@ -29,6 +28,9 @@ /** Corresponds to `pkl.base#Object`. */ public abstract class VmObject extends VmObjectLike { + private static final Object DELETED_ELEMENT_INDICES_KEY = new Object(); + private static final Object DELETED_ENTRIES_AND_PROPERTIES_KEY = new Object(); + @CompilationFinal protected @Nullable VmObject parent; protected final UnmodifiableEconomicMap members; protected final EconomicMap cachedValues; @@ -44,7 +46,8 @@ public VmObject( super(enclosingFrame); this.parent = parent; this.members = members; - this.cachedValues = cachedValues; + this.cachedValues = + extractDeletions(parent != null && parent.hasElements(), members, cachedValues); assert parent != this; } @@ -81,6 +84,55 @@ public final UnmodifiableEconomicMap getMembers() { return members; } + @SuppressWarnings("unchecked") + public final @Nullable SortedSet getDeletedIndices() { + return (SortedSet) getCachedValue(DELETED_ELEMENT_INDICES_KEY); + } + + @SuppressWarnings("unchecked") + public final @Nullable Set getDeletedKeys() { + return (Set) getCachedValue(DELETED_ENTRIES_AND_PROPERTIES_KEY); + } + + /** Tells whether this object has any elements. */ + public abstract boolean hasElements(); + + /** + * Members in this {@code VmObject} are subscripted with their {@code referenceKey}. Given that + * the definition of this object may contain use of {@code delete}, the keys used in said + * definition are different from the {@code referenceKey}. In case of element deletions, the + * definition key may be higher {@code long} than the {@code referenceKey}. In case of entry or + * property deletions, the key is no longer valid for this object. + * + * @param referenceKey The key used from outside of this object to dereference a member. + * @return {@code null} if the key is deleted on this object, an offset {@code Long} in case of an + * element index with earlier elements being deleted in this object, or the original input. + */ + public @Nullable Object toDefinitionKey(Object referenceKey) { + if (!(referenceKey instanceof Long index) || !hasElements()) { + var member = getMember(referenceKey); + if (member != null && member.isDelete()) { + return null; + } + return referenceKey; + } + + var deletedIndices = getDeletedIndices(); + if (deletedIndices == null) { + return referenceKey; + } + + var lowerBound = 0L; + var add = 0L; + do { + add = deletedIndices.subSet(lowerBound, index + 1L).size(); + lowerBound = index + 1L; + index += add; + } while (add > 0L); + + return index; + } + @Override public @Nullable Object getCachedValue(Object key) { return EconomicMaps.get(cachedValues, key); @@ -135,21 +187,72 @@ public final boolean iterateAlreadyForcedMemberValues(ForcedMemberValueConsumer @Override @TruffleBoundary - public final boolean iterateMembers(BiFunction consumer) { - var parent = getParent(); - if (parent != null) { - var completed = parent.iterateMembers(consumer); - if (!completed) return false; + public final boolean iterateMembers(MemberConsumer consumer) { + var ancestors = new ArrayDeque(); + var deletedKeys = new HashMap(); + var deletedIndices = new ArrayDeque>(); + + for (var owner = this; owner != null; owner = owner.getParent()) { + ancestors.addFirst(owner); + var keysDeletedHere = owner.getDeletedKeys(); + for (var key : keysDeletedHere == null ? Collections.EMPTY_LIST : keysDeletedHere) { + deletedKeys.compute(key, (k, v) -> v == null ? 1 : v + 1); + } + var indicesDeletedHere = owner.getDeletedIndices(); + if (indicesDeletedHere != null) { + deletedIndices.push(indicesDeletedHere); + } } - var entries = members.getEntries(); - while (entries.advance()) { - var member = entries.getValue(); - if (member.isLocal()) continue; - if (!consumer.apply(entries.getKey(), member)) return false; + + while (!ancestors.isEmpty()) { + var current = ancestors.peek(); + + var entries = current.members.getEntries(); + while (entries.advance()) { + var definitionKey = entries.getKey(); + var referenceKey = toReferenceKey(definitionKey, deletedKeys, deletedIndices); + + var member = entries.getValue(); + if (member.isLocal() || referenceKey == null) continue; + + if (!consumer.accept(referenceKey, member)) return false; + } + + ancestors.pop(); + + var keysDeletedHere = current.getDeletedKeys(); + for (var key : keysDeletedHere == null ? Collections.EMPTY_LIST : keysDeletedHere) { + deletedKeys.compute(key, (k, v) -> Objects.requireNonNull(v) == 1 ? null : v - 1); + } + if (current.getDeletedIndices() != null) { + deletedIndices.pop(); + } } + assert deletedKeys.isEmpty() && deletedIndices.isEmpty(); return true; } + private @Nullable Object toReferenceKey( + Object definitionKey, + Map deletedKeys, + Collection> deletedIndices) { + if (deletedKeys.containsKey(definitionKey)) { + return null; + } + if (!(definitionKey instanceof Long index) || !hasElements()) { + return definitionKey; + } + + for (var indicesDeletedHere : deletedIndices) { + if (indicesDeletedHere.contains(index)) { + return null; + } + index -= indicesDeletedHere.subSet(0L, index).size(); + } + + return index; + } + /** Evaluates this object's members. Skips local, hidden, and external members. */ @Override @TruffleBoundary @@ -158,23 +261,39 @@ public final void force(boolean allowUndefinedValues, boolean recurse) { if (recurse) forced = true; + var deletedKeys = new HashMap(); + var deletedIndices = new ArrayDeque>(); + try { - for (VmObjectLike owner = this; owner != null; owner = owner.getParent()) { - var cursor = EconomicMaps.getEntries(owner.getMembers()); + for (var owner = this; owner != null; owner = owner.getParent()) { + + var indicesDeletedHere = owner.getDeletedIndices(); + if (indicesDeletedHere != null) { + deletedIndices.push(indicesDeletedHere); + } + var keysDeletedHere = owner.getDeletedKeys(); + for (var key : keysDeletedHere == null ? Collections.EMPTY_LIST : keysDeletedHere) { + deletedKeys.compute(key, (k, v) -> v == null ? 1 : v + 1); + } + + var cursor = owner.members.getEntries(); var clazz = owner.getVmClass(); while (cursor.advance()) { - var memberKey = cursor.getKey(); + var referenceKey = toReferenceKey(cursor.getKey(), deletedKeys, deletedIndices); var member = cursor.getValue(); + // isAbstract() can occur when VmAbstractObject.toString() is called // on a prototype of an abstract class (e.g., in the Java debugger) - if (member.isLocalOrExternalOrAbstract() || clazz.isHiddenProperty(memberKey)) { + if (referenceKey == null + || member.isLocalOrExternalOrAbstractOrDelete() + || clazz.isHiddenProperty(referenceKey)) { continue; } - var memberValue = getCachedValue(memberKey); + var memberValue = getCachedValue(referenceKey); if (memberValue == null) { try { - memberValue = VmUtils.doReadMember(this, owner, memberKey, member); + memberValue = VmUtils.doReadMember(this, owner, referenceKey, member); } catch (VmUndefinedValueException e) { if (!allowUndefinedValues) throw e; continue; @@ -220,4 +339,36 @@ protected final Map exportMembers() { return result; } + + private static EconomicMap extractDeletions( + boolean hasElements, + UnmodifiableEconomicMap members, + EconomicMap cachedValues) { + + var elementDeletions = new TreeSet(); + var otherDeletions = new HashSet<>(); + for (var member : members.getValues()) { + if (hasElements) { + break; + } + hasElements = member.isElement(); + } + for (var memberKey : members.getKeys()) { + if (!members.get(memberKey).isDelete()) { + continue; + } + if (memberKey instanceof Long key && hasElements) { + elementDeletions.add(key); + } else { + otherDeletions.add(memberKey); + } + } + if (!elementDeletions.isEmpty()) { + EconomicMaps.put(cachedValues, DELETED_ELEMENT_INDICES_KEY, elementDeletions); + } + if (!otherDeletions.isEmpty()) { + EconomicMaps.put(cachedValues, DELETED_ENTRIES_AND_PROPERTIES_KEY, otherDeletions); + } + return cachedValues; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java index f062600c0..3185660b7 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmObjectLike.java @@ -17,7 +17,6 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.frame.MaterializedFrame; -import java.util.function.BiFunction; import org.graalvm.collections.UnmodifiableEconomicMap; import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.util.Nullable; @@ -135,7 +134,7 @@ public boolean isModuleObject() { * members are not visited, and `false` is returned. Otherwise, all members are visited, and * `true` is returned. */ - public abstract boolean iterateMembers(BiFunction consumer); + public abstract boolean iterateMembers(MemberConsumer consumer); /** Forces shallow or recursive (deep) evaluation of this object. */ public abstract void force(boolean allowUndefinedValues, boolean recurse); @@ -163,4 +162,9 @@ public interface ForcedMemberValueConsumer { */ boolean accept(Object key, ObjectMember member, Object value); } + + @FunctionalInterface + public interface MemberConsumer { + boolean accept(Object key, ObjectMember member); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java index 3f7d046ff..ed5e88df3 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java @@ -64,6 +64,11 @@ public VmClass getVmClass() { return (VmTyped) parent; } + @Override + public boolean hasElements() { + return false; + } + @Override public boolean isPrototype() { return this == getPrototype(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index 80aeb0ab4..69ef12a44 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -37,6 +37,7 @@ import org.organicdesign.fp.collections.ImMap; import org.pkl.core.PClassInfo; import org.pkl.core.PObject; +import org.pkl.core.PklBugException; import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManagerException; import org.pkl.core.StackFrame; @@ -70,6 +71,14 @@ public final class VmUtils { public static final URI REPL_TEXT_URI = URI.create(REPL_TEXT); + public static final ExpressionNode DELETE_MARKER = + new ExpressionNode() { + @Override + public Object executeGeneric(VirtualFrame frame) { + throw new PklBugException("Evaluated `delete`"); + } + }; + private static final Engine PKL_ENGINE = Engine.newBuilder("pkl").option("engine.WarnInterpreterOnly", "false").build(); @@ -240,9 +249,15 @@ public static Object readMember( if (cachedValue != null) return cachedValue; for (var owner = receiver; owner != null; owner = owner.getParent()) { - var member = owner.getMember(memberKey); - if (member == null) continue; - return doReadMember(receiver, owner, memberKey, member, checkType, callNode); + var key = (owner instanceof VmObject obj) ? obj.toDefinitionKey(memberKey) : memberKey; + if (key == null) { + return null; + } + var member = owner.getMember(key); + if (member != null) { + return doReadMember(receiver, owner, key, member, checkType, callNode); + } + memberKey = key; } return null; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java index 036529310..19ca04c0e 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValue.java @@ -46,7 +46,7 @@ public boolean isTyped() { /** * Tells if this value is a {@link VmCollection}, {@link VmListing}, or {@link VmDynamic} with - * {@link VmDynamic#hasElements() elements}. + * {@link VmObject#hasElements() elements}. */ public boolean isSequence() { return false; diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/delete.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/delete.pkl new file mode 100644 index 000000000..742e309e6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/delete.pkl @@ -0,0 +1,97 @@ +amends ".../snippetTest.pkl" + +facts { + ["literal deletion of entries"] { + (new Mapping { + ["deleteFoo"] = 1 + ["deleteBar"] = 2 + ["deleteBaz"] = 3 + ["deleteQux"] = 4 + }) { + ["deleteBar"] = delete + ["deleteBaz"] = 42 + ["deleteQux"] = delete + }.toMap() == Map("deleteFoo", 1, "deleteBaz", 42) + } + + ["literal deletion of elements"] { + (new Listing { + "foo" + "bar" + "baz" + }) { + [1] = delete + }.toList() == List("foo", "baz") + } + + ["member predicate deletion"] { + local source = (new Dynamic { + "one" + "two" + "three" + "four" + "five" + ["foo"] = "six" + ["bar"] = "seven" + ["baz"] = "eight" + }) { + [[contains("e")]] = delete + } + source.toList() == List("two", "four") + source.toMap() == Map("foo", "six") + } + + ["deleted properties are no longer listed in errors"] { + local source = (new Dynamic { + foo = 1 + bar = 2 + baz = 3 + }) { + foo = delete + } + !module.catch(() -> source.qux).contains("foo") + } + + ["element indices are correctly renamed"] { + local source = (new Dynamic { + "foo" + "bar" + "baz" + }) { + [1] = delete + } + new Dynamic { + for (i, v in source) { + "\(i) -> \(v)" + } + }.toList() == List("0 -> foo", "1 -> baz") + } + + ["direct element access after multiple deletes"] { + local source = (new Dynamic { + "zero" // 0 0 0 + "one" // 1 - - + "two" // 2 1 - + "three" // 3 - - + "four" // 4 2 - + "five" // 5 3 1 + "six" // 6 - - + "seven" // 7 4 - + "eight" // 8 5 2 + }) { + [1] = delete + [3] = delete + [6] = delete + } { + [1] = delete + [2] = delete + [3] = "expected" + [4] = delete + } + local result = source[1] + source[1] == "expected" + result == "expected" + source[2] == "eight" + } +} + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteInExpressionPosition.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteInExpressionPosition.pkl new file mode 100644 index 000000000..81c8431e9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteInExpressionPosition.pkl @@ -0,0 +1,8 @@ +foo = new Dynamic { + bar = 42 + baz = false +} + +qux = (foo) { + bar = if (false) 0 else delete +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteRemovesKeyFromSuggestion.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteRemovesKeyFromSuggestion.pkl new file mode 100644 index 000000000..395700656 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/deleteRemovesKeyFromSuggestion.pkl @@ -0,0 +1,8 @@ +foo = new Dynamic { + bar = 0 + baz = 1 + qux = 2 +} + +// The error should _not_ suggest `baz`. +bar = (foo) { baz = delete }.foo diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/delete.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/delete.pcf new file mode 100644 index 000000000..b55bc61a2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/delete.pcf @@ -0,0 +1,23 @@ +facts { + ["literal deletion of entries"] { + true + } + ["literal deletion of elements"] { + true + } + ["member predicate deletion"] { + true + true + } + ["deleted properties are no longer listed in errors"] { + true + } + ["element indices are correctly renamed"] { + true + } + ["direct element access after multiple deletes"] { + true + true + true + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteInExpressionPosition.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteInExpressionPosition.err new file mode 100644 index 000000000..b27dc6d84 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteInExpressionPosition.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Keyword `delete` is not allowed here. (If you must use this name as identifier, enclose it in backticks.) + +x | bar = if (false) 0 else delete + ^^^^^^ +at deleteInExpressionPosition (file:///$snippetsDir/input/errors/deleteInExpressionPosition.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteRemovesKeyFromSuggestion.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteRemovesKeyFromSuggestion.err new file mode 100644 index 000000000..8f9d6507e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/deleteRemovesKeyFromSuggestion.err @@ -0,0 +1,15 @@ +–– Pkl Error –– +Cannot find property `foo` in object of type `Dynamic`. + +x | bar = (foo) { baz = delete }.foo + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +at deleteRemovesKeyFromSuggestion#bar (file:///$snippetsDir/input/errors/deleteRemovesKeyFromSuggestion.pkl) + +Available properties: +bar +default +qux + +xxx | text = renderer.renderDocument(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +at pkl.base#Module.output.text (pkl:base)