9 Commits
main ... 0.27.1

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
Dan Chao
d3ac4b288c Prepare 0.27.0 release 2024-11-05 09:51:48 -08:00
53 changed files with 1035 additions and 515 deletions

View File

@@ -1,6 +1,6 @@
name: main
title: Main Project
version: 0.27.0-dev
prerelease: true
version: 0.27.1
prerelease: false
nav:
- nav.adoc

View File

@@ -3,10 +3,10 @@
// 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-no-suffix: 0.27.0
:pkl-version-no-suffix: 0.27.1
// tells whether pkl version corresponding to current git commit
// is a release version (:is-release-version: '') or dev version (:!is-release-version:)
:!is-release-version:
:is-release-version: ''
// the remaining attributes do not need to be updated regularly

View File

@@ -1,6 +1,6 @@
= Pkl 0.27 Release Notes
:version: 0.27
:version-minor: 0.27.0
:version-minor: 0.27.1
:release-date: November 5th, 2024
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]).
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]
----

View File

@@ -1,6 +1,28 @@
= Changelog
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]]
== 0.27.0 (2024-11-05)

View File

@@ -1,7 +1,7 @@
# suppress inspection "UnusedProperty" for whole file
group=org.pkl-lang
version=0.27.0
version=0.27.1
# google-java-format requires jdk.compiler exports
org.gradle.jvmargs= \

View File

@@ -2,17 +2,17 @@
"catalogs": {},
"aliases": {
"pkl": {
"script-ref": "org.pkl-lang:pkl-cli-java:0.26.3",
"script-ref": "org.pkl-lang:pkl-cli-java:0.27.1",
"java-agents": []
},
"pkl-codegen-java": {
"script-ref": "org.pkl-lang:pkl-codegen-java:0.26.3",
"script-ref": "org.pkl-lang:pkl-codegen-java:0.27.1",
"java-agents": []
},
"pkl-codegen-kotlin": {
"script-ref": "org.pkl-lang:pkl-codegen-kotlin:0.26.3",
"script-ref": "org.pkl-lang:pkl-codegen-kotlin:0.27.1",
"java-agents": []
}
},
"templates": {}
}
}

View File

@@ -19,6 +19,7 @@ import java.nio.file.Path
import java.util.stream.Collectors
import kotlin.io.path.*
import org.assertj.core.api.Assertions.fail
import org.opentest4j.AssertionFailedError
import org.pkl.commons.*
object FileTestUtils {
@@ -110,5 +111,11 @@ data class SnippetOutcome(val expectedOutFile: Path, val actual: String, val suc
}
private fun failWithDiff(message: String): Nothing =
throw PklAssertionFailedError(message, expected, actual)
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)
}
}

View File

@@ -1221,7 +1221,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
member.initConstantValue(constantNode);
} else {
member.initMemberNode(
new UntypedObjectMemberNode(
ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, elementNode));
}
@@ -1278,7 +1278,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
member.initConstantValue(constantNode);
} else {
member.initMemberNode(
new UntypedObjectMemberNode(
ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, valueNode));
}
} else { // ["key"] { ... }
@@ -1287,7 +1287,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
objectBodyCtxs,
new ReadSuperEntryNode(unavailableSourceSection(), new GetMemberKeyNode()));
member.initMemberNode(
new UntypedObjectMemberNode(
ElementOrEntryNodeGen.create(
language, scope.buildFrameDescriptor(), member, objectBody));
}
@@ -2446,6 +2446,7 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
return new UnresolvedTypeNode.Parameterized(
createSourceSection(ctx),
language,
doVisitTypeName(idCtx),
argCtx.ts.stream().map(this::visitType).toArray(UnresolvedTypeNode[]::new));
}

View File

@@ -103,7 +103,7 @@ public abstract class GeneratorPredicateMemberNode extends GeneratorMemberNode {
var callTarget = member.getCallTarget();
value = callTarget.call(parent, owner, key);
}
owner.setCachedValue(key, value, member);
owner.setCachedValue(key, value);
}
frame.setAuxiliarySlot(customThisSlot, value);

View File

@@ -43,15 +43,15 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
@Children private final ExpressionNode[] keyNodes;
private final ObjectMember[] values;
public EntriesLiteralNode(
protected EntriesLiteralNode(
SourceSection sourceSection,
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,
boolean isCustomThisScope,
@Nullable FrameDescriptor parametersDescriptor,
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,
ExpressionNode[] keyNodes,
ObjectMember[] values) {
@@ -103,7 +103,8 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
frame.materialize(),
parent,
createListMembers(frame, parent.getLength()),
parent.getLength() + keyNodes.length);
// `[x] = y` overrides existing element and doesn't increase length
parent.getLength());
}
@Specialization

View File

@@ -71,7 +71,7 @@ public final class ReadLocalPropertyNode extends ExpressionNode {
if (result == null) {
result = callNode.call(objReceiver, owner, property.getName());
objReceiver.setCachedValue(property, result, property);
objReceiver.setCachedValue(property, result);
}
return result;

View File

@@ -184,12 +184,12 @@ public final class ResolveVariableNode extends ExpressionNode {
if (member != null) {
var constantValue = member.getConstantValue();
if (constantValue != null) {
baseModule.setCachedValue(variableName, constantValue, member);
baseModule.setCachedValue(variableName, constantValue);
return new ConstantValueNode(sourceSection, constantValue);
}
var computedValue = member.getCallTarget().call(baseModule, baseModule);
baseModule.setCachedValue(variableName, computedValue, member);
baseModule.setCachedValue(variableName, computedValue);
return new ConstantValueNode(sourceSection, computedValue);
}
}

View File

@@ -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);
}
}

View File

@@ -26,7 +26,7 @@ import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.util.Nullable;
/** 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;
private final String qualifiedName;

View File

@@ -73,7 +73,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(importName);
if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, importName);
module.setCachedValue(importName, result, member);
module.setCachedValue(importName, result);
}
return (VmTyped) result;
}
@@ -94,7 +94,7 @@ public abstract class ResolveDeclaredTypeNode extends ExpressionNode {
var result = module.getCachedValue(typeName);
if (result == null) {
result = callNode.call(member.getCallTarget(), module, module, typeName);
module.setCachedValue(typeName, result, member);
module.setCachedValue(typeName, result);
}
return result;
}

View File

@@ -21,6 +21,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback;
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.FrameSlotKind;
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. */
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() {
return null;
@@ -304,7 +309,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UnknownTypeNode;
}
@@ -371,7 +376,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NothingTypeNode;
}
@@ -408,7 +413,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FinalModuleTypeNode finalModuleTypeNode)) {
return false;
}
@@ -460,7 +465,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NonFinalModuleTypeNode nonFinalModuleTypeNode)) {
return false;
}
@@ -496,7 +501,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof StringLiteralTypeNode stringLiteralTypeNode)) {
return false;
}
@@ -551,7 +556,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof TypedTypeNode;
}
@@ -586,7 +591,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof DynamicTypeNode;
}
@@ -636,7 +641,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FinalClassTypeNode finalClassTypeNode)) {
return false;
}
@@ -705,7 +710,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NonFinalClassTypeNode nonFinalClassTypeNode)) {
return false;
}
@@ -772,7 +777,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof NullableTypeNode nullableTypeNode)) {
return false;
}
@@ -842,7 +847,7 @@ public abstract class TypeNode extends PklNode {
@Override
@ExplodeLoop
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof UnionTypeNode unionTypeNode)) {
return false;
}
@@ -1016,7 +1021,7 @@ public abstract class TypeNode extends PklNode {
@Override
@TruffleBoundary
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof UnionOfStringLiteralsTypeNode unionOfStringLiteralsTypeNode)) {
return false;
}
@@ -1092,7 +1097,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return false;
}
@@ -1223,7 +1228,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof ListTypeNode listTypeNode)) {
return false;
}
@@ -1268,7 +1273,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof SetTypeNode setTypeNode)) {
return false;
}
@@ -1360,7 +1365,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof MapTypeNode mapTypeNode)) {
return false;
}
@@ -1414,8 +1419,9 @@ public abstract class TypeNode extends PklNode {
}
public static final class ListingTypeNode extends ListingOrMappingTypeNode {
public ListingTypeNode(SourceSection sourceSection, TypeNode valueTypeNode) {
super(sourceSection, null, valueTypeNode);
public ListingTypeNode(
SourceSection sourceSection, VmLanguage language, TypeNode valueTypeNode) {
super(sourceSection, language, null, valueTypeNode);
}
@Override
@@ -1423,7 +1429,17 @@ public abstract class TypeNode extends PklNode {
if (!(value instanceof VmListing vmListing)) {
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
@@ -1451,7 +1467,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof ListingTypeNode listingTypeNode)) {
return false;
}
@@ -1466,9 +1482,12 @@ public abstract class TypeNode extends PklNode {
public static final class MappingTypeNode extends ListingOrMappingTypeNode {
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
@@ -1478,7 +1497,16 @@ public abstract class TypeNode extends PklNode {
}
// execute type checks on mapping keys
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
@@ -1509,7 +1537,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof MappingTypeNode mappingTypeNode)) {
return false;
}
@@ -1526,17 +1554,22 @@ public abstract class TypeNode extends PklNode {
}
public abstract static class ListingOrMappingTypeNode extends ObjectSlotTypeNode {
private final VmLanguage language;
@Child protected @Nullable TypeNode keyTypeNode;
@Child protected TypeNode valueTypeNode;
@Child @Nullable protected ListingOrMappingTypeCastNode listingOrMappingTypeCastNode;
@Child @Nullable protected ListingOrMappingTypeCastNode valueTypeCastNode;
private final boolean skipKeyTypeChecks;
private final boolean skipValueTypeChecks;
protected ListingOrMappingTypeNode(
SourceSection sourceSection, @Nullable TypeNode keyTypeNode, TypeNode valueTypeNode) {
SourceSection sourceSection,
VmLanguage language,
@Nullable TypeNode keyTypeNode,
TypeNode valueTypeNode) {
super(sourceSection);
this.language = language;
this.keyTypeNode = keyTypeNode;
this.valueTypeNode = valueTypeNode;
@@ -1556,17 +1589,14 @@ public abstract class TypeNode extends PklNode {
return valueTypeNode;
}
protected ListingOrMappingTypeCastNode getListingOrMappingTypeCastNode() {
if (listingOrMappingTypeCastNode == null) {
protected ListingOrMappingTypeCastNode getValueTypeCastNode() {
if (valueTypeCastNode == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
listingOrMappingTypeCastNode =
valueTypeCastNode =
new ListingOrMappingTypeCastNode(
VmLanguage.get(this),
getRootNode().getFrameDescriptor(),
valueTypeNode,
getRootNode().getName());
language, new FrameDescriptor(), valueTypeNode, getRootNode().getName());
}
return listingOrMappingTypeCastNode;
return valueTypeCastNode;
}
// either (if defaultMemberValue != null):
@@ -1647,15 +1677,6 @@ public abstract class TypeNode extends PklNode {
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) {
doEagerCheck(frame, object, skipKeyTypeChecks, skipValueTypeChecks);
}
@@ -1700,7 +1721,7 @@ public abstract class TypeNode extends PklNode {
var callTarget = member.getCallTarget();
memberValue = callTarget.call(object, owner, memberKey);
}
object.setCachedValue(memberKey, memberValue, member);
object.setCachedValue(memberKey, memberValue);
}
valueTypeNode.executeEagerly(frame, memberValue);
}
@@ -1748,7 +1769,7 @@ public abstract class TypeNode extends PklNode {
@Override
@ExplodeLoop
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionTypeNode functionTypeNode)) {
return false;
}
@@ -1845,7 +1866,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionClassTypeNode functionClassTypeNode)) {
return false;
}
@@ -1883,7 +1904,7 @@ public abstract class TypeNode extends PklNode {
@Override
@ExplodeLoop
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof FunctionNClassTypeNode functionNClassTypeNode)) {
return false;
}
@@ -1986,7 +2007,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof PairTypeNode pairTypeNode)) {
return false;
}
@@ -2043,7 +2064,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if (!(other instanceof VarArgsTypeNode varArgsTypeNode)) {
return false;
}
@@ -2095,7 +2116,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof TypeVariableNode;
}
@@ -2135,7 +2156,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NonNullTypeAliasTypeNode;
}
@@ -2184,7 +2205,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof UIntTypeAliasTypeNode aliasTypeNode && mask == aliasTypeNode.mask;
}
@@ -2228,7 +2249,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int8TypeAliasTypeNode;
}
@@ -2272,7 +2293,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int16TypeAliasTypeNode;
}
@@ -2316,7 +2337,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof Int32TypeAliasTypeNode;
}
@@ -2387,14 +2408,14 @@ public abstract class TypeNode extends PklNode {
public Object execute(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.execute(frame, value);
} finally {
VmUtils.setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver);
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
}
@@ -2403,14 +2424,14 @@ public abstract class TypeNode extends PklNode {
public Object executeAndSet(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.executeAndSet(frame, value);
} finally {
VmUtils.setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver);
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
}
@@ -2419,14 +2440,14 @@ public abstract class TypeNode extends PklNode {
public Object executeEagerlyAndSet(VirtualFrame frame, Object value) {
var prevOwner = VmUtils.getOwner(frame);
var prevReceiver = VmUtils.getReceiver(frame);
VmUtils.setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
VmUtils.setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
setOwner(frame, VmUtils.getOwner(typeAlias.getEnclosingFrame()));
setReceiver(frame, VmUtils.getReceiver(typeAlias.getEnclosingFrame()));
try {
return aliasedTypeNode.executeEagerlyAndSet(frame, value);
} finally {
VmUtils.setOwner(frame, prevOwner);
VmUtils.setReceiver(frame, prevReceiver);
setOwner(frame, prevOwner);
setReceiver(frame, prevReceiver);
}
}
@@ -2471,7 +2492,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
if ((other instanceof TypeAliasTypeNode typeAliasTypeNode)) {
return aliasedTypeNode.isEquivalentTo(typeAliasTypeNode.aliasedTypeNode);
}
@@ -2498,6 +2519,22 @@ public abstract class TypeNode extends PklNode {
protected boolean isParametric() {
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 {
@@ -2581,7 +2618,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
// consider constrained types as always different
return false;
}
@@ -2622,7 +2659,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof AnyTypeNode;
}
@@ -2650,7 +2687,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof StringTypeNode;
}
@@ -2714,7 +2751,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof NumberTypeNode;
}
@@ -2742,7 +2779,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof IntTypeNode;
}
@@ -2787,7 +2824,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof FloatTypeNode;
}
@@ -2832,7 +2869,7 @@ public abstract class TypeNode extends PklNode {
}
@Override
public boolean isEquivalentTo(TypeNode other) {
public boolean doIsEquivalentTo(TypeNode other) {
return other instanceof BooleanTypeNode;
}

View File

@@ -197,14 +197,17 @@ public abstract class UnresolvedTypeNode extends PklNode {
}
public static final class Parameterized extends UnresolvedTypeNode {
private final VmLanguage language;
@Child private ExpressionNode resolveTypeNode;
@Children private final UnresolvedTypeNode[] typeArgumentNodes;
public Parameterized(
SourceSection sourceSection,
VmLanguage language,
ExpressionNode resolveTypeNode,
UnresolvedTypeNode[] typeArgumentNodes) {
super(sourceSection);
this.language = language;
this.resolveTypeNode = resolveTypeNode;
this.typeArgumentNodes = typeArgumentNodes;
}
@@ -238,12 +241,13 @@ public abstract class UnresolvedTypeNode extends PklNode {
}
if (clazz.isListingClass()) {
return new ListingTypeNode(sourceSection, typeArgumentNodes[0].execute(frame));
return new ListingTypeNode(sourceSection, language, typeArgumentNodes[0].execute(frame));
}
if (clazz.isMappingClass()) {
return new MappingTypeNode(
sourceSection,
language,
typeArgumentNodes[0].execute(frame),
typeArgumentNodes[1].execute(frame));
}

View File

@@ -17,17 +17,16 @@ package org.pkl.core.externalreader;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.concurrent.GuardedBy;
import org.pkl.core.Duration;
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader;
import org.pkl.core.externalreader.ExternalReaderMessages.*;
import org.pkl.core.messaging.MessageTransport;
@@ -152,40 +151,23 @@ final class ExternalReaderProcessImpl implements ExternalReaderProcess {
@Override
public void close() {
synchronized (lock) {
if (closed) return;
closed = true;
if (process == null || !process.isAlive()) {
return;
}
try {
if (transport != null) {
if (transport != null && process != null && process.isAlive()) {
transport.send(new CloseExternalProcess());
process.waitFor(CLOSE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
}
} catch (Exception ignored) {
} finally {
if (process != null) {
// no-op unless process is alive
process.destroyForcibly();
}
if (transport != null) {
transport.close();
}
// 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 {
process = null;
transport = null;
}
}
}

View File

@@ -113,7 +113,7 @@ public final class VmFunction extends VmObjectLike {
@Override
@TruffleBoundary
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
public void setCachedValue(Object key, Object value) {
throw new VmExceptionBuilder()
.bug("Class `VmFunction` does not support method `setCachedValue()`.")
.build();

View File

@@ -33,7 +33,7 @@ import org.pkl.core.util.Nullable;
@TruffleLanguage.Registration(
id = "pkl",
name = "Pkl",
version = "0.27.0-dev",
version = "0.27.1",
characterMimeTypes = VmLanguage.MIME_TYPE,
contextPolicy = ContextPolicy.SHARED)
public final class VmLanguage extends TruffleLanguage<VmContext> {

View File

@@ -19,14 +19,13 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.util.EconomicMaps;
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 VmListing EMPTY =
new VmListing(
@@ -47,7 +46,7 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
int length) {
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
super(enclosingFrame, parent, members);
this.length = length;
}
@@ -56,16 +55,10 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
int length,
@Nullable VmListing delegate,
ListingOrMappingTypeCastNode typeCheckNode,
MaterializedFrame typeNodeFrame) {
super(
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
ListingOrMappingTypeCastNode typeCastNode,
Object typeCheckReceiver,
VmObjectLike typeCheckOwner) {
super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
this.length = length;
}
@@ -117,20 +110,6 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
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
@TruffleBoundary
public boolean equals(@Nullable Object obj) {
@@ -142,10 +121,14 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
force(false);
other.force(false);
for (var i = 0L; i < length; i++) {
var value = getCachedValue(i);
var cursor = cachedValues.getEntries();
while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null;
var otherValue = other.getCachedValue(i);
var otherValue = other.getCachedValue(key);
if (!value.equals(otherValue)) return false;
}
@@ -156,14 +139,16 @@ public final class VmListing extends VmListingOrMapping<VmListing> {
@TruffleBoundary
public int hashCode() {
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);
var result = 0;
for (var i = 0L; i < length; i++) {
var value = getCachedValue(i);
var cursor = cachedValues.getEntries();
while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null;
result = 31 * result + value.hashCode();
}

View File

@@ -16,159 +16,122 @@
package org.pkl.core.runtime;
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.nodes.IndirectCallNode;
import org.graalvm.collections.EconomicMap;
import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.UnmodifiableEconomicMap;
import org.pkl.core.PklBugException;
import org.pkl.core.ast.member.ListingOrMappingTypeCastNode;
import org.pkl.core.ast.member.ObjectMember;
import org.pkl.core.ast.type.TypeNode;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.EconomicSets;
import org.pkl.core.util.Nullable;
public abstract class VmListingOrMapping<SELF extends VmListingOrMapping<SELF>> extends VmObject {
/**
* 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;
public abstract class VmListingOrMapping extends VmObject {
// reified type of listing elements and mapping values
private final @Nullable ListingOrMappingTypeCastNode typeCastNode;
private final MaterializedFrame typeNodeFrame;
private final EconomicMap<Object, ObjectMember> cachedMembers = EconomicMaps.create();
private final EconomicSet<Object> checkedMembers = EconomicSets.create();
private final @Nullable Object typeCheckReceiver;
private final @Nullable VmObjectLike typeCheckOwner;
public VmListingOrMapping(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, parent, members);
typeCastNode = null;
typeCheckReceiver = null;
typeCheckOwner = null;
}
public VmListingOrMapping(
MaterializedFrame enclosingFrame,
@Nullable VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
@Nullable SELF delegate,
@Nullable ListingOrMappingTypeCastNode typeCastNode,
@Nullable MaterializedFrame typeNodeFrame) {
ListingOrMappingTypeCastNode typeCastNode,
Object typeCheckReceiver,
VmObjectLike typeCheckOwner) {
super(enclosingFrame, parent, members);
this.delegate = delegate;
this.typeCastNode = typeCastNode;
this.typeNodeFrame = typeNodeFrame;
this.typeCheckReceiver = typeCheckReceiver;
this.typeCheckOwner = typeCheckOwner;
}
ObjectMember findMember(Object key) {
var member = EconomicMaps.get(cachedMembers, key);
if (member != null) {
return member;
}
if (delegate != null) {
return delegate.findMember(key);
}
// member is guaranteed to exist; this is only called if `getCachedValue()` returns non-null
// and `setCachedValue` will record the object member in `cachedMembers`.
throw PklBugException.unreachableCode();
}
public @Nullable ListingOrMappingTypeCastNode getTypeCastNode() {
return typeCastNode;
}
@Override
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);
}
// Recursively executes type casts between `owner` and `this` and returns the resulting value.
public final Object executeTypeCasts(
Object value,
VmObjectLike owner,
IndirectCallNode callNode,
// if non-null, a stack frame for this member is inserted if a type cast fails
@Nullable ObjectMember member,
// Next type cast to be performed by the caller.
// Avoids repeating the same type cast in some cases.
@Nullable ListingOrMappingTypeCastNode nextTypeCastNode) {
var newNextTypeCastNode = typeCastNode != null ? typeCastNode : nextTypeCastNode;
@SuppressWarnings("DataFlowIssue")
var result =
this == owner
? value
: ((VmListingOrMapping) parent)
.executeTypeCasts(value, owner, callNode, member, newNextTypeCastNode);
if (typeCastNode == null || typeCastNode == nextTypeCastNode) return result;
var callTarget = typeCastNode.getCallTarget();
try {
return callNode.call(
callTarget, VmUtils.getReceiver(typeNodeFrame), VmUtils.getOwner(typeNodeFrame), ret);
} catch (VmException vmException) {
return callNode.call(callTarget, typeCheckReceiver, typeCheckOwner, result);
} catch (VmException e) {
CompilerDirectives.transferToInterpreter();
// treat typecheck as part of the call stack to read the original member if there is a
// source section for it.
var sourceSection = member.getBodySection();
if (!sourceSection.isAvailable()) {
sourceSection = member.getSourceSection();
if (member != null) {
VmUtils.insertStackFrame(member, callTarget, e);
}
if (sourceSection.isAvailable()) {
vmException
.getInsertedStackFrames()
.put(callTarget, VmUtils.createStackFrame(sourceSection, member.getQualifiedName()));
}
throw vmException;
throw e;
}
}
public abstract SELF withCheckedMembers(
ListingOrMappingTypeCastNode typeCastNode, MaterializedFrame typeNodeFrame);
@Override
@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}. */
public boolean hasSameChecksAs(TypeNode typeNode) {
// Optimization: Recursively steal value from parent cache to avoid computing it multiple times.
// 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) {
return false;
}
if (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;
return typeCastNode.getTypeNode().isEquivalentTo(typeNode);
}
}

View File

@@ -18,7 +18,6 @@ package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.frame.MaterializedFrame;
import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.GuardedBy;
import org.graalvm.collections.UnmodifiableEconomicMap;
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.LateInit;
public final class VmMapping extends VmListingOrMapping<VmMapping> {
public final class VmMapping extends VmListingOrMapping {
private int cachedEntryCount = -1;
@@ -50,24 +49,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
MaterializedFrame enclosingFrame,
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members) {
super(enclosingFrame, Objects.requireNonNull(parent), members, null, null, null);
super(enclosingFrame, parent, members);
}
public VmMapping(
MaterializedFrame enclosingFrame,
VmObject parent,
UnmodifiableEconomicMap<Object, ObjectMember> members,
VmMapping delegate,
ListingOrMappingTypeCastNode typeCheckNode,
MaterializedFrame typeNodeFrame) {
super(
enclosingFrame,
Objects.requireNonNull(parent),
members,
delegate,
typeCheckNode,
typeNodeFrame);
ListingOrMappingTypeCastNode typeCastNode,
Object typeCheckReceiver,
VmObjectLike typeCheckOwner) {
super(enclosingFrame, parent, members, typeCastNode, typeCheckReceiver, typeCheckOwner);
}
public static boolean isDefaultProperty(Object propertyKey) {
@@ -81,16 +73,12 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
@TruffleBoundary
public VmSet getAllKeys() {
if (delegate != null) {
return delegate.getAllKeys();
}
synchronized (this) {
if (__allKeys == null) {
// building upon parent's `getAllKeys()` should improve at least worst case efficiency
var parentKeys =
getParent() instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
var parentKeys = parent instanceof VmMapping mapping ? mapping.getAllKeys() : VmSet.EMPTY;
var builder = VmSet.builder(parentKeys);
for (var cursor = getMembers().getEntries(); cursor.advance(); ) {
for (var cursor = members.getEntries(); cursor.advance(); ) {
var member = cursor.getValue();
if (!member.isEntry()) continue;
builder.add(cursor.getKey());
@@ -133,12 +121,17 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
if (this == obj) return true;
if (!(obj instanceof VmMapping other)) return false;
if (getEntryCount() != other.getEntryCount()) return false;
// could use shallow force, but deep force is cached
force(false);
other.force(false);
for (var key : getAllKeys()) {
var value = getCachedValue(key);
if (getEntryCount() != other.getEntryCount()) return false;
var cursor = cachedValues.getEntries();
while (cursor.advance()) {
Object key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = cursor.getValue();
assert value != null;
var otherValue = other.getCachedValue(key);
if (!value.equals(otherValue)) return false;
@@ -151,38 +144,34 @@ public final class VmMapping extends VmListingOrMapping<VmMapping> {
@TruffleBoundary
public int hashCode() {
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);
var result = 0;
for (var key : getAllKeys()) {
var cursor = cachedValues.getEntries();
while (cursor.advance()) {
var key = cursor.getKey();
if (key instanceof Identifier) continue;
var value = getCachedValue(key);
var value = cursor.getValue();
assert value != null;
result += key.hashCode() ^ value.hashCode();
}
cachedHash = result;
return result;
}
// assumes mapping has been forced
public int getEntryCount() {
if (cachedEntryCount != -1) return cachedEntryCount;
cachedEntryCount = getAllKeys().getLength();
return cachedEntryCount;
}
@Override
@TruffleBoundary
public VmMapping withCheckedMembers(
ListingOrMappingTypeCastNode typeCheckNode, MaterializedFrame typeNodeFrame) {
return new VmMapping(
getEnclosingFrame(),
Objects.requireNonNull(getParent()),
getMembers(),
this,
typeCheckNode,
typeNodeFrame);
var result = 0;
for (var key : cachedValues.getKeys()) {
if (key instanceof Identifier) continue;
result += 1;
}
cachedEntryCount = result;
return result;
}
}

View File

@@ -87,12 +87,12 @@ public abstract class VmObject extends VmObjectLike {
}
@Override
public void setCachedValue(Object key, Object value, ObjectMember objectMember) {
public final void setCachedValue(Object key, Object value) {
EconomicMaps.put(cachedValues, key, value);
}
@Override
public boolean hasCachedValue(Object key) {
public final boolean hasCachedValue(Object key) {
return EconomicMaps.containsKey(cachedValues, key);
}

View File

@@ -96,7 +96,7 @@ public abstract class VmObjectLike extends VmValue {
* receiver.
*/
@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

View File

@@ -15,6 +15,7 @@
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
@@ -134,10 +135,6 @@ public final class VmUtils {
return result;
}
public static void setReceiver(Frame frame, Object receiver) {
frame.getArguments()[0] = receiver;
}
public static VmObjectLike getObjectReceiver(Frame frame) {
return (VmObjectLike) getReceiver(frame);
}
@@ -158,10 +155,6 @@ public final class VmUtils {
return result;
}
public static void setOwner(Frame frame, VmObjectLike owner) {
frame.getArguments()[1] = owner;
}
/** Returns a `ObjectMember`'s key while executing the corresponding `MemberNode`. */
public static Object getMemberKey(Frame frame) {
return frame.getArguments()[2];
@@ -261,17 +254,17 @@ public final class VmUtils {
final var constantValue = member.getConstantValue();
if (constantValue != null) {
var ret = constantValue;
// for a property, do a type check
var result = constantValue;
// for a property, Listing element, or Mapping value, do a type check
if (member.isProp()) {
var property = receiver.getVmClass().getProperty(member.getName());
if (property != null && property.getTypeNode() != null) {
var callTarget = property.getTypeNode().getCallTarget();
try {
if (checkType) {
ret = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
result = callNode.call(callTarget, receiver, property.getOwner(), constantValue);
} else {
ret =
result =
callNode.call(
callTarget,
receiver,
@@ -281,44 +274,52 @@ public final class VmUtils {
}
} catch (VmException e) {
CompilerDirectives.transferToInterpreter();
// A failed property type check looks as follows in the stack trace:
// x: Int(isPositive)
// at ...
// x = -10
// at ...
// However, if the value of `x` is a parse-time constant (as in `x = -10`),
// 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
// as in the non-constant case. (Parse-time constants are an internal optimization.)
e.getInsertedStackFrames()
.put(
callTarget,
createStackFrame(member.getBodySection(), member.getQualifiedName()));
insertStackFrame(member, callTarget, e);
throw e;
}
}
} 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);
} 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, ret, member);
return ret;
receiver.setCachedValue(memberKey, result);
return result;
}
var callTarget = member.getCallTarget();
Object ret;
Object result;
if (checkType) {
ret = callNode.call(callTarget, receiver, owner, memberKey);
result = callNode.call(callTarget, receiver, owner, memberKey);
} else {
ret = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
result = callNode.call(callTarget, receiver, owner, memberKey, VmUtils.SKIP_TYPECHECK_MARKER);
}
if (receiver instanceof VmListingOrMapping<?> vmListingOrMapping) {
ret = vmListingOrMapping.typecastObjectMember(member, ret, callNode);
receiver.setCachedValue(memberKey, result);
return result;
}
// A failed property type check looks as follows in the stack trace:
// x: Int(isPositive)
// at ...
// x = -10
// at ...
// However, if the value of `x` is a parse-time constant (as in `x = -10`),
// 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
// as in the non-constant case. (Parse-time constants are an internal optimization.)
public static void insertStackFrame(
ObjectMember member, CallTarget location, VmException exception) {
var sourceSection = member.getBodySection();
if (!sourceSection.isAvailable()) {
sourceSection = member.getSourceSection();
}
if (sourceSection.isAvailable()) {
exception
.getInsertedStackFrames()
.put(location, createStackFrame(sourceSection, member.getQualifiedName()));
}
receiver.setCachedValue(memberKey, ret, member);
return ret;
}
public static @Nullable ObjectMember findMember(VmObjectLike receiver, Object memberKey) {

View File

@@ -50,6 +50,7 @@ import org.pkl.core.runtime.VmDuration;
import org.pkl.core.runtime.VmDynamic;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmIntSeq;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.VmListing;
import org.pkl.core.runtime.VmMap;
@@ -573,7 +574,8 @@ public final class RendererNodes {
type =
requiresWrapper()
? null
: new ListingTypeNode(VmUtils.unavailableSourceSection(), valueType);
: new ListingTypeNode(
VmUtils.unavailableSourceSection(), VmLanguage.get(null), valueType);
return type;
} else if (type instanceof MappingTypeNode mappingType) {
var keyType = resolveType(mappingType.getKeyTypeNode());
@@ -587,7 +589,9 @@ public final class RendererNodes {
}
var valueType = resolveType(mappingType.getValueTypeNode());
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;
return type;

View File

@@ -30,6 +30,10 @@ import org.graalvm.collections.UnmodifiableMapCursor;
public final class EconomicMaps {
private EconomicMaps() {}
public static <K, V> UnmodifiableEconomicMap<K, V> emptyMap() {
return EconomicMap.emptyMap();
}
@TruffleBoundary
public static <K, V> EconomicMap<K, V> create() {
return EconomicMap.create();

View File

@@ -0,0 +1,3 @@
function isValid(str): Boolean = str.startsWith("a")
foo: Listing<String(isValid(this))>(isDistinct)

View File

@@ -27,12 +27,20 @@ local altered: Listing<Person> = (base) {
[0] { name = "Wood Pigeon" }
}
local computedIndex: Listing<Person> = (base) {
[computeIndex()] { name = "Wood Pigeon" }
}
local function computeIndex() = 0
facts {
["isEmpty"] {
empty.isEmpty
empty2.isEmpty
!base.isEmpty
!derived.isEmpty
!altered.isEmpty
!computedIndex.isEmpty
}
["lastIndex"] {
@@ -41,6 +49,8 @@ facts {
base.lastIndex == 2
derived.lastIndex == 4
duplicate.lastIndex == 5
altered.lastIndex == 2
computedIndex.lastIndex == 2
}
["isDistinct"] {
@@ -49,6 +59,8 @@ facts {
base.isDistinct
derived.isDistinct
!duplicate.isDistinct
altered.isDistinct
computedIndex.isDistinct
}
["isDistinctBy()"] {
@@ -57,18 +69,24 @@ facts {
base.isDistinctBy((it) -> it)
derived.isDistinctBy((it) -> it)
!duplicate.isDistinctBy((it) -> it)
altered.isDistinctBy((it) -> it)
computedIndex.isDistinctBy((it) -> it)
empty.isDistinctBy((it) -> it.name)
empty2.isDistinctBy((it) -> it.name)
base.isDistinctBy((it) -> it.name)
derived.isDistinctBy((it) -> it.name)
!duplicate.isDistinctBy((it) -> it.name)
altered.isDistinctBy((it) -> it.name)
computedIndex.isDistinctBy((it) -> it.name)
empty.isDistinctBy((it) -> it.getClass())
empty2.isDistinctBy((it) -> it.getClass())
!base.isDistinctBy((it) -> it.getClass())
!derived.isDistinctBy((it) -> it.getClass())
!duplicate.isDistinctBy((it) -> it.getClass())
!altered.isDistinctBy((it) -> it.getClass())
!computedIndex.isDistinctBy((it) -> it.getClass())
}
["getOrNull"] {
@@ -85,24 +103,32 @@ facts {
module.catch(() -> empty.first) == "Expected a non-empty Listing."
base.first == base[0]
derived.first == base[0]
altered.first != base[0]
computedIndex.first == altered.first
}
["firstOrNull"] {
empty.firstOrNull == null
base.firstOrNull == base[0]
derived.firstOrNull == base[0]
altered.firstOrNull != base[0]
computedIndex.firstOrNull == altered.first
}
["last"] {
module.catch(() -> empty.last) == "Expected a non-empty Listing."
base.last == base[2]
derived.last == derived[4]
altered.last == base[2]
computedIndex.last == base[2]
}
["lastOrNull"] {
empty.lastOrNull == null
base.lastOrNull == base[2]
derived.lastOrNull == derived[4]
altered.lastOrNull == base[2]
computedIndex.lastOrNull == base[2]
}
["single"] {
@@ -135,6 +161,7 @@ facts {
derived.contains(base[1])
derived.contains(derived[3])
!altered.contains(base[0])
!computedIndex.contains(base[0])
}
}
@@ -144,6 +171,17 @@ examples {
empty2.length
base.length
derived.length
altered.length
computedIndex.length
local elementsAndEntries = (base) {
new { name = "" }
[2] { name = "" }
[computeIndex()] { name = "" }
new { name = "" }
[1 + 0] { name = "" }
}
elementsAndEntries.length
}
["toList()"] {
@@ -152,6 +190,8 @@ examples {
base.toList()
derived.toList()
duplicate.toList()
altered.toList()
computedIndex.toList()
}
["toSet()"] {
@@ -160,6 +200,8 @@ examples {
base.toSet()
derived.toSet()
duplicate.toSet()
altered.toSet()
computedIndex.toSet()
}
["distinct"] {
@@ -168,6 +210,8 @@ examples {
base.distinct
derived.distinct
duplicate.distinct
altered.distinct
computedIndex.distinct
}
["distinctBy()"] {
@@ -176,36 +220,48 @@ examples {
base.distinctBy((it) -> it)
derived.distinctBy((it) -> it)
duplicate.distinctBy((it) -> it)
altered.distinctBy((it) -> it)
computedIndex.distinctBy((it) -> it)
empty.distinctBy((it) -> it.name)
empty2.distinctBy((it) -> it.name)
base.distinctBy((it) -> it.name)
derived.distinctBy((it) -> it.name)
duplicate.distinctBy((it) -> it.name)
altered.distinctBy((it) -> it.name)
computedIndex.distinctBy((it) -> it.name)
empty.distinctBy((it) -> it.getClass())
empty2.distinctBy((it) -> it.getClass())
base.distinctBy((it) -> it.getClass())
derived.distinctBy((it) -> it.getClass())
duplicate.distinctBy((it) -> it.getClass())
altered.distinctBy((it) -> it.getClass())
computedIndex.distinctBy((it) -> it.getClass())
}
["fold"] {
empty.fold(List(), (l, e) -> l.add(e))
base.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"] {
empty.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)))
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 baseString = new Listing { "Pigeon"; "Barn Owl"; "Parrot" }
local derivedString = (baseString) { "Albatross"; "Elf Owl" }
local alteredString = (baseString) { [0] = "Wood Pigeon" }
local computedIndexString = (baseString) { [computeIndex()] = "Wood Pigeon" }
["join"] {
empty.join("")
@@ -215,5 +271,9 @@ examples {
baseString.join("---")
derivedString.join("")
derivedString.join("\n")
alteredString.join("")
alteredString.join("\n")
computedIndexString.join("")
computedIndexString.join("\n")
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -0,0 +1,5 @@
const local lastName = "Birdo"
typealias Birds = Listing<String(endsWith(lastName))>
typealias Birds2 = Pair<Listing<String(endsWith(lastName))>, Int>

View File

@@ -1,5 +1,7 @@
import "pkl:test"
import "helpers/originalTypealias.pkl"
typealias Simple = String
const function simple(arg: Simple): Simple = arg
@@ -105,3 +107,8 @@ res19: LocalTypeAlias = "abc"
typealias VeryComposite = Pair<Composite, Composite>
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)

View File

@@ -0,0 +1,3 @@
import "helpers/originalTypealias.pkl"
res: originalTypealias.Birds = new { "Jimmy Bird" }

View File

@@ -4,6 +4,8 @@ facts {
true
true
true
true
true
}
["lastIndex"] {
true
@@ -11,6 +13,8 @@ facts {
true
true
true
true
true
}
["isDistinct"] {
true
@@ -18,6 +22,8 @@ facts {
true
true
true
true
true
}
["isDistinctBy()"] {
true
@@ -35,6 +41,12 @@ facts {
true
true
true
true
true
true
true
true
true
}
["getOrNull"] {
true
@@ -49,21 +61,29 @@ facts {
true
true
true
true
true
}
["firstOrNull"] {
true
true
true
true
true
}
["last"] {
true
true
true
true
true
}
["lastOrNull"] {
true
true
true
true
true
}
["single"] {
true
@@ -91,6 +111,7 @@ facts {
true
true
true
true
}
}
examples {
@@ -99,6 +120,9 @@ examples {
0
3
5
3
3
5
}
["toList()"] {
List()
@@ -134,6 +158,20 @@ examples {
}, new {
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()"] {
Set()
@@ -167,6 +205,20 @@ examples {
}, new {
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"] {
new {}
@@ -216,6 +268,28 @@ examples {
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()"] {
new {}
@@ -265,6 +339,28 @@ examples {
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 {
@@ -312,6 +408,28 @@ examples {
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 {
@@ -329,6 +447,16 @@ examples {
name = "Pigeon"
}
}
new {
new {
name = "Wood Pigeon"
}
}
new {
new {
name = "Wood Pigeon"
}
}
}
["fold"] {
List()
@@ -350,6 +478,20 @@ examples {
}, new {
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"] {
List()
@@ -371,6 +513,20 @@ examples {
}), Pair(4, new {
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"] {
""
@@ -386,5 +542,17 @@ examples {
Albatross
Elf Owl
"""
"Wood PigeonBarn OwlParrot"
"""
Wood Pigeon
Barn Owl
Parrot
"""
"Wood PigeonBarn OwlParrot"
"""
Wood Pigeon
Barn Owl
Parrot
"""
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
foo1 {
"hello"
}
foo2 {
"hello"
}
res1 = true
res2 = true

View File

@@ -0,0 +1,4 @@
foo {
"abcdx"
"ax"
}

View File

@@ -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"
res19 = "abc"
res20 = Pair(Map("abc", List("def")), Map("abc", List("def")))
res21 {
"John Birdo"
}
res22 = Pair(new {
"John Birdo"
}, 0)

View File

@@ -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)

View File

@@ -117,7 +117,7 @@ class DocGenerator(
}
private fun DocPackage.deletePackageDir() {
outputDir.resolve("$name/$version").deleteRecursively()
outputDir.resolve(IoUtils.encodePath("$name/$version")).deleteRecursively()
}
private fun createSymlinks(currentPackagesData: List<PackageData>) {

View File

@@ -18,6 +18,7 @@ package org.pkl.gradle;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
@@ -27,6 +28,7 @@ import org.gradle.api.GradleException;
import org.gradle.api.NamedDomainObjectContainer;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Transformer;
import org.gradle.api.file.SourceDirectorySet;
import org.gradle.api.plugins.Convention;
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.ProjectResolveTask;
import org.pkl.gradle.task.TestTask;
import org.pkl.gradle.utils.PluginUtils;
@SuppressWarnings("unused")
public class PklPlugin implements Plugin<Project> {
@@ -456,6 +459,9 @@ public class PklPlugin implements Plugin<Project> {
private List<File> getTransitiveModules(AnalyzeImportsTask analyzeTask) {
var outputFile = analyzeTask.getOutputFile().get().getAsFile().toPath();
if (!analyzeTask.getOnlyIf().isSatisfiedBy(analyzeTask)) {
return Collections.emptyList();
}
try {
var contents = Files.readString(outputFile);
ImportGraph importGraph = ImportGraph.parseFromJson(contents);
@@ -470,9 +476,16 @@ public class PklPlugin implements Plugin<Project> {
}
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);
task.getSourceModules().set(spec.getSourceModules());
if (mapSourceModules != null) {
task.getSourceModules().set(spec.getSourceModules().map(mapSourceModules));
} else {
task.getSourceModules().set(spec.getSourceModules());
}
task.getNoProject().set(spec.getNoProject());
task.getProjectDir().set(spec.getProjectDir());
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) {
var outputFile =
project
@@ -496,11 +514,26 @@ public class PklPlugin implements Plugin<Project> {
spec.getName() + "GatherImports",
AnalyzeImportsTask.class,
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.setGroup("build");
task.getOutputFormat().set(OutputFormat.JSON.toString());
task.getOutputFile().set(outputFile);
task.onlyIf(ignored -> !task.getSourceModules().get().isEmpty());
});
}

View File

@@ -17,9 +17,6 @@ package org.pkl.gradle.task;
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 java.time.Duration;
@@ -31,10 +28,8 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.MapProperty;
@@ -49,9 +44,9 @@ import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit;
import org.pkl.core.util.Nullable;
import org.pkl.gradle.utils.PluginUtils;
public abstract class BasePklTask extends DefaultTask {
@Input
@@ -74,7 +69,7 @@ public abstract class BasePklTask extends DefaultTask {
@Internal
public Provider<Object> getParsedSettingsModule() {
return getSettingsModule().map(this::parseModuleNotation);
return getSettingsModule().map(PluginUtils::parseModuleNotation);
}
@InputFile
@@ -165,7 +160,7 @@ public abstract class BasePklTask extends DefaultTask {
parseModulePath(),
getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), this::parseModuleNotationToUri),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
null,
getEvalTimeout().getOrNull(),
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());
}
/**
* 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) {
return patterns.stream().map(Pattern::compile).collect(Collectors.toList());
}

View File

@@ -37,8 +37,8 @@ import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Pair;
import org.pkl.gradle.utils.PluginUtils;
public abstract class ModulesTask extends BasePklTask {
// We expose the contents of this property as task inputs via the sourceModuleFiles
@@ -84,7 +84,7 @@ public abstract class ModulesTask extends BasePklTask {
@Override
protected List<URI> getSourceModulesAsUris() {
return getSourceModules().get().stream()
.map(this::parseModuleNotationToUri)
.map(PluginUtils::parseModuleNotationToUri)
.collect(Collectors.toList());
}
@@ -117,7 +117,7 @@ public abstract class ModulesTask extends BasePklTask {
var files = new ArrayList<File>();
var uris = new ArrayList<URI>();
for (var m : modules) {
var parsed = parseModuleNotation(m);
var parsed = PluginUtils.parseModuleNotation(m);
if (parsed instanceof File file) {
files.add(file);
} else if (parsed instanceof URI uri) {
@@ -127,28 +127,6 @@ public abstract class ModulesTask extends BasePklTask {
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
@Override
public void runTask() {
@@ -172,7 +150,7 @@ public abstract class ModulesTask extends BasePklTask {
parseModulePath(),
getProject().getProjectDir().toPath(),
mapAndGetOrNull(getEvalRootDirPath(), Paths::get),
mapAndGetOrNull(getSettingsModule(), this::parseModuleNotationToUri),
mapAndGetOrNull(getSettingsModule(), PluginUtils::parseModuleNotationToUri),
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.gradle.utils;
import org.pkl.core.util.NonnullByDefault;

View File

@@ -15,13 +15,17 @@
*/
package org.pkl.gradle
import java.nio.file.Path
import kotlin.io.path.readText
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.PackageServer
class PkldocGeneratorsTest : AbstractTest() {
@Test
fun `generate docs`() {
fun `generate docs`(@TempDir tempDir: Path) {
PackageServer.populateCacheDir(tempDir)
writeFile(
"build.gradle",
"""
@@ -32,7 +36,8 @@ class PkldocGeneratorsTest : AbstractTest() {
pkl {
pkldocGenerators {
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")
settingsModule = "pkl:settings"
}
@@ -94,6 +99,39 @@ class PkldocGeneratorsTest : AbstractTest() {
checkTextContains(moduleFile.readText(), "<html>", "Person", "Address", "other")
checkTextContains(personFile.readText(), "<html>", "name", "addresses")
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