Compare commits

..

8 Commits

Author SHA1 Message Date
Philip K.F. Hölzenspies 48a710f439 Prepare 0.27.1 release (#839) 2024-12-06 14:23:25 +00:00
Philip K.F. Hölzenspies 15d85b0660 Add release notes for 0.27.1 2024-12-06 13:25:39 +00:00
odenix aeace8bb3c Improve lazy type checking of listings and mappings (#789)
Motivation:
- simplify implementation of lazy type checking
- fix correctness issues of lazy type checking (#785)

Changes:
- implement listing/mapping type cast via amendment (`parent`) instead of delegation (`delegate`)
- handle type checking of *computed* elements/entries in the same way as type checking of computed properties
  - ElementOrEntryNode is the equivalent of TypeCheckedPropertyNode
- remove fields VmListingOrMapping.delegate/typeNodeFrame/cachedMembers/checkedMembers
- fix #785 by executing all type casts between a member's owner and receiver
- fix #823 by storing owner and receiver directly
  instead of storing the mutable frame containing them (typeNodeFrame)
- remove overrides of VmObject methods that are no longer required
  - good for Truffle partial evaluation and JVM inlining
- revert a85a173faa except for added tests
- move `VmUtils.setOwner` and `VmUtils.setReceiver` and make them private
  - these methods aren't generally safe to use

Result:
- simpler code with greater optimization potential
  - VmListingOrMapping can now have both a type node and new members
- fewer changes to surrounding code
- smaller memory footprint
- better performance in some cases
- fixes https://github.com/apple/pkl/issues/785
- fixes https://github.com/apple/pkl/issues/823

Potential future optimizations:
- avoid lazy type checking overhead for untyped listings/mappings
- improve efficiency of forcing a typed listing/mapping
  - currently, lazy type checking will traverse the parent chain once per member,
    reducing the performance benefit of shallow-forcing
	  a listing/mapping over evaluating each member individually
- avoid creating an intermediate untyped listing/mapping in the following cases:
  - `new Listing<X> {...}`
  - amendment of `property: Listing<X>`
2024-12-06 14:00:23 +01:00
Islon Scherer 7b850dd6d9 Fix possible stack overflow in Listing/Mapping type checking (#826) 2024-12-06 13:58:29 +01:00
Daniel Chao c2096f633b Exclude non file-based modules from synthesized *GatherImports task (#821)
This fixes an issue where certain modules tasks fail due to the plugin
attempting to analyze their imports, but the arguments may not actually be
Pkl modules.

For example, the pkldoc task accepts entire packages in its "sourceMoules" property.

This changes the gather imports logic to only analyze file-based modules.
This is also a performance improvement; non file-based modules are unlikely to import
files due to insufficient trust levels.

Also: fix a bug when generating pkldoc on Windows
2024-12-06 13:58:06 +01:00
translatenix d6ba021e12 Fix length of listings with computed index (#797)
Motivation:
The following expression evaluates to 2 instead of 1:
new Listing { "value" } { [0 + 0] = "override" }.length

Changes:
- fix length computation in EntriesLiteralNode
- improve `api/listing` tests
- make snippet test failures diffable in IntelliJ

Result:
- fixes https://github.com/apple/pkl/issues/780
- improved dev experience in IntelliJ
2024-12-06 13:57:19 +01:00
Josh B 077497d9b8 Fix a possible deadlock during external reader process close (#786) (#787)
* Fix a possible deadlock during external reader process close

* Apply spotless

---------

Co-authored-by: Philip K.F. Hölzenspies <holzensp@gmail.com>
2024-11-08 13:18:13 -08:00
Nick Muerdter 552b301451 Fix broken link to documentation site in release notes (#784) 2024-11-05 12:58:43 -08:00
53 changed files with 1032 additions and 512 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
name: main name: main
title: Main Project title: Main Project
version: 0.27.0 version: 0.27.1
prerelease: false prerelease: false
nav: nav:
- nav.adoc - nav.adoc
@@ -3,7 +3,7 @@
// the following attributes must be updated immediately before a release // the following attributes must be updated immediately before a release
// pkl version corresponding to current git commit without -dev suffix or git hash // pkl version corresponding to current git commit without -dev suffix or git hash
:pkl-version-no-suffix: 0.27.0 :pkl-version-no-suffix: 0.27.1
// tells whether pkl version corresponding to current git commit // tells whether pkl version corresponding to current git commit
// is a release version (:is-release-version: '') or dev version (:!is-release-version:) // is a release version (:is-release-version: '') or dev version (:!is-release-version:)
:is-release-version: '' :is-release-version: ''
+2 -2
View File
@@ -1,6 +1,6 @@
= Pkl 0.27 Release Notes = Pkl 0.27 Release Notes
:version: 0.27 :version: 0.27
:version-minor: 0.27.0 :version-minor: 0.27.1
:release-date: November 5th, 2024 :release-date: November 5th, 2024
include::ROOT:partial$component-attributes.adoc[] include::ROOT:partial$component-attributes.adoc[]
@@ -377,7 +377,7 @@ These are:
The `String` to `Number` converter methods, for example, {uri-stdlib-StringToInt}[`String.toInt()`], can now handle underscore separators (https://github.com/apple/pkl/pull/578[#578], https://github.com/apple/pkl/pull/580[#580]). The `String` to `Number` converter methods, for example, {uri-stdlib-StringToInt}[`String.toInt()`], can now handle underscore separators (https://github.com/apple/pkl/pull/578[#578], https://github.com/apple/pkl/pull/580[#580]).
This better aligns with the source code representation of file:///Users/danielchao/code/apple/pkl-lang.org/build/local/main/current/language-reference/index.html#integers[number literals]. This better aligns with the source code representation of xref:language-reference:index.adoc#integers[number literals].
[source,pkl] [source,pkl]
---- ----
@@ -1,6 +1,28 @@
= Changelog = Changelog
include::ROOT:partial$component-attributes.adoc[] include::ROOT:partial$component-attributes.adoc[]
[[release-0.27.1]]
== 0.27.1 (2024-12-06)
=== Fixes
- Fixes a broken "number literals" link in the 0.27 release notes (https://github.com/apple/pkl/pull/784[#784]).
- Fixes a possible deadlock during external reader process close (https://github.com/apple/pkl/pull/786[#786]).
- Fixes counting elements with computed indices multiple times in length computation of listings (https://github.com/apple/pkl/pull/797[#797]).
- Fixes non Pkl modules being reported in GatherImports task, leading to plugin failures (https://github.com/apple/pkl/pull/821[#821]).
- Fixes a problem where the delegate chain of type casts for Listing/Mapping get unreasonably big, even though the type nodes are the same, which may lead to a stack overflow or performance degradation (https://github.com/apple/pkl/pull/826[#826]).
- Fixes incorrect scoping of type variables in lazy Listing/Mapping type checking in cross-module typealiases (https://github.com/apple/pkl/pull/789[#789]).
- Fixes regression in type checking logic for Listing/Mapping (https://github.com/apple/pkl/pull/789[#789]).
=== Contributors ❤️
Thank you to all the contributors for this release!
* link:https://github.com/GUI[@GUI]
* link:https://github.com/HT154[@HT154]
* link:https://github.com/odenix[@odenix] (formerly @translatenix)
[[release-0.27.0]] [[release-0.27.0]]
== 0.27.0 (2024-11-05) == 0.27.0 (2024-11-05)
+1 -1
View File
@@ -1,7 +1,7 @@
# suppress inspection "UnusedProperty" for whole file # suppress inspection "UnusedProperty" for whole file
group=org.pkl-lang group=org.pkl-lang
version=0.27.0 version=0.27.1
# google-java-format requires jdk.compiler exports # google-java-format requires jdk.compiler exports
org.gradle.jvmargs= \ org.gradle.jvmargs= \
+3 -3
View File
@@ -2,15 +2,15 @@
"catalogs": {}, "catalogs": {},
"aliases": { "aliases": {
"pkl": { "pkl": {
"script-ref": "org.pkl-lang:pkl-cli-java:0.27.0", "script-ref": "org.pkl-lang:pkl-cli-java:0.27.1",
"java-agents": [] "java-agents": []
}, },
"pkl-codegen-java": { "pkl-codegen-java": {
"script-ref": "org.pkl-lang:pkl-codegen-java:0.27.0", "script-ref": "org.pkl-lang:pkl-codegen-java:0.27.1",
"java-agents": [] "java-agents": []
}, },
"pkl-codegen-kotlin": { "pkl-codegen-kotlin": {
"script-ref": "org.pkl-lang:pkl-codegen-kotlin:0.27.0", "script-ref": "org.pkl-lang:pkl-codegen-kotlin:0.27.1",
"java-agents": [] "java-agents": []
} }
}, },
@@ -19,6 +19,7 @@ import java.nio.file.Path
import java.util.stream.Collectors import java.util.stream.Collectors
import kotlin.io.path.* import kotlin.io.path.*
import org.assertj.core.api.Assertions.fail import org.assertj.core.api.Assertions.fail
import org.opentest4j.AssertionFailedError
import org.pkl.commons.* import org.pkl.commons.*
object FileTestUtils { object FileTestUtils {
@@ -110,5 +111,11 @@ data class SnippetOutcome(val expectedOutFile: Path, val actual: String, val suc
} }
private fun failWithDiff(message: String): Nothing = private fun failWithDiff(message: String): Nothing =
if (System.getProperty("sun.java.command", "").contains("intellij")) {
// IntelliJ only shows diffs for AssertionFailedError
throw AssertionFailedError(message, expected, actual)
} else {
// Gradle test logging/report only shows diffs for PklAssertionFailedError
throw PklAssertionFailedError(message, expected, actual) throw PklAssertionFailedError(message, expected, actual)
}
} }
@@ -1221,7 +1221,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
member.initConstantValue(constantNode); member.initConstantValue(constantNode);
} else { } else {
member.initMemberNode( member.initMemberNode(
new UntypedObjectMemberNode( ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, elementNode)); language, scope.buildFrameDescriptor(), member, elementNode));
} }
@@ -1278,7 +1278,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
member.initConstantValue(constantNode); member.initConstantValue(constantNode);
} else { } else {
member.initMemberNode( member.initMemberNode(
new UntypedObjectMemberNode( ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, valueNode)); language, scope.buildFrameDescriptor(), member, valueNode));
} }
} else { // ["key"] { ... } } else { // ["key"] { ... }
@@ -1287,7 +1287,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
objectBodyCtxs, objectBodyCtxs,
new ReadSuperEntryNode(unavailableSourceSection(), new GetMemberKeyNode())); new ReadSuperEntryNode(unavailableSourceSection(), new GetMemberKeyNode()));
member.initMemberNode( member.initMemberNode(
new UntypedObjectMemberNode( ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, objectBody)); language, scope.buildFrameDescriptor(), member, objectBody));
} }
@@ -2446,6 +2446,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
return new UnresolvedTypeNode.Parameterized( return new UnresolvedTypeNode.Parameterized(
createSourceSection(ctx), createSourceSection(ctx),
language,
doVisitTypeName(idCtx), doVisitTypeName(idCtx),
argCtx.ts.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new)); argCtx.ts.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new));
} }
@@ -103,7 +103,7 @@ public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode {
var callTarget = member.getCallTarget(); var callTarget = member.getCallTarget();
value = callTarget.call(parent, owner, key); value = callTarget.call(parent, owner, key);
} }
owner.setCachedValue(key, value, member); owner.setCachedValue(key, value);
} }
frame.setAuxiliarySlot(customThisSlot, value); frame.setAuxiliarySlot(customThisSlot, value);
@@ -43,15 +43,15 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
@Children private final ExpressionNode[] keyNodes; @Children private final ExpressionNode[] keyNodes;
private final ObjectMember[] values; private final ObjectMember[] values;
public EntriesLiteralNode( protected EntriesLiteralNode(
SourceSection sourceSection, SourceSection sourceSection,
VmLanguage language, VmLanguage language,
// contains local properties and default property (if present)
// does *not* contain entries with constant keys to maintain definition order of entries
String qualifiedScopeName, String qualifiedScopeName,
boolean isCustomThisScope, boolean isCustomThisScope,
@Nullable FrameDescriptor parametersDescriptor, @Nullable FrameDescriptor parametersDescriptor,
UnresolvedTypeNode[] parameterTypes, UnresolvedTypeNode[] parameterTypes,
// contains local properties and default property (if present)
// does *not* contain entries with constant keys to maintain definition order of entries
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
ExpressionNode[] keyNodes, ExpressionNode[] keyNodes,
ObjectMember[] values) { ObjectMember[] values) {
@@ -103,7 +103,8 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
frame.materialize(), frame.materialize(),
parent, parent,
createListMembers(frame, parent.getLength()), createListMembers(frame, parent.getLength()),
parent.getLength() + keyNodes.length); // `[x] = y` overrides existing element and doesn't increase length
parent.getLength());
} }
@Specialization @Specialization
@@ -71,7 +71,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
if (result == null) { if (result == null) {
result = callNode.call(objReceiver, owner, property.getName()); result = callNode.call(objReceiver, owner, property.getName());
objReceiver.setCachedValue(property, result, property); objReceiver.setCachedValue(property, result);
} }
return result; return result;
@@ -184,12 +184,12 @@ public final class ResolveVariableNode extends ExpressionNode {
if (member != null) { if (member != null) {
var constantValue = member.getConstantValue(); var constantValue = member.getConstantValue();
if (constantValue != null) { if (constantValue != null) {
baseModule.setCachedValue(variableName, constantValue, member); baseModule.setCachedValue(variableName, constantValue);
return new ConstantValueNode(sourceSection, constantValue); return new ConstantValueNode(sourceSection, constantValue);
} }
var computedValue = member.getCallTarget().call(baseModule, baseModule); var computedValue = member.getCallTarget().call(baseModule, baseModule);
baseModule.setCachedValue(variableName, computedValue, member); baseModule.setCachedValue(variableName, computedValue);
return new ConstantValueNode(sourceSection, computedValue); return new ConstantValueNode(sourceSection, computedValue);
} }
} }
@@ -0,0 +1,73 @@
/*
* 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.member;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.dsl.Executed;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.ast.expression.primary.GetReceiverNode;
import org.pkl.core.runtime.VmDynamic;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmListing;
import org.pkl.core.runtime.VmMapping;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.util.Nullable;
/** Equivalent of {@link TypedPropertyNode} for elements/entries. */
public abstract class ElementOrEntryNode extends RegularMemberNode {
@Child @Executed protected ExpressionNode receiverNode = new GetReceiverNode();
protected ElementOrEntryNode(
@Nullable VmLanguage language,
FrameDescriptor descriptor,
ObjectMember member,
ExpressionNode bodyNode) {
super(language, descriptor, member, bodyNode);
}
@Specialization
protected Object evalListing(
VirtualFrame frame,
VmListing receiver,
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
var result = executeBody(frame);
return VmUtils.shouldRunTypeCheck(frame)
? receiver.executeTypeCasts(result, VmUtils.getOwner(frame), callNode, null, null)
: result;
}
@Specialization
protected Object evalMapping(
VirtualFrame frame,
VmMapping receiver,
@Cached("create()") @Shared("callNode") IndirectCallNode callNode) {
var result = executeBody(frame);
return VmUtils.shouldRunTypeCheck(frame)
? receiver.executeTypeCasts(result, VmUtils.getOwner(frame), callNode, null, null)
: result;
}
@Specialization
protected Object evalDynamic(VirtualFrame frame, VmDynamic ignored) {
return executeBody(frame);
}
}
@@ -26,7 +26,7 @@ import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** Performs a typecast on a Mapping entry value, or a Listing element. */ /** Performs a typecast on a Mapping entry value, or a Listing element. */
public class ListingOrMappingTypeCastNode extends PklRootNode { public final class ListingOrMappingTypeCastNode extends PklRootNode {
@Child private TypeNode typeNode; @Child private TypeNode typeNode;
private final String qualifiedName; private final String qualifiedName;
@@ -73,7 +73,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(importName); var result = module.getCachedValue(importName);
if (result == null) { if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, importName); result = callNode.call(member.getCallTarget(), module, module, importName);
module.setCachedValue(importName, result, member); module.setCachedValue(importName, result);
} }
return (VmTyped) result; return (VmTyped) result;
} }
@@ -94,7 +94,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(typeName); var result = module.getCachedValue(typeName);
if (result == null) { if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, typeName); result = callNode.call(member.getCallTarget(), module, module, typeName);
module.setCachedValue(typeName, result, member); module.setCachedValue(typeName, result);
} }
return result; return result;
} }
@@ -21,6 +21,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback; import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlotKind; import com.oracle.truffle.api.frame.FrameSlotKind;
import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.frame.VirtualFrame;
@@ -161,7 +162,11 @@ public abstract class TypeNode extends PklNode {
} }
/** Tells if this typenode is the same typecheck as the other typenode. */ /** Tells if this typenode is the same typecheck as the other typenode. */
public abstract boolean isEquivalentTo(TypeNode other); public boolean isEquivalentTo(TypeNode other) {
return this == other || doIsEquivalentTo(other);
}
protected abstract boolean doIsEquivalentTo(TypeNode other);
public @Nullable VmClass getVmClass() { public @Nullable VmClass getVmClass() {
return null; return null;
@@ -304,7 +309,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UnknownTypeNode; return other instanceof UnknownTypeNode;
} }
@@ -371,7 +376,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NothingTypeNode; return other instanceof NothingTypeNode;
} }
@@ -408,7 +413,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FinalModuleTypeNode finalModuleTypeNode)) { if (!(other instanceof FinalModuleTypeNode finalModuleTypeNode)) {
return false; return false;
} }
@@ -460,7 +465,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NonFinalModuleTypeNode nonFinalModuleTypeNode)) { if (!(other instanceof NonFinalModuleTypeNode nonFinalModuleTypeNode)) {
return false; return false;
} }
@@ -496,7 +501,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof StringLiteralTypeNode stringLiteralTypeNode)) { if (!(other instanceof StringLiteralTypeNode stringLiteralTypeNode)) {
return false; return false;
} }
@@ -551,7 +556,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof TypedTypeNode; return other instanceof TypedTypeNode;
} }
@@ -586,7 +591,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof DynamicTypeNode; return other instanceof DynamicTypeNode;
} }
@@ -636,7 +641,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FinalClassTypeNode finalClassTypeNode)) { if (!(other instanceof FinalClassTypeNode finalClassTypeNode)) {
return false; return false;
} }
@@ -705,7 +710,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NonFinalClassTypeNode nonFinalClassTypeNode)) { if (!(other instanceof NonFinalClassTypeNode nonFinalClassTypeNode)) {
return false; return false;
} }
@@ -772,7 +777,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NullableTypeNode nullableTypeNode)) { if (!(other instanceof NullableTypeNode nullableTypeNode)) {
return false; return false;
} }
@@ -842,7 +847,7 @@ public abstract class TypeNode extends PklNode {
@Override @Override
@ExplodeLoop @ExplodeLoop
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof UnionTypeNode unionTypeNode)) { if (!(other instanceof UnionTypeNode unionTypeNode)) {
return false; return false;
} }
@@ -1016,7 +1021,7 @@ public abstract class TypeNode extends PklNode {
@Override @Override
@TruffleBoundary @TruffleBoundary
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof UnionOfStringLiteralsTypeNode unionOfStringLiteralsTypeNode)) { if (!(other instanceof UnionOfStringLiteralsTypeNode unionOfStringLiteralsTypeNode)) {
return false; return false;
} }
@@ -1092,7 +1097,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return false; return false;
} }
@@ -1223,7 +1228,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof ListTypeNode listTypeNode)) { if (!(other instanceof ListTypeNode listTypeNode)) {
return false; return false;
} }
@@ -1268,7 +1273,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof SetTypeNode setTypeNode)) { if (!(other instanceof SetTypeNode setTypeNode)) {
return false; return false;
} }
@@ -1360,7 +1365,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof MapTypeNode mapTypeNode)) { if (!(other instanceof MapTypeNode mapTypeNode)) {
return false; return false;
} }
@@ -1414,8 +1419,9 @@ public abstract class TypeNode extends PklNode {
} }
public static final class ListingTypeNode extends ListingOrMappingTypeNode { public static final class ListingTypeNode extends ListingOrMappingTypeNode {
public ListingTypeNode(SourceSection sourceSection, TypeNode valueTypeNode) { public ListingTypeNode(
super(sourceSection, null, valueTypeNode); SourceSection sourceSection, VmLanguage language, TypeNode valueTypeNode) {
super(sourceSection, language, null, valueTypeNode);
} }
@Override @Override
@@ -1423,7 +1429,17 @@ public abstract class TypeNode extends PklNode {
if (!(value instanceof VmListing vmListing)) { if (!(value instanceof VmListing vmListing)) {
throw typeMismatch(value, BaseModule.getListingClass()); throw typeMismatch(value, BaseModule.getListingClass());
} }
return doTypeCast(frame, vmListing); if (vmListing.isValueTypeKnownSubtypeOf(valueTypeNode)) {
return vmListing;
}
return new VmListing(
vmListing.getEnclosingFrame(),
vmListing,
EconomicMaps.emptyMap(),
vmListing.getLength(),
getValueTypeCastNode(),
VmUtils.getReceiver(frame),
VmUtils.getOwner(frame));
} }
@Override @Override
@@ -1451,7 +1467,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof ListingTypeNode listingTypeNode)) { if (!(other instanceof ListingTypeNode listingTypeNode)) {
return false; return false;
} }
@@ -1466,9 +1482,12 @@ public abstract class TypeNode extends PklNode {
public static final class MappingTypeNode extends ListingOrMappingTypeNode { public static final class MappingTypeNode extends ListingOrMappingTypeNode {
public MappingTypeNode( public MappingTypeNode(
SourceSection sourceSection, TypeNode keyTypeNode, TypeNode valueTypeNode) { SourceSection sourceSection,
VmLanguage language,
TypeNode keyTypeNode,
TypeNode valueTypeNode) {
super(sourceSection, keyTypeNode, valueTypeNode); super(sourceSection, language, keyTypeNode, valueTypeNode);
} }
@Override @Override
@@ -1478,7 +1497,16 @@ public abstract class TypeNode extends PklNode {
} }
// execute type checks on mapping keys // execute type checks on mapping keys
doEagerCheck(frame, vmMapping, false, true); doEagerCheck(frame, vmMapping, false, true);
return doTypeCast(frame, vmMapping); if (vmMapping.isValueTypeKnownSubtypeOf(valueTypeNode)) {
return vmMapping;
}
return new VmMapping(
vmMapping.getEnclosingFrame(),
vmMapping,
EconomicMaps.emptyMap(),
getValueTypeCastNode(),
VmUtils.getReceiver(frame),
VmUtils.getOwner(frame));
} }
@Override @Override
@@ -1509,7 +1537,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof MappingTypeNode mappingTypeNode)) { if (!(other instanceof MappingTypeNode mappingTypeNode)) {
return false; return false;
} }
@@ -1526,17 +1554,22 @@ public abstract class TypeNode extends PklNode {
} }
public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode { public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode {
private final VmLanguage language;
@Child protected @Nullable TypeNode keyTypeNode; @Child protected @Nullable TypeNode keyTypeNode;
@Child protected TypeNode valueTypeNode; @Child protected TypeNode valueTypeNode;
@Child @Nullable protected ListingOrMappingTypeCastNode listingOrMappingTypeCastNode; @Child @Nullable protected ListingOrMappingTypeCastNode valueTypeCastNode;
private final boolean skipKeyTypeChecks; private final boolean skipKeyTypeChecks;
private final boolean skipValueTypeChecks; private final boolean skipValueTypeChecks;
protected ListingOrMappingTypeNode( protected ListingOrMappingTypeNode(
SourceSection sourceSection, @Nullable TypeNode keyTypeNode, TypeNode valueTypeNode) { SourceSection sourceSection,
VmLanguage language,
@Nullable TypeNode keyTypeNode,
TypeNode valueTypeNode) {
super(sourceSection); super(sourceSection);
this.language = language;
this.keyTypeNode = keyTypeNode; this.keyTypeNode = keyTypeNode;
this.valueTypeNode = valueTypeNode; this.valueTypeNode = valueTypeNode;
@@ -1556,17 +1589,14 @@ public abstract class TypeNode extends PklNode {
return valueTypeNode; return valueTypeNode;
} }
protected ListingOrMappingTypeCastNode getListingOrMappingTypeCastNode() { protected ListingOrMappingTypeCastNode getValueTypeCastNode() {
if (listingOrMappingTypeCastNode == null) { if (valueTypeCastNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate(); CompilerDirectives.transferToInterpreterAndInvalidate();
listingOrMappingTypeCastNode = valueTypeCastNode =
new ListingOrMappingTypeCastNode( new ListingOrMappingTypeCastNode(
VmLanguage.get(this), language, new FrameDescriptor(), valueTypeNode, getRootNode().getName());
getRootNode().getFrameDescriptor(),
valueTypeNode,
getRootNode().getName());
} }
return listingOrMappingTypeCastNode; return valueTypeCastNode;
} }
// either (if defaultMemberValue != null): // either (if defaultMemberValue != null):
@@ -1647,15 +1677,6 @@ public abstract class TypeNode extends PklNode {
EconomicMaps.of(Identifier.DEFAULT, defaultMember)); EconomicMaps.of(Identifier.DEFAULT, defaultMember));
} }
protected <T extends VmListingOrMapping<T>> T doTypeCast(VirtualFrame frame, T original) {
// optimization: don't create new object if the original already has the same typecheck, or if
// this typecheck is a no-op.
if (isNoopTypeCheck() || original.hasSameChecksAs(valueTypeNode)) {
return original;
}
return original.withCheckedMembers(getListingOrMappingTypeCastNode(), frame.materialize());
}
protected void doEagerCheck(VirtualFrame frame, VmObject object) { protected void doEagerCheck(VirtualFrame frame, VmObject object) {
doEagerCheck(frame, object, skipKeyTypeChecks, skipValueTypeChecks); doEagerCheck(frame, object, skipKeyTypeChecks, skipValueTypeChecks);
} }
@@ -1700,7 +1721,7 @@ public abstract class TypeNode extends PklNode {
var callTarget = member.getCallTarget(); var callTarget = member.getCallTarget();
memberValue = callTarget.call(object, owner, memberKey); memberValue = callTarget.call(object, owner, memberKey);
} }
object.setCachedValue(memberKey, memberValue, member); object.setCachedValue(memberKey, memberValue);
} }
valueTypeNode.executeEagerly(frame, memberValue); valueTypeNode.executeEagerly(frame, memberValue);
} }
@@ -1748,7 +1769,7 @@ public abstract class TypeNode extends PklNode {
@Override @Override
@ExplodeLoop @ExplodeLoop
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionTypeNode functionTypeNode)) { if (!(other instanceof FunctionTypeNode functionTypeNode)) {
return false; return false;
} }
@@ -1845,7 +1866,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionClassTypeNode functionClassTypeNode)) { if (!(other instanceof FunctionClassTypeNode functionClassTypeNode)) {
return false; return false;
} }
@@ -1883,7 +1904,7 @@ public abstract class TypeNode extends PklNode {
@Override @Override
@ExplodeLoop @ExplodeLoop
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionNClassTypeNode functionNClassTypeNode)) { if (!(other instanceof FunctionNClassTypeNode functionNClassTypeNode)) {
return false; return false;
} }
@@ -1986,7 +2007,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof PairTypeNode pairTypeNode)) { if (!(other instanceof PairTypeNode pairTypeNode)) {
return false; return false;
} }
@@ -2043,7 +2064,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof VarArgsTypeNode varArgsTypeNode)) { if (!(other instanceof VarArgsTypeNode varArgsTypeNode)) {
return false; return false;
} }
@@ -2095,7 +2116,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof TypeVariableNode; return other instanceof TypeVariableNode;
} }
@@ -2135,7 +2156,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NonNullTypeAliasTypeNode; return other instanceof NonNullTypeAliasTypeNode;
} }
@@ -2184,7 +2205,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UIntTypeAliasTypeNode aliasTypeNode && mask == aliasTypeNode.mask; return other instanceof UIntTypeAliasTypeNode aliasTypeNode && mask == aliasTypeNode.mask;
} }
@@ -2228,7 +2249,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int8TypeAliasTypeNode; return other instanceof Int8TypeAliasTypeNode;
} }
@@ -2272,7 +2293,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int16TypeAliasTypeNode; return other instanceof Int16TypeAliasTypeNode;
} }
@@ -2316,7 +2337,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int32TypeAliasTypeNode; return other instanceof Int32TypeAliasTypeNode;
} }
@@ -2387,14 +2408,14 @@ public abstract class TypeNode extends PklNode {
public Object execute(VirtualFrame frame, Object value) { public Object execute(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame); var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame())); setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame())); setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try { try {
return aliasedTypeNode.execute(frame, value); return aliasedTypeNode.execute(frame, value);
} finally { } finally {
VmUtils.setOwner(frame, prevOwner); setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver); setReceiver(frame, prevReceiver);
} }
} }
@@ -2403,14 +2424,14 @@ public abstract class TypeNode extends PklNode {
public Object executeAndSet(VirtualFrame frame, Object value) { public Object executeAndSet(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame); var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame())); setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame())); setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try { try {
return aliasedTypeNode.executeAndSet(frame, value); return aliasedTypeNode.executeAndSet(frame, value);
} finally { } finally {
VmUtils.setOwner(frame, prevOwner); setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver); setReceiver(frame, prevReceiver);
} }
} }
@@ -2419,14 +2440,14 @@ public abstract class TypeNode extends PklNode {
public Object executeEagerlyAndSet(VirtualFrame frame, Object value) { public Object executeEagerlyAndSet(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame); var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame); var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame())); setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame())); setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try { try {
return aliasedTypeNode.executeEagerlyAndSet(frame, value); return aliasedTypeNode.executeEagerlyAndSet(frame, value);
} finally { } finally {
VmUtils.setOwner(frame, prevOwner); setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver); setReceiver(frame, prevReceiver);
} }
} }
@@ -2471,7 +2492,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
if ((other instanceof TypeAliasTypeNode typeAliasTypeNode)) { if ((other instanceof TypeAliasTypeNode typeAliasTypeNode)) {
return aliasedTypeNode.isEquivalentTo(typeAliasTypeNode.aliasedTypeNode); return aliasedTypeNode.isEquivalentTo(typeAliasTypeNode.aliasedTypeNode);
} }
@@ -2498,6 +2519,22 @@ public abstract class TypeNode extends PklNode {
protected boolean isParametric() { protected boolean isParametric() {
return typeArgumentNodes.length > 0; return typeArgumentNodes.length > 0;
} }
// Note that mutating a frame's receiver and owner argument is very risky
// because any VmObject instantiated within the same root node execution
// holds a reference to (not immutable snapshot of) the frame
// via VmObjectLike.enclosingFrame.
// *Maybe* this works out for TypeAliasTypeNode because an object instantiated
// within a type constraint doesn't escape the constraint expression.
// If mutating receiver and owner can't be avoided, it would be safer
// to have VmObjectLike store them directly instead of storing enclosingFrame.
private static void setReceiver(Frame frame, Object receiver) {
frame.getArguments()[0] = receiver;
}
private static void setOwner(Frame frame, VmObjectLike owner) {
frame.getArguments()[1] = owner;
}
} }
public static final class ConstrainedTypeNode extends TypeNode { public static final class ConstrainedTypeNode extends TypeNode {
@@ -2581,7 +2618,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
// consider constrained types as always different // consider constrained types as always different
return false; return false;
} }
@@ -2622,7 +2659,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof AnyTypeNode; return other instanceof AnyTypeNode;
} }
@@ -2650,7 +2687,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof StringTypeNode; return other instanceof StringTypeNode;
} }
@@ -2714,7 +2751,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NumberTypeNode; return other instanceof NumberTypeNode;
} }
@@ -2742,7 +2779,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof IntTypeNode; return other instanceof IntTypeNode;
} }
@@ -2787,7 +2824,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof FloatTypeNode; return other instanceof FloatTypeNode;
} }
@@ -2832,7 +2869,7 @@ public abstract class TypeNode extends PklNode {
} }
@Override @Override
public boolean isEquivalentTo(TypeNode other) { public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof BooleanTypeNode; return other instanceof BooleanTypeNode;
} }
@@ -197,14 +197,17 @@ public abstract class UnresolvedTypeNode extends PklNode {
} }
public static final class Parameterized extends UnresolvedTypeNode { public static final class Parameterized extends UnresolvedTypeNode {
private final VmLanguage language;
@Child private ExpressionNode resolveTypeNode; @Child private ExpressionNode resolveTypeNode;
@Children private final UnresolvedTypeNode[] typeArgumentNodes; @Children private final UnresolvedTypeNode[] typeArgumentNodes;
public Parameterized( public Parameterized(
SourceSection sourceSection, SourceSection sourceSection,
VmLanguage language,
ExpressionNode resolveTypeNode, ExpressionNode resolveTypeNode,
UnresolvedTypeNode[] typeArgumentNodes) { UnresolvedTypeNode[] typeArgumentNodes) {
super(sourceSection); super(sourceSection);
this.language = language;
this.resolveTypeNode = resolveTypeNode; this.resolveTypeNode = resolveTypeNode;
this.typeArgumentNodes = typeArgumentNodes; this.typeArgumentNodes = typeArgumentNodes;
} }
@@ -238,12 +241,13 @@ public abstract class UnresolvedTypeNode extends PklNode {
} }
if (clazz.isListingClass()) { if (clazz.isListingClass()) {
return new ListingTypeNode(sourceSection, typeArgumentNodes[0].execute(frame)); return new ListingTypeNode(sourceSection, language, typeArgumentNodes[0].execute(frame));
} }
if (clazz.isMappingClass()) { if (clazz.isMappingClass()) {
return new MappingTypeNode( return new MappingTypeNode(
sourceSection, sourceSection,
language,
typeArgumentNodes[0].execute(frame), typeArgumentNodes[0].execute(frame),
typeArgumentNodes[1].execute(frame)); typeArgumentNodes[1].execute(frame));
} }
@@ -17,17 +17,16 @@ package org.pkl.core.externalreader;
import java.io.IOException; import java.io.IOException;
import java.lang.ProcessBuilder.Redirect; import java.lang.ProcessBuilder.Redirect;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import org.pkl.core.Duration;
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader; import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader;
import org.pkl.core.externalreader.ExternalReaderMessages.*; import org.pkl.core.externalreader.ExternalReaderMessages.*;
import org.pkl.core.messaging.MessageTransport; import org.pkl.core.messaging.MessageTransport;
@@ -152,40 +151,23 @@ final class ExternalReaderProcessImpl implements ExternalReaderProcess {
@Override @Override
public void close() { public void close() {
synchronized (lock) { synchronized (lock) {
if (closed) return;
closed = true; closed = true;
if (process == null || !process.isAlive()) {
return;
}
try { try {
if (transport != null) { if (transport != null && process != null && process.isAlive()) {
transport.send(new CloseExternalProcess()); transport.send(new CloseExternalProcess());
transport.close(); process.waitFor(CLOSE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
} }
} catch (Exception ignored) {
// forcefully stop the process after the timeout
// note that both transport.close() and process.destroy() are safe to call multiple times
new Timer()
.schedule(
new TimerTask() {
@Override
public void run() {
if (process != null) {
transport.close();
process.destroyForcibly();
}
}
},
CLOSE_TIMEOUT.inWholeMillis());
// block on process exit
process.onExit().get();
} catch (Exception e) {
transport.close();
process.destroyForcibly();
} finally { } finally {
process = null; if (process != null) {
transport = null; // no-op unless process is alive
process.destroyForcibly();
}
if (transport != null) {
transport.close();
}
} }
} }
} }
@@ -113,7 +113,7 @@ public final class VmFunction extends VmObjectLike {
@Override @Override
@TruffleBoundary @TruffleBoundary
public void setCachedValue(Object key, Object value, ObjectMember objectMember) { public void setCachedValue(Object key, Object value) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.bug("Class `VmFunction` does not support method `setCachedValue()`.") .bug("Class `VmFunction` does not support method `setCachedValue()`.")
.build(); .build();
@@ -33,7 +33,7 @@ import org.pkl.core.util.Nullable;
@TruffleLanguage.Registration( @TruffleLanguage.Registration(
id = "pkl", id = "pkl",
name = "Pkl", name = "Pkl",
version = "0.27.0", version = "0.27.1",
characterMimeTypes = VmLanguage.MIME_TYPE, characterMimeTypes = VmLanguage.MIME_TYPE,
contextPolicy = ContextPolicy.SHARED) contextPolicy = ContextPolicy.SHARED)
public final class VmLanguage extends TruffleLanguage<VmContext> { public final class VmLanguage extends TruffleLanguage<VmContext> {
@@ -19,14 +19,13 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public final class VmListing extends VmListingOrMapping<VmListing> { public final class VmListing extends VmListingOrMapping {
private static final class EmptyHolder { private static final class EmptyHolder {
private static final VmListing EMPTY = private static final VmListing EMPTY =
new VmListing( new VmListing(
@@ -47,7 +46,7 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
VmObject parent, VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
int length) { int length) {
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null); super(enclosingFrame, parent, members);
this.length = length; this.length = length;
} }
@@ -56,16 +55,10 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
VmObject parent, VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
int length, int length,
@Nullable VmListing delegate, ListingOrMappingTypeCastNode typeCastNode,
ListingOrMappingTypeCastNode typeCheckNode, Object typeCheckReceiver,
MaterializedFrame typeNodeFrame) { VmObjectLike typeCheckOwner) {
super( super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
this.length = length; this.length = length;
} }
@@ -117,20 +110,6 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
return converter.convertListing(this, path); return converter.convertListing(this, path);
} }
@Override
public VmListing withCheckedMembers(
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) {
return new VmListing(
getEnclosingFrame(),
Objects.requireNonNull(parent),
members,
length,
this,
typeCheckNode,
typeNodeFrame);
}
@Override @Override
@TruffleBoundary @TruffleBoundary
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
@@ -142,10 +121,14 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
force(false); force(false);
other.force(false); other.force(false);
for (var i = 0L; i < length; i++) { var cursor = cachedValues.getEntries();
var value = getCachedValue(i); while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null; assert value != null;
var otherValue = other.getCachedValue(i); var otherValue = other.getCachedValue(key);
if (!value.equals(otherValue)) return false; if (!value.equals(otherValue)) return false;
} }
@@ -156,14 +139,16 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
@TruffleBoundary @TruffleBoundary
public int hashCode() { public int hashCode() {
if (cachedHash != 0) return cachedHash; if (cachedHash != 0) return cachedHash;
// It's possible that the delegate has already computed its hash code.
// If so, we can go ahead and use it.
if (delegate != null && delegate.cachedHash != 0) return delegate.cachedHash;
force(false); force(false);
var result = 0; var result = 0;
for (var i = 0L; i < length; i++) { var cursor = cachedValues.getEntries();
var value = getCachedValue(i);
while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null; assert value != null;
result = 31 * result + value.hashCode(); result = 31 * result + value.hashCode();
} }
@@ -16,159 +16,122 @@
package org.pkl.core.runtime; package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.IndirectCallNode;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.ast.type.TypeNode; import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.EconomicSets;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public abstract class VmListingOrMapping<SELF extends VmListingOrMapping<SELF>> extends VmObject { public abstract class VmListingOrMapping extends VmObject {
// reified type of listing elements and mapping values
/**
* A Listing or Mapping typecast creates a new object that contains a new typecheck node, and
* delegates member lookups to this delegate.
*/
protected final @Nullable SELF delegate;
private final @Nullable ListingOrMappingTypeCastNode typeCastNode; private final @Nullable ListingOrMappingTypeCastNode typeCastNode;
private final MaterializedFrame typeNodeFrame; private final @Nullable Object typeCheckReceiver;
private final EconomicMap<Object, ObjectMember> cachedMembers = EconomicMaps.create(); private final @Nullable VmObjectLike typeCheckOwner;
private final EconomicSet<Object> checkedMembers = EconomicSets.create();
public VmListingOrMapping(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, parent, members);
typeCastNode = null;
typeCheckReceiver = null;
typeCheckOwner = null;
}
public VmListingOrMapping( public VmListingOrMapping(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
@Nullable VmObject parent, @Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
@Nullable SELF delegate, ListingOrMappingTypeCastNode typeCastNode,
@Nullable ListingOrMappingTypeCastNode typeCastNode, Object typeCheckReceiver,
@Nullable MaterializedFrame typeNodeFrame) { VmObjectLike typeCheckOwner) {
super(enclosingFrame, parent, members); super(enclosingFrame, parent, members);
this.delegate = delegate;
this.typeCastNode = typeCastNode; this.typeCastNode = typeCastNode;
this.typeNodeFrame = typeNodeFrame; this.typeCheckReceiver = typeCheckReceiver;
this.typeCheckOwner = typeCheckOwner;
} }
ObjectMember findMember(Object key) { // Recursively executes type casts between `owner` and `this` and returns the resulting value.
var member = EconomicMaps.get(cachedMembers, key); public final Object executeTypeCasts(
if (member != null) { Object value,
return member; VmObjectLike owner,
} IndirectCallNode callNode,
if (delegate != null) { // if non-null, a stack frame for this member is inserted if a type cast fails
return delegate.findMember(key); @Nullable ObjectMember member,
} // Next type cast to be performed by the caller.
// member is guaranteed to exist; this is only called if `getCachedValue()` returns non-null // Avoids repeating the same type cast in some cases.
// and `setCachedValue` will record the object member in `cachedMembers`. @Nullable ListingOrMappingTypeCastNode nextTypeCastNode) {
throw PklBugException.unreachableCode(); var newNextTypeCastNode = typeCastNode != null ? typeCastNode : nextTypeCastNode;
} @SuppressWarnings("DataFlowIssue")
var result =
public @Nullable ListingOrMappingTypeCastNode getTypeCastNode() { this == owner
return typeCastNode; ? value
} : ((VmListingOrMapping) parent)
.executeTypeCasts(value, owner, callNode, member, newNextTypeCastNode);
@Override if (typeCastNode == null || typeCastNode == nextTypeCastNode) return result;
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
super.setCachedValue(key, value, objectMember);
EconomicMaps.put(cachedMembers, key, objectMember);
}
@Override
public boolean hasCachedValue(Object key) {
return super.hasCachedValue(key) || delegate != null && delegate.hasCachedValue(key);
}
@Override
public @Nullable Object getCachedValue(Object key) {
var myCachedValue = super.getCachedValue(key);
if (myCachedValue != null || delegate == null) {
return myCachedValue;
}
var memberValue = delegate.getCachedValue(key);
// if this object member appears inside `checkedMembers`, we have already checked its type
// and can safely return it.
if (EconomicSets.contains(checkedMembers, key)) {
return memberValue;
}
if (memberValue == null) {
return null;
}
// If a cached value already exists on the delegate, run a typecast on it.
// optimization: don't use `VmUtils.findMember` to avoid iterating over all members
var objectMember = findMember(key);
var ret = typecastObjectMember(objectMember, memberValue, IndirectCallNode.getUncached());
if (ret != memberValue) {
EconomicMaps.put(cachedValues, key, ret);
} else {
// optimization: don't add to own cached values if typecast results in the same value
EconomicSets.add(checkedMembers, key);
}
return ret;
}
@Override
public Object getExtraStorage() {
if (delegate != null) {
return delegate.getExtraStorage();
}
assert extraStorage != null;
return extraStorage;
}
/** Perform a typecast on this member, */
public Object typecastObjectMember(
ObjectMember member, Object memberValue, IndirectCallNode callNode) {
if (!(member.isEntry() || member.isElement()) || typeCastNode == null) {
return memberValue;
}
assert typeNodeFrame != null;
var ret = memberValue;
if (delegate != null) {
ret = delegate.typecastObjectMember(member, ret, callNode);
}
var callTarget = typeCastNode.getCallTarget(); var callTarget = typeCastNode.getCallTarget();
try { try {
return callNode.call( return callNode.call(callTarget, typeCheckReceiver, typeCheckOwner, result);
callTarget, VmUtils.getReceiver(typeNodeFrame), VmUtils.getOwner(typeNodeFrame), ret); } catch (VmException e) {
} catch (VmException vmException) {
CompilerDirectives.transferToInterpreter(); CompilerDirectives.transferToInterpreter();
// treat typecheck as part of the call stack to read the original member if there is a if (member != null) {
// source section for it. VmUtils.insertStackFrame(member, callTarget, e);
var sourceSection = member.getBodySection();
if (!sourceSection.isAvailable()) {
sourceSection = member.getSourceSection();
} }
if (sourceSection.isAvailable()) { throw e;
vmException
.getInsertedStackFrames()
.put(callTarget, VmUtils.createStackFrame(sourceSection, member.getQualifiedName()));
}
throw vmException;
} }
} }
public abstract SELF withCheckedMembers( @Override
ListingOrMappingTypeCastNode typeCastNode, MaterializedFrame typeNodeFrame); @TruffleBoundary
public final @Nullable Object getCachedValue(Object key) {
var result = EconomicMaps.get(cachedValues, key);
// if this object has members, `this[key]` may differ from `parent[key]`, so stop the search
if (result != null || !members.isEmpty()) return result;
/** Tells if this mapping/listing runs the same typechecks as {@code typeNode}. */ // Optimization: Recursively steal value from parent cache to avoid computing it multiple times.
public boolean hasSameChecksAs(TypeNode typeNode) { // The current implementation has the following limitations and drawbacks:
// * It only works if a parent has, coincidentally, already cached `key`.
// * It turns getCachedValue() into an operation that isn't guaranteed to be fast and fail-safe.
// * It requires making VmObject.getCachedValue() non-final,
// which is unfavorable for Truffle partial evaluation and JVM inlining.
// * It may not be worth its cost for constant members and members that are cheap to compute.
assert parent != null; // VmListingOrMapping always has a parent
result = parent.getCachedValue(key);
if (result == null) return null;
if (typeCastNode != null && !(key instanceof Identifier)) {
var callNode = IndirectCallNode.getUncached();
var callTarget = typeCastNode.getCallTarget();
try {
result = callNode.call(callTarget, typeCheckReceiver, typeCheckOwner, result);
} catch (VmException e) {
var member = VmUtils.findMember(parent, key);
assert member != null; // already found the member's cached value
VmUtils.insertStackFrame(member, callTarget, e);
throw e;
}
}
setCachedValue(key, result);
return result;
}
/**
* Tells whether the value type of this listing/mapping is known to be a subtype of {@code
* typeNode}. If {@code true}, type checks of individual values can be elided because
* listings/mappings are covariant in their value type.
*/
public final boolean isValueTypeKnownSubtypeOf(TypeNode typeNode) {
if (typeNode.isNoopTypeCheck()) {
return true;
}
if (typeCastNode == null) { if (typeCastNode == null) {
return false; return false;
} }
if (typeCastNode.getTypeNode().isEquivalentTo(typeNode)) { return typeCastNode.getTypeNode().isEquivalentTo(typeNode);
return true;
}
// we can say the check is the same if the delegate has this check.
// when `Listing<Any>` delegates to `Listing<UInt>`, it has the same checks as a `UInt`
// typenode.
if (delegate != null) {
return delegate.hasSameChecksAs(typeNode);
}
return false;
} }
} }
@@ -18,7 +18,6 @@ package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.MaterializedFrame;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import org.graalvm.collections.UnmodifiableEconomicMap; import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode; import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
@@ -27,7 +26,7 @@ import org.pkl.core.util.CollectionUtils;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.LateInit; import org.pkl.core.util.LateInit;
public final class VmMapping extends VmListingOrMapping<VmMapping> { public final class VmMapping extends VmListingOrMapping {
private int cachedEntryCount = -1; private int cachedEntryCount = -1;
@@ -50,24 +49,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
VmObject parent, VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) { UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, parent, members);
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
} }
public VmMapping( public VmMapping(
MaterializedFrame enclosingFrame, MaterializedFrame enclosingFrame,
VmObject parent, VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members, UnmodifiableEconomicMap<Object, ObjectMember> members,
VmMapping delegate, ListingOrMappingTypeCastNode typeCastNode,
ListingOrMappingTypeCastNode typeCheckNode, Object typeCheckReceiver,
MaterializedFrame typeNodeFrame) { VmObjectLike typeCheckOwner) {
super( super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
} }
public static boolean isDefaultProperty(Object propertyKey) { public static boolean isDefaultProperty(Object propertyKey) {
@@ -81,16 +73,12 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
@TruffleBoundary @TruffleBoundary
public VmSet getAllKeys() { public VmSet getAllKeys() {
if (delegate != null) {
return delegate.getAllKeys();
}
synchronized (this) { synchronized (this) {
if (__allKeys == null) { if (__allKeys == null) {
// building upon parent's `getAllKeys()` should improve at least worst case efficiency // building upon parent's `getAllKeys()` should improve at least worst case efficiency
var parentKeys = var parentKeys = parent instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
getParent() instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
var builder = VmSet.builder(parentKeys); var builder = VmSet.builder(parentKeys);
for (var cursor = getMembers().getEntries(); cursor.advance(); ) { for (var cursor = members.getEntries(); cursor.advance(); ) {
var member = cursor.getValue(); var member = cursor.getValue();
if (!member.isEntry()) continue; if (!member.isEntry()) continue;
builder.add(cursor.getKey()); builder.add(cursor.getKey());
@@ -133,12 +121,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
if (this == obj) return true; if (this == obj) return true;
if (!(obj instanceof VmMapping other)) return false; if (!(obj instanceof VmMapping other)) return false;
if (getEntryCount() != other.getEntryCount()) return false;
// could use shallow force, but deep force is cached // could use shallow force, but deep force is cached
force(false); force(false);
other.force(false); other.force(false);
for (var key : getAllKeys()) { if (getEntryCount() != other.getEntryCount()) return false;
var value = getCachedValue(key);
var cursor = cachedValues.getEntries();
while (cursor.advance()) {
Object key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null; assert value != null;
var otherValue = other.getCachedValue(key); var otherValue = other.getCachedValue(key);
if (!value.equals(otherValue)) return false; if (!value.equals(otherValue)) return false;
@@ -151,38 +144,34 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
@TruffleBoundary @TruffleBoundary
public int hashCode() { public int hashCode() {
if (cachedHash != 0) return cachedHash; if (cachedHash != 0) return cachedHash;
// It's possible that the delegate has already computed its hash code.
// If so, we can go ahead and use it.
if (delegate != null && delegate.cachedHash != 0) return delegate.cachedHash;
force(false); force(false);
var result = 0; var result = 0;
for (var key : getAllKeys()) { var cursor = cachedValues.getEntries();
while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue; if (key instanceof Identifier) continue;
var value = getCachedValue(key);
var value = cursor.getValue();
assert value != null; assert value != null;
result += key.hashCode() ^ value.hashCode(); result += key.hashCode() ^ value.hashCode();
} }
cachedHash = result; cachedHash = result;
return result; return result;
} }
// assumes mapping has been forced
public int getEntryCount() { public int getEntryCount() {
if (cachedEntryCount != -1) return cachedEntryCount; if (cachedEntryCount != -1) return cachedEntryCount;
cachedEntryCount = getAllKeys().getLength();
return cachedEntryCount;
}
@Override var result = 0;
@TruffleBoundary for (var key : cachedValues.getKeys()) {
public VmMapping withCheckedMembers( if (key instanceof Identifier) continue;
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) { result += 1;
return new VmMapping( }
getEnclosingFrame(), cachedEntryCount = result;
Objects.requireNonNull(getParent()), return result;
getMembers(),
this,
typeCheckNode,
typeNodeFrame);
} }
} }
@@ -87,12 +87,12 @@ public abstract class VmObject extends VmObjectLike {
} }
@Override @Override
public void setCachedValue(Object key, Object value, ObjectMember objectMember) { public final void setCachedValue(Object key, Object value) {
EconomicMaps.put(cachedValues, key, value); EconomicMaps.put(cachedValues, key, value);
} }
@Override @Override
public boolean hasCachedValue(Object key) { public final boolean hasCachedValue(Object key) {
return EconomicMaps.containsKey(cachedValues, key); return EconomicMaps.containsKey(cachedValues, key);
} }
@@ -96,7 +96,7 @@ public abstract class VmObjectLike extends VmValue {
* receiver. * receiver.
*/ */
@TruffleBoundary @TruffleBoundary
public abstract void setCachedValue(Object key, Object value, ObjectMember objectMember); public abstract void setCachedValue(Object key, Object value);
/** /**
* Prefer this method over {@link #getCachedValue} if the value is not required. (There is no * Prefer this method over {@link #getCachedValue} if the value is not required. (There is no
@@ -15,6 +15,7 @@
*/ */
package org.pkl.core.runtime; package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.Truffle;
@@ -134,10 +135,6 @@ public final class VmUtils {
return result; return result;
} }
public static void setReceiver(Frame frame, Object receiver) {
frame.getArguments()[0] = receiver;
}
public static VmObjectLike getObjectReceiver(Frame frame) { public static VmObjectLike getObjectReceiver(Frame frame) {
return (VmObjectLike) getReceiver(frame); return (VmObjectLike) getReceiver(frame);
} }
@@ -158,10 +155,6 @@ public final class VmUtils {
return result; return result;
} }
public static void setOwner(Frame frame, VmObjectLike owner) {
frame.getArguments()[1] = owner;
}
/** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */ /** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */
public static Object getMemberKey(Frame frame) { public static Object getMemberKey(Frame frame) {
return frame.getArguments()[2]; return frame.getArguments()[2];
@@ -261,17 +254,17 @@ public final class VmUtils {
final var constantValue = member.getConstantValue(); final var constantValue = member.getConstantValue();
if (constantValue != null) { if (constantValue != null) {
var ret = constantValue; var result = constantValue;
// for a property, do a type check // for a property, Listing element, or Mapping value, do a type check
if (member.isProp()) { if (member.isProp()) {
var property = receiver.getVmClass().getProperty(member.getName()); var property = receiver.getVmClass().getProperty(member.getName());
if (property != null && property.getTypeNode() != null) { if (property != null && property.getTypeNode() != null) {
var callTarget = property.getTypeNode().getCallTarget(); var callTarget = property.getTypeNode().getCallTarget();
try { try {
if (checkType) { if (checkType) {
ret = callNode.call(callTarget, receiver, property.getOwner(), constantValue); result = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
} else { } else {
ret = result =
callNode.call( callNode.call(
callTarget, callTarget,
receiver, receiver,
@@ -281,6 +274,32 @@ public final class VmUtils {
} }
} catch (VmException e) { } catch (VmException e) {
CompilerDirectives.transferToInterpreter(); CompilerDirectives.transferToInterpreter();
insertStackFrame(member, callTarget, e);
throw e;
}
}
} else if (receiver instanceof VmListingOrMapping listingOrMapping
&& owner instanceof VmListingOrMapping) {
// `owner instanceof VmListingOrMapping` guards against
// PropertiesRenderer amending VmDynamic with VmListing (hack?)
result = listingOrMapping.executeTypeCasts(constantValue, owner, callNode, member, null);
}
receiver.setCachedValue(memberKey, result);
return result;
}
var callTarget = member.getCallTarget();
Object result;
if (checkType) {
result = callNode.call(callTarget, receiver, owner, memberKey);
} else {
result = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
}
receiver.setCachedValue(memberKey, result);
return result;
}
// A failed property type check looks as follows in the stack trace: // A failed property type check looks as follows in the stack trace:
// x: Int(isPositive) // x: Int(isPositive)
// at ... // at ...
@@ -290,35 +309,17 @@ public final class VmUtils {
// there's no root node for it and hence no stack trace element. // there's no root node for it and hence no stack trace element.
// In this case, insert a stack trace element to make the stack trace look the same // In this case, insert a stack trace element to make the stack trace look the same
// as in the non-constant case. (Parse-time constants are an internal optimization.) // as in the non-constant case. (Parse-time constants are an internal optimization.)
e.getInsertedStackFrames() public static void insertStackFrame(
.put( ObjectMember member, CallTarget location, VmException exception) {
callTarget, var sourceSection = member.getBodySection();
createStackFrame(member.getBodySection(), member.getQualifiedName())); if (!sourceSection.isAvailable()) {
throw e; sourceSection = member.getSourceSection();
} }
if (sourceSection.isAvailable()) {
exception
.getInsertedStackFrames()
.put(location, createStackFrame(sourceSection, member.getQualifiedName()));
} }
} else if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
if (owner != receiver && owner instanceof VmListingOrMapping<?> vmListingOrMappingOwner) {
ret = vmListingOrMappingOwner.typecastObjectMember(member, ret, callNode);
}
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
}
receiver.setCachedValue(memberKey, ret, member);
return ret;
}
var callTarget = member.getCallTarget();
Object ret;
if (checkType) {
ret = callNode.call(callTarget, receiver, owner, memberKey);
} else {
ret = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
}
if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
}
receiver.setCachedValue(memberKey, ret, member);
return ret;
} }
public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) { public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) {
@@ -50,6 +50,7 @@ import org.pkl.core.runtime.VmDuration;
import org.pkl.core.runtime.VmDynamic; import org.pkl.core.runtime.VmDynamic;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmIntSeq; import org.pkl.core.runtime.VmIntSeq;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmList; import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.VmListing; import org.pkl.core.runtime.VmListing;
import org.pkl.core.runtime.VmMap; import org.pkl.core.runtime.VmMap;
@@ -573,7 +574,8 @@ public final class RendererNodes {
type = type =
requiresWrapper() requiresWrapper()
? null ? null
: new ListingTypeNode(VmUtils.unavailableSourceSection(), valueType); : new ListingTypeNode(
VmUtils.unavailableSourceSection(), VmLanguage.get(null), valueType);
return type; return type;
} else if (type instanceof MappingTypeNode mappingType) { } else if (type instanceof MappingTypeNode mappingType) {
var keyType = resolveType(mappingType.getKeyTypeNode()); var keyType = resolveType(mappingType.getKeyTypeNode());
@@ -587,7 +589,9 @@ public final class RendererNodes {
} }
var valueType = resolveType(mappingType.getValueTypeNode()); var valueType = resolveType(mappingType.getValueTypeNode());
assert valueType != null : "Incomplete or malformed Mapping type"; assert valueType != null : "Incomplete or malformed Mapping type";
mappingType = new MappingTypeNode(VmUtils.unavailableSourceSection(), keyType, valueType); mappingType =
new MappingTypeNode(
VmUtils.unavailableSourceSection(), VmLanguage.get(null), keyType, valueType);
type = requiresWrapper() ? null : mappingType; type = requiresWrapper() ? null : mappingType;
return type; return type;
@@ -30,6 +30,10 @@ import org.graalvm.collections.UnmodifiableMapCursor;
public final class EconomicMaps { public final class EconomicMaps {
private EconomicMaps() {} private EconomicMaps() {}
public static <K, V> UnmodifiableEconomicMap<K, V> emptyMap() {
return EconomicMap.emptyMap();
}
@TruffleBoundary @TruffleBoundary
public static <K, V> EconomicMap<K, V> create() { public static <K, V> EconomicMap<K, V> create() {
return EconomicMap.create(); return EconomicMap.create();
@@ -0,0 +1,3 @@
function isValid(str): Boolean = str.startsWith("a")
foo: Listing<String(isValid(this))>(isDistinct)
@@ -27,12 +27,20 @@ local altered: Listing<Person> = (base) {
[0] { name = "Wood Pigeon" } [0] { name = "Wood Pigeon" }
} }
local computedIndex: Listing<Person> = (base) {
[computeIndex()] { name = "Wood Pigeon" }
}
local function computeIndex() = 0
facts { facts {
["isEmpty"] { ["isEmpty"] {
empty.isEmpty empty.isEmpty
empty2.isEmpty empty2.isEmpty
!base.isEmpty !base.isEmpty
!derived.isEmpty !derived.isEmpty
!altered.isEmpty
!computedIndex.isEmpty
} }
["lastIndex"] { ["lastIndex"] {
@@ -41,6 +49,8 @@ facts {
base.lastIndex == 2 base.lastIndex == 2
derived.lastIndex == 4 derived.lastIndex == 4
duplicate.lastIndex == 5 duplicate.lastIndex == 5
altered.lastIndex == 2
computedIndex.lastIndex == 2
} }
["isDistinct"] { ["isDistinct"] {
@@ -49,6 +59,8 @@ facts {
base.isDistinct base.isDistinct
derived.isDistinct derived.isDistinct
!duplicate.isDistinct !duplicate.isDistinct
altered.isDistinct
computedIndex.isDistinct
} }
["isDistinctBy()"] { ["isDistinctBy()"] {
@@ -57,18 +69,24 @@ facts {
base.isDistinctBy((it) -> it) base.isDistinctBy((it) -> it)
derived.isDistinctBy((it) -> it) derived.isDistinctBy((it) -> it)
!duplicate.isDistinctBy((it) -> it) !duplicate.isDistinctBy((it) -> it)
altered.isDistinctBy((it) -> it)
computedIndex.isDistinctBy((it) -> it)
empty.isDistinctBy((it) -> it.name) empty.isDistinctBy((it) -> it.name)
empty2.isDistinctBy((it) -> it.name) empty2.isDistinctBy((it) -> it.name)
base.isDistinctBy((it) -> it.name) base.isDistinctBy((it) -> it.name)
derived.isDistinctBy((it) -> it.name) derived.isDistinctBy((it) -> it.name)
!duplicate.isDistinctBy((it) -> it.name) !duplicate.isDistinctBy((it) -> it.name)
altered.isDistinctBy((it) -> it.name)
computedIndex.isDistinctBy((it) -> it.name)
empty.isDistinctBy((it) -> it.getClass()) empty.isDistinctBy((it) -> it.getClass())
empty2.isDistinctBy((it) -> it.getClass()) empty2.isDistinctBy((it) -> it.getClass())
!base.isDistinctBy((it) -> it.getClass()) !base.isDistinctBy((it) -> it.getClass())
!derived.isDistinctBy((it) -> it.getClass()) !derived.isDistinctBy((it) -> it.getClass())
!duplicate.isDistinctBy((it) -> it.getClass()) !duplicate.isDistinctBy((it) -> it.getClass())
!altered.isDistinctBy((it) -> it.getClass())
!computedIndex.isDistinctBy((it) -> it.getClass())
} }
["getOrNull"] { ["getOrNull"] {
@@ -85,24 +103,32 @@ facts {
module.catch(() -> empty.first) == "Expected a non-empty Listing." module.catch(() -> empty.first) == "Expected a non-empty Listing."
base.first == base[0] base.first == base[0]
derived.first == base[0] derived.first == base[0]
altered.first != base[0]
computedIndex.first == altered.first
} }
["firstOrNull"] { ["firstOrNull"] {
empty.firstOrNull == null empty.firstOrNull == null
base.firstOrNull == base[0] base.firstOrNull == base[0]
derived.firstOrNull == base[0] derived.firstOrNull == base[0]
altered.firstOrNull != base[0]
computedIndex.firstOrNull == altered.first
} }
["last"] { ["last"] {
module.catch(() -> empty.last) == "Expected a non-empty Listing." module.catch(() -> empty.last) == "Expected a non-empty Listing."
base.last == base[2] base.last == base[2]
derived.last == derived[4] derived.last == derived[4]
altered.last == base[2]
computedIndex.last == base[2]
} }
["lastOrNull"] { ["lastOrNull"] {
empty.lastOrNull == null empty.lastOrNull == null
base.lastOrNull == base[2] base.lastOrNull == base[2]
derived.lastOrNull == derived[4] derived.lastOrNull == derived[4]
altered.lastOrNull == base[2]
computedIndex.lastOrNull == base[2]
} }
["single"] { ["single"] {
@@ -135,6 +161,7 @@ facts {
derived.contains(base[1]) derived.contains(base[1])
derived.contains(derived[3]) derived.contains(derived[3])
!altered.contains(base[0]) !altered.contains(base[0])
!computedIndex.contains(base[0])
} }
} }
@@ -144,6 +171,17 @@ examples {
empty2.length empty2.length
base.length base.length
derived.length derived.length
altered.length
computedIndex.length
local elementsAndEntries = (base) {
new { name = "" }
[2] { name = "" }
[computeIndex()] { name = "" }
new { name = "" }
[1 + 0] { name = "" }
}
elementsAndEntries.length
} }
["toList()"] { ["toList()"] {
@@ -152,6 +190,8 @@ examples {
base.toList() base.toList()
derived.toList() derived.toList()
duplicate.toList() duplicate.toList()
altered.toList()
computedIndex.toList()
} }
["toSet()"] { ["toSet()"] {
@@ -160,6 +200,8 @@ examples {
base.toSet() base.toSet()
derived.toSet() derived.toSet()
duplicate.toSet() duplicate.toSet()
altered.toSet()
computedIndex.toSet()
} }
["distinct"] { ["distinct"] {
@@ -168,6 +210,8 @@ examples {
base.distinct base.distinct
derived.distinct derived.distinct
duplicate.distinct duplicate.distinct
altered.distinct
computedIndex.distinct
} }
["distinctBy()"] { ["distinctBy()"] {
@@ -176,36 +220,48 @@ examples {
base.distinctBy((it) -> it) base.distinctBy((it) -> it)
derived.distinctBy((it) -> it) derived.distinctBy((it) -> it)
duplicate.distinctBy((it) -> it) duplicate.distinctBy((it) -> it)
altered.distinctBy((it) -> it)
computedIndex.distinctBy((it) -> it)
empty.distinctBy((it) -> it.name) empty.distinctBy((it) -> it.name)
empty2.distinctBy((it) -> it.name) empty2.distinctBy((it) -> it.name)
base.distinctBy((it) -> it.name) base.distinctBy((it) -> it.name)
derived.distinctBy((it) -> it.name) derived.distinctBy((it) -> it.name)
duplicate.distinctBy((it) -> it.name) duplicate.distinctBy((it) -> it.name)
altered.distinctBy((it) -> it.name)
computedIndex.distinctBy((it) -> it.name)
empty.distinctBy((it) -> it.getClass()) empty.distinctBy((it) -> it.getClass())
empty2.distinctBy((it) -> it.getClass()) empty2.distinctBy((it) -> it.getClass())
base.distinctBy((it) -> it.getClass()) base.distinctBy((it) -> it.getClass())
derived.distinctBy((it) -> it.getClass()) derived.distinctBy((it) -> it.getClass())
duplicate.distinctBy((it) -> it.getClass()) duplicate.distinctBy((it) -> it.getClass())
altered.distinctBy((it) -> it.getClass())
computedIndex.distinctBy((it) -> it.getClass())
} }
["fold"] { ["fold"] {
empty.fold(List(), (l, e) -> l.add(e)) empty.fold(List(), (l, e) -> l.add(e))
base.fold(List(), (l, e) -> l.add(e)) base.fold(List(), (l, e) -> l.add(e))
derived.fold(List(), (l, e) -> l.add(e)) derived.fold(List(), (l, e) -> l.add(e))
altered.fold(List(), (l, e) -> l.add(e))
computedIndex.fold(List(), (l, e) -> l.add(e))
} }
["foldIndexed"] { ["foldIndexed"] {
empty.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e))) empty.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e)))
base.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e))) base.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e)))
derived.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e))) derived.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e)))
altered.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e)))
computedIndex.foldIndexed(List(), (i, l, e) -> l.add(Pair(i, e)))
} }
local baseNum = new Listing { 1; 2; 3 } local baseNum = new Listing { 1; 2; 3 }
local baseString = new Listing { "Pigeon"; "Barn Owl"; "Parrot" } local baseString = new Listing { "Pigeon"; "Barn Owl"; "Parrot" }
local derivedString = (baseString) { "Albatross"; "Elf Owl" } local derivedString = (baseString) { "Albatross"; "Elf Owl" }
local alteredString = (baseString) { [0] = "Wood Pigeon" }
local computedIndexString = (baseString) { [computeIndex()] = "Wood Pigeon" }
["join"] { ["join"] {
empty.join("") empty.join("")
@@ -215,5 +271,9 @@ examples {
baseString.join("---") baseString.join("---")
derivedString.join("") derivedString.join("")
derivedString.join("\n") derivedString.join("\n")
alteredString.join("")
alteredString.join("\n")
computedIndexString.join("")
computedIndexString.join("\n")
} }
} }
@@ -0,0 +1,6 @@
local a = new Listing { new Listing { 0 } }
local b = a as Listing<Listing<String>>
local c = (b) { new Listing { 1 } }
local d = c as Listing<Listing<Int>>
result = d
@@ -0,0 +1,10 @@
local a = new Listing { new Foo {} }
local b = (a) { new Bar {} }
local c = b as Listing<Bar>
local d = (c) { new Foo {} }
local e = d as Listing<Foo>
result = e
open class Foo
class Bar extends Foo
@@ -0,0 +1,10 @@
local a = new Mapping { [0] = new Foo {} }
local b = (a) { [1] = new Bar {} }
local c = b as Mapping<Int, Bar>
local d = (c) { [2] = new Foo {} }
local e = d as Mapping<Int, Foo>
result = e
open class Foo
class Bar extends Foo
@@ -0,0 +1,7 @@
foo1: Listing<String> = new { "hello" }
foo2: Listing<String|Int> = foo1
res1 = foo1.isDistinct
// steals isDistinct from foo1's VmListing.cachedValues but must not
// perform a String|Int type check because isDistinct is not an element
res2 = foo2.isDistinct
@@ -0,0 +1,8 @@
amends "../../input-helper/listings/cacheStealingTypeCheck.pkl"
// this test covers a regression where the wrong receiver
// and owner was used to typecheck a stolen value
foo {
"abcdx"
"ax"
}
@@ -0,0 +1,5 @@
const local lastName = "Birdo"
typealias Birds = Listing<String(endsWith(lastName))>
typealias Birds2 = Pair<Listing<String(endsWith(lastName))>, Int>
@@ -1,5 +1,7 @@
import "pkl:test" import "pkl:test"
import "helpers/originalTypealias.pkl"
typealias Simple = String typealias Simple = String
const function simple(arg: Simple): Simple = arg const function simple(arg: Simple): Simple = arg
@@ -105,3 +107,8 @@ res19: LocalTypeAlias = "abc"
typealias VeryComposite = Pair<Composite, Composite> typealias VeryComposite = Pair<Composite, Composite>
res20: VeryComposite = Pair(Map("abc", List("def")), Map("abc", List("def"))) res20: VeryComposite = Pair(Map("abc", List("def")), Map("abc", List("def")))
// typealiases should be executed in their original context
res21: originalTypealias.Birds = new { "John Birdo" }
res22: originalTypealias.Birds2 = Pair(new Listing { "John Birdo" }, 0)
@@ -0,0 +1,3 @@
import "helpers/originalTypealias.pkl"
res: originalTypealias.Birds = new { "Jimmy Bird" }
@@ -4,6 +4,8 @@ facts {
true true
true true
true true
true
true
} }
["lastIndex"] { ["lastIndex"] {
true true
@@ -11,6 +13,8 @@ facts {
true true
true true
true true
true
true
} }
["isDistinct"] { ["isDistinct"] {
true true
@@ -18,6 +22,8 @@ facts {
true true
true true
true true
true
true
} }
["isDistinctBy()"] { ["isDistinctBy()"] {
true true
@@ -35,6 +41,12 @@ facts {
true true
true true
true true
true
true
true
true
true
true
} }
["getOrNull"] { ["getOrNull"] {
true true
@@ -49,21 +61,29 @@ facts {
true true
true true
true true
true
true
} }
["firstOrNull"] { ["firstOrNull"] {
true true
true true
true true
true
true
} }
["last"] { ["last"] {
true true
true true
true true
true
true
} }
["lastOrNull"] { ["lastOrNull"] {
true true
true true
true true
true
true
} }
["single"] { ["single"] {
true true
@@ -91,6 +111,7 @@ facts {
true true
true true
true true
true
} }
} }
examples { examples {
@@ -99,6 +120,9 @@ examples {
0 0
3 3
5 5
3
3
5
} }
["toList()"] { ["toList()"] {
List() List()
@@ -134,6 +158,20 @@ examples {
}, new { }, new {
name = "Elf Owl" name = "Elf Owl"
}) })
List(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
List(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
} }
["toSet()"] { ["toSet()"] {
Set() Set()
@@ -167,6 +205,20 @@ examples {
}, new { }, new {
name = "Elf Owl" name = "Elf Owl"
}) })
Set(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
Set(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
} }
["distinct"] { ["distinct"] {
new {} new {}
@@ -216,6 +268,28 @@ examples {
name = "Elf Owl" name = "Elf Owl"
} }
} }
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
} }
["distinctBy()"] { ["distinctBy()"] {
new {} new {}
@@ -265,6 +339,28 @@ examples {
name = "Elf Owl" name = "Elf Owl"
} }
} }
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
new {} new {}
new {} new {}
new { new {
@@ -312,6 +408,28 @@ examples {
name = "Elf Owl" name = "Elf Owl"
} }
} }
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
new {
new {
name = "Wood Pigeon"
}
new {
name = "Barn Owl"
}
new {
name = "Parrot"
}
}
new {} new {}
new {} new {}
new { new {
@@ -329,6 +447,16 @@ examples {
name = "Pigeon" name = "Pigeon"
} }
} }
new {
new {
name = "Wood Pigeon"
}
}
new {
new {
name = "Wood Pigeon"
}
}
} }
["fold"] { ["fold"] {
List() List()
@@ -350,6 +478,20 @@ examples {
}, new { }, new {
name = "Elf Owl" name = "Elf Owl"
}) })
List(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
List(new {
name = "Wood Pigeon"
}, new {
name = "Barn Owl"
}, new {
name = "Parrot"
})
} }
["foldIndexed"] { ["foldIndexed"] {
List() List()
@@ -371,6 +513,20 @@ examples {
}), Pair(4, new { }), Pair(4, new {
name = "Elf Owl" name = "Elf Owl"
})) }))
List(Pair(0, new {
name = "Wood Pigeon"
}), Pair(1, new {
name = "Barn Owl"
}), Pair(2, new {
name = "Parrot"
}))
List(Pair(0, new {
name = "Wood Pigeon"
}), Pair(1, new {
name = "Barn Owl"
}), Pair(2, new {
name = "Parrot"
}))
} }
["join"] { ["join"] {
"" ""
@@ -386,5 +542,17 @@ examples {
Albatross Albatross
Elf Owl Elf Owl
""" """
"Wood PigeonBarn OwlParrot"
"""
Wood Pigeon
Barn Owl
Parrot
"""
"Wood PigeonBarn OwlParrot"
"""
Wood Pigeon
Barn Owl
Parrot
"""
} }
} }
@@ -0,0 +1,15 @@
–– Pkl Error ––
Expected value of type `String`, but got type `Int`.
Value: 0
x | local b = a as Listing<Listing<String>>
^^^^^^
at listingTypeCheckError8#b (file:///$snippetsDir/input/errors/listingTypeCheckError8.pkl)
x | local a = new Listing { new Listing { 0 } }
^
at listingTypeCheckError8#a[#1][#1] (file:///$snippetsDir/input/errors/listingTypeCheckError8.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
@@ -0,0 +1,15 @@
–– Pkl Error ––
Expected value of type `listingTypeCheckError9#Bar`, but got type `listingTypeCheckError9#Foo`.
Value: new Foo {}
x | local c = b as Listing<Bar>
^^^
at listingTypeCheckError9#c (file:///$snippetsDir/input/errors/listingTypeCheckError9.pkl)
x | local a = new Listing { new Foo {} }
^^^^^^^^^^
at listingTypeCheckError9#a[#1] (file:///$snippetsDir/input/errors/listingTypeCheckError9.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
@@ -0,0 +1,15 @@
–– Pkl Error ––
Expected value of type `mappingTypeCheckError11#Bar`, but got type `mappingTypeCheckError11#Foo`.
Value: new Foo {}
x | local c = b as Mapping<Int, Bar>
^^^
at mappingTypeCheckError11#c (file:///$snippetsDir/input/errors/mappingTypeCheckError11.pkl)
x | local a = new Mapping { [0] = new Foo {} }
^^^^^^^^^^
at mappingTypeCheckError11#a[0] (file:///$snippetsDir/input/errors/mappingTypeCheckError11.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
@@ -0,0 +1,8 @@
foo1 {
"hello"
}
foo2 {
"hello"
}
res1 = true
res2 = true
@@ -0,0 +1,4 @@
foo {
"abcdx"
"ax"
}
@@ -38,3 +38,9 @@ res18 = "Expected value of type `Duration`, but got type `DataSize`. Value: 5.mb
res18b = "Expected value of type `Duration`, but got type `DataSize`. Value: 5.mb" res18b = "Expected value of type `Duration`, but got type `DataSize`. Value: 5.mb"
res19 = "abc" res19 = "abc"
res20 = Pair(Map("abc", List("def")), Map("abc", List("def"))) res20 = Pair(Map("abc", List("def")), Map("abc", List("def")))
res21 {
"John Birdo"
}
res22 = Pair(new {
"John Birdo"
}, 0)
@@ -0,0 +1,15 @@
–– Pkl Error ––
Type constraint `endsWith(lastName)` violated.
Value: "Jimmy Bird"
x | typealias Birds = Listing<String(endsWith(lastName))>
^^^^^^^^^^^^^^^^^^
at typeAliasContext#res (file:///$snippetsDir/input/types/helpers/originalTypealias.pkl)
x | res: originalTypealias.Birds = new { "Jimmy Bird" }
^^^^^^^^^^^^
at typeAliasContext#res[#1] (file:///$snippetsDir/input/types/typeAliasContext.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
@@ -117,7 +117,7 @@ class DocGenerator(
} }
private fun DocPackage.deletePackageDir() { private fun DocPackage.deletePackageDir() {
outputDir.resolve("$name/$version").deleteRecursively() outputDir.resolve(IoUtils.encodePath("$name/$version")).deleteRecursively()
} }
private fun createSymlinks(currentPackagesData: List<PackageData>) { private fun createSymlinks(currentPackagesData: List<PackageData>) {
@@ -18,6 +18,7 @@ package org.pkl.gradle;
import java.io.File; import java.io.File;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -27,6 +28,7 @@ import org.gradle.api.GradleException;
import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Transformer;
import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.plugins.Convention; import org.gradle.api.plugins.Convention;
import org.gradle.api.provider.Provider; import org.gradle.api.provider.Provider;
@@ -64,6 +66,7 @@ import org.pkl.gradle.task.PkldocTask;
import org.pkl.gradle.task.ProjectPackageTask; import org.pkl.gradle.task.ProjectPackageTask;
import org.pkl.gradle.task.ProjectResolveTask; import org.pkl.gradle.task.ProjectResolveTask;
import org.pkl.gradle.task.TestTask; import org.pkl.gradle.task.TestTask;
import org.pkl.gradle.utils.PluginUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PklPlugin implements Plugin<Project> { public class PklPlugin implements Plugin<Project> {
@@ -456,6 +459,9 @@ public class PklPlugin implements Plugin<Project> {
private List<File> getTransitiveModules(AnalyzeImportsTask analyzeTask) { private List<File> getTransitiveModules(AnalyzeImportsTask analyzeTask) {
var outputFile = analyzeTask.getOutputFile().get().getAsFile().toPath(); var outputFile = analyzeTask.getOutputFile().get().getAsFile().toPath();
if (!analyzeTask.getOnlyIf().isSatisfiedBy(analyzeTask)) {
return Collections.emptyList();
}
try { try {
var contents = Files.readString(outputFile); var contents = Files.readString(outputFile);
ImportGraph importGraph = ImportGraph.parseFromJson(contents); ImportGraph importGraph = ImportGraph.parseFromJson(contents);
@@ -470,9 +476,16 @@ public class PklPlugin implements Plugin<Project> {
} }
private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask( private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask(
T task, S spec, @Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask) { T task,
S spec,
@Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask,
@Nullable Transformer<List<?>, List<?>> mapSourceModules) {
configureBaseTask(task, spec); configureBaseTask(task, spec);
if (mapSourceModules != null) {
task.getSourceModules().set(spec.getSourceModules().map(mapSourceModules));
} else {
task.getSourceModules().set(spec.getSourceModules()); task.getSourceModules().set(spec.getSourceModules());
}
task.getNoProject().set(spec.getNoProject()); task.getNoProject().set(spec.getNoProject());
task.getProjectDir().set(spec.getProjectDir()); task.getProjectDir().set(spec.getProjectDir());
task.getOmitProjectSettings().set(spec.getOmitProjectSettings()); task.getOmitProjectSettings().set(spec.getOmitProjectSettings());
@@ -484,6 +497,11 @@ public class PklPlugin implements Plugin<Project> {
} }
} }
private <T extends ModulesTask, S extends ModulesSpec> void configureModulesTask(
T task, S spec, @Nullable TaskProvider<AnalyzeImportsTask> analyzeImportsTask) {
configureModulesTask(task, spec, analyzeImportsTask, null);
}
private TaskProvider<AnalyzeImportsTask> createAnalyzeImportsTask(ModulesSpec spec) { private TaskProvider<AnalyzeImportsTask> createAnalyzeImportsTask(ModulesSpec spec) {
var outputFile = var outputFile =
project project
@@ -496,11 +514,26 @@ public class PklPlugin implements Plugin<Project> {
spec.getName() + "GatherImports", spec.getName() + "GatherImports",
AnalyzeImportsTask.class, AnalyzeImportsTask.class,
task -> { task -> {
configureModulesTask(task, spec, null); configureModulesTask(
task,
spec,
null,
(modules) ->
// only need to analyze imports of file-based modules; it's unlikely that a
// non-file-based module will import a file-based module due to security
// manager trust levels (see
// org.pkl.core.SecurityManagers.getDefaultTrustLevel).
modules.stream()
.map(PluginUtils::parseModuleNotationToUri)
.filter(
(it) ->
it.getScheme() == null || it.getScheme().equalsIgnoreCase("file"))
.toList());
task.setDescription("Compute the set of imports declared by input modules"); task.setDescription("Compute the set of imports declared by input modules");
task.setGroup("build"); task.setGroup("build");
task.getOutputFormat().set(OutputFormat.JSON.toString()); task.getOutputFormat().set(OutputFormat.JSON.toString());
task.getOutputFile().set(outputFile); task.getOutputFile().set(outputFile);
task.onlyIf(ignored -> !task.getSourceModules().get().isEmpty());
}); });
} }
@@ -17,9 +17,6 @@ package org.pkl.gradle.task;
import java.io.File; import java.io.File;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Duration; import java.time.Duration;
@@ -31,10 +28,8 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import org.gradle.api.DefaultTask; import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.model.ObjectFactory; import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.MapProperty;
@@ -49,9 +44,9 @@ import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions; import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color; import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit; import org.pkl.core.util.LateInit;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
import org.pkl.gradle.utils.PluginUtils;
public abstract class BasePklTask extends DefaultTask { public abstract class BasePklTask extends DefaultTask {
@Input @Input
@@ -74,7 +69,7 @@ public abstract class BasePklTask extends DefaultTask {
@Internal @Internal
public Provider<Object> getParsedSettingsModule() { public Provider<Object> getParsedSettingsModule() {
return getSettingsModule().map(this::parseModuleNotation); return getSettingsModule().map(PluginUtils::parseModuleNotation);
} }
@InputFile @InputFile
@@ -165,7 +160,7 @@ public abstract class BasePklTask extends DefaultTask {
parseModulePath(), parseModulePath(),
getProject().getProjectDir().toPath(), getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get), mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), this::parseModuleNotationToUri), mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
null, null,
getEvalTimeout().getOrNull(), getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()), mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
@@ -199,100 +194,6 @@ public abstract class BasePklTask extends DefaultTask {
return getModulePath().getFiles().stream().map(File::toPath).collect(Collectors.toList()); return getModulePath().getFiles().stream().map(File::toPath).collect(Collectors.toList());
} }
/**
* Parses the specified source module notation into a "parsed" notation which is then used for
* input path tracking and as an argument for the CLI API.
*
* <p>This method accepts the following input types:
*
* <ul>
* <li>{@link URI} - used as is.
* <li>{@link File} - used as is.
* <li>{@link Path} - converted to a {@link File}. This conversion may fail because not all
* {@link Path}s point to the local file system.
* <li>{@link URL} - converted to a {@link URI}. This conversion may fail because {@link URL}
* allows for URLs which are not compliant URIs.
* <li>{@link CharSequence} - first, converted to a string. If this string is "URI-like" (see
* {@link IoUtils#isUriLike(String)}), then we attempt to parse it as a {@link URI}, which
* may fail. Otherwise, we attempt to parse it as a {@link Path}, which is then converted to
* a {@link File} (both of these operations may fail).
* <li>{@link FileSystemLocation} - converted to a {@link File} via the {@link
* FileSystemLocation#getAsFile()} method.
* </ul>
*
* In case the returned value is determined to be a {@link URI}, then this URI is first checked
* for whether its scheme is {@code file}, like {@code file:///example/path}. In such case, this
* method returns a {@link File} corresponding to the file path in the URI. Otherwise, a {@link
* URI} instance is returned.
*
* @throws InvalidUserDataException In case the input is none of the types described above, or
* when the underlying value cannot be parsed correctly.
*/
protected Object parseModuleNotation(Object notation) {
if (notation instanceof URI uri) {
if ("file".equals(uri.getScheme())) {
return new File(uri.getPath());
}
return uri;
} else if (notation instanceof File) {
return notation;
} else if (notation instanceof Path path) {
try {
return path.toFile();
} catch (UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + notation, e);
}
} else if (notation instanceof URL url) {
try {
return parseModuleNotation(url.toURI());
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + notation, e);
}
} else if (notation instanceof CharSequence) {
var s = notation.toString();
if (IoUtils.isUriLike(s)) {
try {
return parseModuleNotation(IoUtils.toUri(s));
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + s, e);
}
} else {
try {
return Paths.get(s).toFile();
} catch (InvalidPathException | UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + s, e);
}
}
} else if (notation instanceof FileSystemLocation location) {
return location.getAsFile();
} else {
throw new InvalidUserDataException(
"Unsupported value of type "
+ notation.getClass()
+ " used as a module path: "
+ notation);
}
}
protected URI parseModuleNotationToUri(Object m) {
var parsed1 = parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
/**
* Converts either a file or a URI to a URI. We convert a file to a URI via the {@link
* IoUtils#createUri(String)} because other ways of conversion can make relative paths into
* absolute URIs, which may break module loading.
*/
private URI parsedModuleNotationToUri(Object notation) {
if (notation instanceof File file) {
return IoUtils.createUri(file.getPath());
} else if (notation instanceof URI uri) {
return uri;
}
throw new IllegalArgumentException("Invalid parsed module notation: " + notation);
}
protected List<Pattern> patternsFromStrings(List<String> patterns) { protected List<Pattern> patternsFromStrings(List<String> patterns) {
return patterns.stream().map(Pattern::compile).collect(Collectors.toList()); return patterns.stream().map(Pattern::compile).collect(Collectors.toList());
} }
@@ -37,8 +37,8 @@ import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions; import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color; import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Pair; import org.pkl.core.util.Pair;
import org.pkl.gradle.utils.PluginUtils;
public abstract class ModulesTask extends BasePklTask { public abstract class ModulesTask extends BasePklTask {
// We expose the contents of this property as task inputs via the sourceModuleFiles // We expose the contents of this property as task inputs via the sourceModuleFiles
@@ -84,7 +84,7 @@ public abstract class ModulesTask extends BasePklTask {
@Override @Override
protected List<URI> getSourceModulesAsUris() { protected List<URI> getSourceModulesAsUris() {
return getSourceModules().get().stream() return getSourceModules().get().stream()
.map(this::parseModuleNotationToUri) .map(PluginUtils::parseModuleNotationToUri)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@@ -117,7 +117,7 @@ public abstract class ModulesTask extends BasePklTask {
var files = new ArrayList<File>(); var files = new ArrayList<File>();
var uris = new ArrayList<URI>(); var uris = new ArrayList<URI>();
for (var m : modules) { for (var m : modules) {
var parsed = parseModuleNotation(m); var parsed = PluginUtils.parseModuleNotation(m);
if (parsed instanceof File file) { if (parsed instanceof File file) {
files.add(file); files.add(file);
} else if (parsed instanceof URI uri) { } else if (parsed instanceof URI uri) {
@@ -127,28 +127,6 @@ public abstract class ModulesTask extends BasePklTask {
return Pair.of(files, uris); return Pair.of(files, uris);
} }
/**
* Converts either a file or a URI to a URI. We convert a relative file to a URI via the {@link
* IoUtils#createUri(String)} because other ways of conversion can make relative paths into
* absolute URIs, which may break module loading.
*/
private URI parsedModuleNotationToUri(Object notation) {
if (notation instanceof File file) {
if (file.isAbsolute()) {
return file.toPath().toUri();
}
return IoUtils.createUri(IoUtils.toNormalizedPathString(file.toPath()));
} else if (notation instanceof URI uri) {
return uri;
}
throw new IllegalArgumentException("Invalid parsed module notation: " + notation);
}
protected URI parseModuleNotationToUri(Object m) {
var parsed1 = parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
@TaskAction @TaskAction
@Override @Override
public void runTask() { public void runTask() {
@@ -172,7 +150,7 @@ public abstract class ModulesTask extends BasePklTask {
parseModulePath(), parseModulePath(),
getProject().getProjectDir().toPath(), getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get), mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), this::parseModuleNotationToUri), mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null, getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(), getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()), mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
@@ -0,0 +1,128 @@
/*
* 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.gradle.utils;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.FileSystemLocation;
import org.pkl.core.util.IoUtils;
public class PluginUtils {
private PluginUtils() {}
/**
* Parses the specified source module notation into a "parsed" notation which is then used for
* input path tracking and as an argument for the CLI API.
*
* <p>This method accepts the following input types:
*
* <ul>
* <li>{@link URI} - used as is.
* <li>{@link File} - used as is.
* <li>{@link Path} - converted to a {@link File}. This conversion may fail because not all
* {@link Path}s point to the local file system.
* <li>{@link URL} - converted to a {@link URI}. This conversion may fail because {@link URL}
* allows for URLs which are not compliant URIs.
* <li>{@link CharSequence} - first, converted to a string. If this string is "URI-like" (see
* {@link IoUtils#isUriLike(String)}), then we attempt to parse it as a {@link URI}, which
* may fail. Otherwise, we attempt to parse it as a {@link Path}, which is then converted to
* a {@link File} (both of these operations may fail).
* <li>{@link FileSystemLocation} - converted to a {@link File} via the {@link
* FileSystemLocation#getAsFile()} method.
* </ul>
*
* In case the returned value is determined to be a {@link URI}, then this URI is first checked
* for whether its scheme is {@code file}, like {@code file:///example/path}. In such case, this
* method returns a {@link File} corresponding to the file path in the URI. Otherwise, a {@link
* URI} instance is returned.
*
* @throws InvalidUserDataException In case the input is none of the types described above, or
* when the underlying value cannot be parsed correctly.
*/
public static Object parseModuleNotation(Object notation) {
if (notation instanceof URI uri) {
if ("file".equals(uri.getScheme())) {
return new File(uri.getPath());
}
return uri;
} else if (notation instanceof File) {
return notation;
} else if (notation instanceof Path path) {
try {
return path.toFile();
} catch (UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + notation, e);
}
} else if (notation instanceof URL url) {
try {
return parseModuleNotation(url.toURI());
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + notation, e);
}
} else if (notation instanceof CharSequence) {
var s = notation.toString();
if (IoUtils.isUriLike(s)) {
try {
return parseModuleNotation(IoUtils.toUri(s));
} catch (URISyntaxException e) {
throw new InvalidUserDataException("Failed to parse Pkl module URI: " + s, e);
}
} else {
try {
return Paths.get(s).toFile();
} catch (InvalidPathException | UnsupportedOperationException e) {
throw new InvalidUserDataException("Failed to parse Pkl module file path: " + s, e);
}
}
} else if (notation instanceof FileSystemLocation location) {
return location.getAsFile();
} else {
throw new InvalidUserDataException(
"Unsupported value of type "
+ notation.getClass()
+ " used as a module path: "
+ notation);
}
}
/**
* Converts either a file or a URI to a URI. We convert a relative file to a URI via the {@link
* IoUtils#createUri(String)} because other ways of conversion can make relative paths into
* absolute URIs, which may break module loading.
*/
public static URI parsedModuleNotationToUri(Object notation) {
if (notation instanceof File file) {
if (file.isAbsolute()) {
return file.toPath().toUri();
}
return IoUtils.createUri(IoUtils.toNormalizedPathString(file.toPath()));
} else if (notation instanceof URI uri) {
return uri;
}
throw new IllegalArgumentException("Invalid parsed module notation: " + notation);
}
public static URI parseModuleNotationToUri(Object m) {
var parsed1 = PluginUtils.parseModuleNotation(m);
return parsedModuleNotationToUri(parsed1);
}
}
@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.gradle.utils;
import org.pkl.core.util.NonnullByDefault;
@@ -15,13 +15,17 @@
*/ */
package org.pkl.gradle package org.pkl.gradle
import java.nio.file.Path
import kotlin.io.path.readText import kotlin.io.path.readText
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.PackageServer
class PkldocGeneratorsTest : AbstractTest() { class PkldocGeneratorsTest : AbstractTest() {
@Test @Test
fun `generate docs`() { fun `generate docs`(@TempDir tempDir: Path) {
PackageServer.populateCacheDir(tempDir)
writeFile( writeFile(
"build.gradle", "build.gradle",
""" """
@@ -32,7 +36,8 @@ class PkldocGeneratorsTest : AbstractTest() {
pkl { pkl {
pkldocGenerators { pkldocGenerators {
pkldoc { pkldoc {
sourceModules = ["person.pkl", "doc-package-info.pkl"] moduleCacheDir = file("${tempDir.toUri()}")
sourceModules = ["package://localhost:0/birds@0.5.0", "person.pkl", "doc-package-info.pkl"]
outputDir = file("build/pkldoc") outputDir = file("build/pkldoc")
settingsModule = "pkl:settings" settingsModule = "pkl:settings"
} }
@@ -94,6 +99,39 @@ class PkldocGeneratorsTest : AbstractTest() {
checkTextContains(moduleFile.readText(), "<html>", "Person", "Address", "other") checkTextContains(moduleFile.readText(), "<html>", "Person", "Address", "other")
checkTextContains(personFile.readText(), "<html>", "name", "addresses") checkTextContains(personFile.readText(), "<html>", "name", "addresses")
checkTextContains(addressFile.readText(), "<html>", "street", "zip") checkTextContains(addressFile.readText(), "<html>", "street", "zip")
val birdsPackageFile = baseDir.resolve("localhost(3a)0/birds/0.5.0/index.html")
assertThat(birdsPackageFile).exists()
}
@Test
fun `generate docs only for package`(@TempDir tempDir: Path) {
PackageServer.populateCacheDir(tempDir)
writeFile(
"build.gradle",
"""
plugins {
id "org.pkl-lang"
}
pkl {
pkldocGenerators {
pkldoc {
moduleCacheDir = file("${tempDir.toUri()}")
sourceModules = ["package://localhost:0/birds@0.5.0"]
outputDir = file("build/pkldoc")
settingsModule = "pkl:settings"
}
}
}
"""
)
runTask("pkldoc")
val baseDir = testProjectDir.resolve("build/pkldoc")
val birdsPackageFile = baseDir.resolve("localhost(3a)0/birds/0.5.0/index.html")
assertThat(birdsPackageFile).exists()
} }
@Test @Test