2121import java .util .HashSet ;
2222import java .util .List ;
2323import java .util .Set ;
24+ import java .util .function .BiConsumer ;
25+ import java .util .function .Supplier ;
2426import org .jspecify .annotations .Nullable ;
2527import org .pkl .core .Composite ;
2628import 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}
0 commit comments