@@ -13,6 +13,9 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
1313 if ( a . void || b . void ) { throw new Error ( 'isSuperType() does not handle voids' ) ; }
1414 if ( a . isAny ) { return { success : true } ; }
1515
16+ // Assume true if we're currently already checking this.
17+ if ( CURRENTLY_CHECKING . has ( a , b ) ) { return { success : true } ; }
18+
1619 if ( a . primitive !== undefined ) {
1720 if ( a . primitive === b . primitive ) { return { success : true } ; }
1821 return failure ( `${ b } is not assignable to ${ a } ` ) ;
@@ -52,32 +55,39 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
5255 ) ;
5356 }
5457
55- // For named types, we'll always do a nominal typing relationship.
56- // That is, if in the updated typesystem someone were to use the type name
57- // from the old assembly, do they have a typing relationship that's accepted
58- // by a nominal type system. (That check also rules out enums)
59- const nominalCheck = isNominalSuperType ( a , b , updatedSystem ) ;
60- if ( nominalCheck . success === false ) { return nominalCheck ; }
61-
62- // At this point, the types are legal in the updated assembly's type system.
63- // However, for structs we also structurally check the fields between the OLD
64- // and the NEW type system.
65- // We could do more complex analysis on typing of methods, but it doesn't seem
66- // worth it.
58+ // We have two named types, recursion might happen so protect against it.
59+ CURRENTLY_CHECKING . add ( a , b ) ;
6760 try {
68- const A = a . type ! ; // Note: lookup in old type system!
69- const B = b . type ! ;
70- if ( A . isInterfaceType ( ) && A . isDataType ( ) && B . isInterfaceType ( ) && B . datatype ) {
71- return isStructuralSuperType ( A , B , updatedSystem ) ;
61+
62+ // For named types, we'll always do a nominal typing relationship.
63+ // That is, if in the updated typesystem someone were to use the type name
64+ // from the old assembly, do they have a typing relationship that's accepted
65+ // by a nominal type system. (That check also rules out enums)
66+ const nominalCheck = isNominalSuperType ( a , b , updatedSystem ) ;
67+ if ( nominalCheck . success === false ) { return nominalCheck ; }
68+
69+ // At this point, the types are legal in the updated assembly's type system.
70+ // However, for structs we also structurally check the fields between the OLD
71+ // and the NEW type system.
72+ // We could do more complex analysis on typing of methods, but it doesn't seem
73+ // worth it.
74+ try {
75+ const A = a . type ! ; // Note: lookup in old type system!
76+ const B = b . type ! ;
77+ if ( A . isInterfaceType ( ) && A . isDataType ( ) && B . isInterfaceType ( ) && B . datatype ) {
78+ return isStructuralSuperType ( A , B , updatedSystem ) ;
79+ }
80+ } catch ( e ) {
81+ // We might get an exception if the type is supposed to come from a different
82+ // assembly and the lookup fails.
83+ return failure ( e . message ) ;
7284 }
73- } catch ( e ) {
74- // We might get an exception if the type is supposed to come from a different
75- // assembly and the lookup fails.
76- return { success : false , reasons : [ e . message ] } ;
77- }
7885
79- // All seems good
80- return { success : true } ;
86+ // All seems good
87+ return { success : true } ;
88+ } finally {
89+ CURRENTLY_CHECKING . remove ( a , b ) ;
90+ }
8191}
8292
8393/**
@@ -147,7 +157,7 @@ function isStructuralSuperType(a: reflect.InterfaceType, b: reflect.InterfaceTyp
147157 }
148158
149159 const ana = isSuperType ( aProp . type , bProp . type , updatedSystem ) ;
150- if ( ! ana . success ) { return ana ; }
160+ if ( ! ana . success ) { return failure ( `property ${ name } ` , ... ana . reasons ) ; }
151161 }
152162
153163 return { success : true } ;
@@ -166,3 +176,49 @@ export function prependReason(analysis: Analysis, message: string): Analysis {
166176 if ( analysis . success ) { return analysis ; }
167177 return failure ( message , ...analysis . reasons ) ;
168178}
179+
180+ /**
181+ * Keep a set of type pairs.
182+ *
183+ * Needs more code than it should because JavaScript has an anemic runtime.
184+ *
185+ * (The ideal type would have been Set<[string, string]> but different
186+ * instances of those pairs wouldn't count as "the same")
187+ */
188+ class TypePairs {
189+ private readonly pairs = new Map < string , Set < string > > ( ) ;
190+
191+ public add ( a : reflect . TypeReference , b : reflect . TypeReference ) : void {
192+ if ( ! a . fqn || ! b . fqn ) { return ; } // Only for user-defined types
193+
194+ let image = this . pairs . get ( a . fqn ) ;
195+ if ( ! image ) {
196+ this . pairs . set ( a . fqn , image = new Set < string > ( ) ) ;
197+ }
198+ image . add ( b . fqn ) ;
199+ }
200+
201+ public remove ( a : reflect . TypeReference , b : reflect . TypeReference ) : void {
202+ if ( ! a . fqn || ! b . fqn ) { return ; } // Only for user-defined types
203+
204+ const image = this . pairs . get ( a . fqn ) ;
205+ if ( image ) {
206+ image . delete ( b . fqn ) ;
207+ }
208+ }
209+
210+ public has ( a : reflect . TypeReference , b : reflect . TypeReference ) : boolean {
211+ if ( ! a . fqn || ! b . fqn ) { return false ; } // Only for user-defined types
212+
213+ const image = this . pairs . get ( a . fqn ) ;
214+ return ! ! image && image . has ( b . fqn ) ;
215+ }
216+ }
217+
218+ /**
219+ * To avoid recursion, we keep a set of relationships we're *currently*
220+ * checking, so that if we're recursing we can just assume the subtyping
221+ * relationship holds (and let the other struct members falsify that
222+ * statement).
223+ */
224+ const CURRENTLY_CHECKING = new TypePairs ( ) ;
0 commit comments