Skip to content

Commit d4deef5

Browse files
committed
track PType instead of Set<PType>, fix subtype check for type args with unions
1 parent 50fe92e commit d4deef5

4 files changed

Lines changed: 100 additions & 65 deletions

File tree

pkl-core/src/main/java/org/pkl/core/runtime/VmReference.java

Lines changed: 83 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.HashSet;
2222
import java.util.List;
2323
import java.util.Set;
24+
import java.util.function.BiConsumer;
25+
import java.util.function.Supplier;
2426
import org.jspecify.annotations.Nullable;
2527
import org.pkl.core.Composite;
2628
import org.pkl.core.PClass;
@@ -38,7 +40,7 @@ public final class VmReference extends VmValue {
3840
private final ImRrbt<VmTyped> path;
3941
// candidate types can only be: PType.Class, PType.Alias (only preservedAliasTypes),
4042
// PType.StringLiteral, or PType.UNKNOWN
41-
private final Set<PType> candidateTypes;
43+
private final PType referentType;
4244

4345
private boolean forced = false;
4446

@@ -69,10 +71,10 @@ public VmReference(VmTyped domain, VmClass clazz, Object data) {
6971
normalizeTypes(new PType.Class(clazz.export()), clazz.getModule().getVmClass().export()));
7072
}
7173

72-
public VmReference(VmTyped domain, Object data, ImRrbt<VmTyped> path, Set<PType> candidateTypes) {
74+
public VmReference(VmTyped domain, Object data, ImRrbt<VmTyped> path, PType referentType) {
7375
this.domain = domain;
7476
this.data = data;
75-
this.candidateTypes = candidateTypes;
77+
this.referentType = referentType;
7678
this.path = path;
7779
}
7880

@@ -88,19 +90,26 @@ public List<VmTyped> getPath() {
8890
return path;
8991
}
9092

93+
public PType getReferentType() {
94+
return referentType;
95+
}
96+
9197
// simplifies a type by:
9298
// * erasing constraints
9399
// * transforming T? into T|Null
94100
// * dereferencing aliases (except for well-known stdlib alias types)
95101
// * flattening unions
96102
// * when moduleClass is supplied, replace PType.MODULE with appropriate PType.Class
97103
// * drop PType.NOTHING, PType.Function, and PType.TypeVariable
98-
private static Set<PType> normalizeTypes(PType type, PClass moduleClass) {
104+
private static PType normalizeTypes(PType type, PClass moduleClass) {
99105
var types = new HashSet<PType>();
100106
normalizeTypes(type, moduleClass, types);
101-
if (types.contains(PType.UNKNOWN)) return Set.of(PType.UNKNOWN);
102-
if (containsClass(types, anyType.getPClass())) return Set.of(anyType);
103-
return types;
107+
if (types.size() == 1) return types.iterator().next();
108+
if (types.contains(PType.UNKNOWN)) return PType.UNKNOWN;
109+
if (containsClass(types, anyType.getPClass())) return anyType;
110+
var typesList = new ArrayList<>(types);
111+
typesList.sort(Comparator.comparing(Object::toString));
112+
return new PType.Union(typesList);
104113
}
105114

106115
private static void normalizeTypes(PType type, PClass moduleClass, Set<PType> result) {
@@ -119,8 +128,7 @@ private static void normalizeTypes(PType type, PClass moduleClass, Set<PType> re
119128
} else {
120129
var typeArgs = new ArrayList<PType>(clazz.getTypeArguments().size());
121130
for (var arg : clazz.getTypeArguments()) {
122-
var tt = new ArrayList<>(normalizeTypes(arg, moduleClass));
123-
typeArgs.add(tt.size() == 1 ? tt.get(0) : new PType.Union(tt));
131+
typeArgs.add(normalizeTypes(arg, moduleClass));
124132
}
125133
result.add(new PType.Class(clazz.getPClass(), typeArgs));
126134
}
@@ -144,33 +152,50 @@ private static void normalizeTypes(PType type, PClass moduleClass, Set<PType> re
144152
}
145153
}
146154

155+
private static Iterable<PType> iterateTypes(PType t) {
156+
if (t instanceof PType.Union union) return union.getElementTypes();
157+
return Collections.singleton(t);
158+
}
159+
147160
public @Nullable VmReference withPropertyAccess(Identifier property) {
148-
Set<PType> candidates = new HashSet<>();
149-
for (var t : candidateTypes) {
150-
getCandidatePropertyType(t, property.toString(), candidates);
151-
}
152-
if (candidates.isEmpty()) {
153-
return null; // no valid property found
154-
} else if (candidates.contains(PType.UNKNOWN)) {
155-
// optimization: unknown allows all references, erase all candidates to only unknown
156-
candidates = Set.of(PType.UNKNOWN);
157-
}
158-
return new VmReference(
159-
domain, data, path.append(newAccess(property.toString(), null)), candidates);
161+
var propString = property.toString();
162+
return withAccess(
163+
(t, candidates) -> getCandidatePropertyType(t, propString, candidates),
164+
() -> newAccess(property.toString(), null));
160165
}
161166

162167
public @Nullable VmReference withSubscriptAccess(Object key) {
168+
return withAccess(
169+
(t, candidates) -> getCandidateSubscriptType(t, key, candidates),
170+
() -> newAccess(null, key));
171+
}
172+
173+
private @Nullable VmReference withAccess(
174+
BiConsumer<PType, Set<PType>> checkCandidate, Supplier<VmTyped> makeAccess) {
163175
Set<PType> candidates = new HashSet<>();
164-
for (var t : candidateTypes) {
165-
getCandidateSubscriptType(t, key, candidates);
176+
for (var t : iterateTypes(referentType)) {
177+
checkCandidate.accept(t, candidates);
166178
}
167179
if (candidates.isEmpty()) {
168-
return null; // no valid subscript found
180+
return null; // no valid access found
181+
}
182+
183+
PType newReferent;
184+
if (candidates.size() == 1) {
185+
newReferent = candidates.iterator().next();
169186
} else if (candidates.contains(PType.UNKNOWN)) {
170187
// optimization: unknown allows all references, erase all candidates to only unknown
171-
candidates = Set.of(PType.UNKNOWN);
188+
newReferent = PType.UNKNOWN;
189+
} else if (containsClass(candidates, anyType.getPClass())) {
190+
// optimization: All allows all references, erase all candidates to only All
191+
newReferent = anyType;
192+
} else {
193+
var types = new ArrayList<>(candidates);
194+
types.sort(Comparator.comparing(Object::toString));
195+
newReferent = new PType.Union(types);
172196
}
173-
return new VmReference(domain, data, path.append(newAccess(null, key)), candidates);
197+
198+
return new VmReference(domain, data, path.append(makeAccess.get()), newReferent);
174199
}
175200

176201
@SuppressWarnings("DuplicatedCode")
@@ -234,7 +259,7 @@ private static void getCandidateSubscriptType(PType type, Object key, Set<PType>
234259
|| clazz.getPClass().getInfo() == PClassInfo.Map) {
235260
var typeArgs = clazz.getTypeArguments();
236261
var keyTypes = normalizeTypes(typeArgs.get(0), clazz.getPClass().getModuleClass());
237-
for (var kt : keyTypes) {
262+
for (var kt : iterateTypes(keyTypes)) {
238263
if (kt == PType.UNKNOWN
239264
|| (kt instanceof PType.Class klazz
240265
&& klazz.getPClass().getInfo() == PClassInfo.forValue(VmValue.export(key)))
@@ -252,38 +277,34 @@ private static void getCandidateSubscriptType(PType type, Object key, Set<PType>
252277
*/
253278
public boolean referentTypeIsSubtypeOf(PType type, PClass moduleClass) {
254279
// fast path: if referent is unknown it can match any type check
255-
if (candidateTypes.contains(PType.UNKNOWN)) {
280+
if (referentType == PType.UNKNOWN) {
256281
return true;
257282
}
258283

259-
var checkTypes = normalizeTypes(type, moduleClass);
284+
var checkType = normalizeTypes(type, moduleClass);
260285
// fast path: short circuit if any referent is accepted
261-
if (checkTypes.contains(PType.UNKNOWN) || containsClass(checkTypes, anyType.getPClass())) {
286+
if (checkType == PType.UNKNOWN || isClass(checkType, anyType.getPClass())) {
262287
return true;
263288
}
264289
// fast path: short circuit if nothing is accepted
265-
if (checkTypes.size() == 1 && checkTypes.contains(PType.NOTHING)) {
290+
if (checkType == PType.NOTHING) {
266291
return false;
267292
}
268293

269-
// all candidate types must be subtypes of at least one target type
270-
candidate:
271-
for (var c : candidateTypes) {
272-
for (var t : checkTypes) {
273-
if (isSubtype(c, t)) continue candidate;
274-
}
275-
return false;
276-
}
277-
return true;
294+
return isSubtype(referentType, checkType);
278295
}
279296

280297
private static boolean containsClass(Set<PType> types, PClass pClass) {
281298
for (var t : types) {
282-
if (t instanceof PType.Class clazz && clazz.getPClass() == pClass) return true;
299+
if (isClass(t, pClass)) return true;
283300
}
284301
return false;
285302
}
286303

304+
private static boolean isClass(PType t, PClass pClass) {
305+
return t instanceof PType.Class clazz && clazz.getPClass() == pClass;
306+
}
307+
287308
private static boolean isSubtype(PType a, PType b) {
288309
// checks if A is a subtype of B
289310
// cases (A -> B)
@@ -305,6 +326,8 @@ private static boolean isSubtype(PType a, PType b) {
305326
// * invariant: A_i must be identical to B_i
306327
// * covariant: A_i must be a subtype of B_i
307328
// * contravariant: B_i must be a subtype of A_i
329+
// * Union -> Union: Each elem of A must be a subtype of at least one elem of B
330+
// * Non-union -> Union: A must be a subtype of at least one elem of B
308331
if (a == b) return true;
309332

310333
if (a instanceof PType.StringLiteral aStr) {
@@ -366,6 +389,21 @@ private static boolean isSubtype(PType a, PType b) {
366389
}
367390
}
368391
return true;
392+
} else if (b instanceof PType.Union bUnion) {
393+
if (a instanceof PType.Union aUnion) {
394+
a:
395+
for (var aElem : aUnion.getElementTypes()) {
396+
for (var bElem : bUnion.getElementTypes()) {
397+
if (isSubtype(aElem, bElem)) continue a;
398+
}
399+
return false;
400+
}
401+
return true;
402+
} else {
403+
for (var bElem : bUnion.getElementTypes()) {
404+
if (isSubtype(a, bElem)) return true;
405+
}
406+
}
369407
}
370408
return false;
371409
}
@@ -395,22 +433,14 @@ public Reference export() {
395433
pathList.add(elem.export());
396434
}
397435

398-
return new Reference(domain.export(), VmValue.export(data), pathList, exportReferentType());
399-
}
400-
401-
public PType exportReferentType() {
402-
if (candidateTypes.size() == 1) return candidateTypes.iterator().next();
403-
var types = new ArrayList<>(candidateTypes);
404-
// sort multiple candidate types to ensure stable output
405-
types.sort(Comparator.comparing(Object::toString));
406-
return new PType.Union(types);
436+
return new Reference(domain.export(), VmValue.export(data), pathList, getReferentType());
407437
}
408438

409439
public PType exportType() {
410440
return new PType.Class(
411441
RefModule.getReferenceClass().export(),
412442
new PType.Class(domain.getVmClass().export()),
413-
exportReferentType());
443+
getReferentType());
414444
}
415445

416446
@Override
@@ -433,15 +463,15 @@ public boolean equals(@Nullable Object o) {
433463
return domain.equals(that.domain)
434464
&& data.equals(that.data)
435465
&& path.equals(that.path)
436-
&& candidateTypes.equals(that.candidateTypes);
466+
&& referentType.equals(that.referentType);
437467
}
438468

439469
@Override
440470
public int hashCode() {
441471
int result = domain.hashCode();
442472
result = 31 * result + data.hashCode();
443473
result = 31 * result + path.hashCode();
444-
result = 31 * result + candidateTypes.hashCode();
474+
result = 31 * result + referentType.hashCode();
445475
return result;
446476
}
447477
}

pkl-core/src/main/java/org/pkl/core/runtime/VmValueRenderer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public void visitReference(VmReference value) {
277277
append("Reference(");
278278
visit(value.getDomain());
279279
append(", ");
280-
append(value.exportReferentType());
280+
append(value.getReferentType());
281281
append(", ");
282282
visit(value.getData());
283283
append(")");

pkl-core/src/test/files/LanguageSnippetTests/input/api/reference.pkl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ import "pkl:ref"
44
class D extends ref.Domain {
55
function renderReference(reference: ref.Reference<D, Any>): String =
66
let (data = reference.getData())
7-
if (data is Resource)
8-
let (
9-
path =
10-
reference
11-
.getPath()
12-
.map((elem) -> if (elem.isProperty) ".\(elem.property)" else "[\(elem.key)]")
13-
)
14-
"${\(data.name)\(path.join(""))}"
15-
else
16-
throw("can only render references rooted to Resource instances")
7+
let (root = if (data is Resource) data.name else data.toString())
8+
let (
9+
path =
10+
reference
11+
.getPath()
12+
.map((elem) -> if (elem.isProperty) ".\(elem.property)" else "[\(elem.key)]")
13+
)
14+
"${\(root)\(path.join(""))}"
1715
}
1816

1917
local const d: D = new {}
@@ -86,7 +84,7 @@ aRef: Ref<A> = a.$
8684
bRef: Ref<B> = b.$
8785
unknownRef: Ref<A | B> = aRef
8886
unknownRef2: Ref<A> | Ref<B> = aRef
89-
unknownRef3: Ref<A | B> = aOrB.$
87+
unknownRef3: Ref<B | A> = aOrB.$
9088

9189
k: K = new {
9290
aId = aRef.id
@@ -121,6 +119,12 @@ refInterpolation = "\(aRef.outputs.someListing[1])"
121119
kInterpolation = "\(k)"
122120
aValuesJoined = k.aValues.join("\n").replaceAll(Regex("@[a-z0-9]+"), "@<addr>")
123121

122+
// ensure that type arguments that are unions are handled correctly
123+
typeArgs = ref.Reference(d, TypeHolder, null).prop as Ref<Listing<Number | Boolean | String>>
124+
class TypeHolder {
125+
prop: Listing<String | Boolean | Number>
126+
}
127+
124128
output {
125129
renderer {
126130
converters {

pkl-core/src/test/files/LanguageSnippetTests/output/api/reference.pcf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ aValuesJoined = """
5757
org.pkl.core.runtime.VmReference@<addr>
5858
org.pkl.core.runtime.VmReference@<addr>
5959
"""
60+
typeArgs = "${null.prop}"

0 commit comments

Comments
 (0)