@@ -17,7 +17,7 @@ export function toRepresentation(
1717export function toRepresentations (
1818 asts : readonly [ SchemaAST . AST , ...Array < SchemaAST . AST > ]
1919) : SchemaRepresentation . MultiDocument {
20- return lowerASTs ( asts )
20+ return fromASTs ( asts )
2121}
2222
2323type CheckRepresentationAnnotation = SchemaRepresentation . CheckRepresentationAnnotation <
@@ -37,34 +37,32 @@ function isShareable(ast: SchemaAST.AST): boolean {
3737
3838interface ReferenceIdentifier {
3939 readonly identifier : string
40- readonly isFallback : boolean
40+ readonly fallback ?: string | undefined
4141}
4242
43- function resolveReferenceIdentifier ( ast : SchemaAST . AST ) : ReferenceIdentifier | undefined {
44- const identifier = InternalAnnotations . resolveIdentifier ( ast )
45- if ( identifier !== undefined ) return { identifier, isFallback : false }
46- const fallback = InternalAnnotations . resolveIdentifierFallback ( ast )
47- return fallback === undefined ? undefined : { identifier : `${ fallback } JsonEncoding` , isFallback : true }
43+ function resolveReferenceIdentifier (
44+ input : SchemaAST . AST ,
45+ encoded : SchemaAST . AST
46+ ) : ReferenceIdentifier | undefined {
47+ const identifier = InternalAnnotations . resolveIdentifier ( encoded )
48+ if ( identifier !== undefined ) return { identifier }
49+ const fallback = ( encoded !== input ? InternalAnnotations . resolveIdentifier ( input ) : undefined ) ??
50+ InternalAnnotations . resolveIdentifierFallback ( encoded )
51+ return fallback === undefined
52+ ? undefined
53+ : { identifier : `${ fallback } Encoded` , fallback }
4854}
4955
50- function hasSameReferenceOwner ( self : SchemaAST . AST , that : SchemaAST . AST ) : boolean {
51- if ( self === that ) return true
52- const selfKeys = Reflect . ownKeys ( self )
53- const thatKeys = Reflect . ownKeys ( that )
54- if ( selfKeys . length !== thatKeys . length ) return false
55- for ( const key of selfKeys ) {
56- if ( key !== "context" && ( self as any ) [ key ] !== ( that as any ) [ key ] ) return false
57- }
58- return true
59- }
60-
61- function lowerASTs (
56+ function fromASTs (
6257 asts : readonly [ SchemaAST . AST , ...Array < SchemaAST . AST > ]
6358) : SchemaRepresentation . MultiDocument {
6459 const references : Record < string , SchemaRepresentation . Representation > = { }
65- const referenceMap = new Map < SchemaAST . AST , string > ( )
66- const fallbackReferences : Array < readonly [ SchemaAST . AST , string ] > = [ ]
60+ const anonymousReferences = new Map < SchemaAST . AST , string > ( )
6761 const referenceOwners = new Map < string , SchemaAST . AST > ( )
62+ const valueIds = new Map < unknown , number > ( )
63+ const canonicalByKey = new Map < string , SchemaAST . AST > ( )
64+ let nextValueId = 0
65+ const buildingReferences = new Set < string > ( )
6866 const visiting = new Set < SchemaAST . AST > ( )
6967 const visited = new Set < SchemaAST . AST > ( )
7068 const shared = new Set < SchemaAST . AST > ( )
@@ -75,35 +73,82 @@ function lowerASTs(
7573
7674 return { representations, references }
7775
78- function generateReference ( prefix : string , owner : SchemaAST . AST ) : string {
76+ function getReference ( prefix : string , owner : SchemaAST . AST , separator = "_" ) : string {
7977 let candidate = prefix
8078 let suffix = 0
8179 while ( referenceOwners . has ( candidate ) ) {
82- candidate = `${ prefix } ${ ++ suffix } `
80+ if ( referenceOwners . get ( candidate ) === owner ) return candidate
81+ candidate = `${ prefix } ${ separator } ${ ++ suffix } `
8382 }
8483 referenceOwners . set ( candidate , owner )
8584 return candidate
8685 }
8786
87+ function getValueId ( value : unknown ) : number {
88+ if ( typeof value === "number" && globalThis . Number . isNaN ( value ) ) {
89+ return nextValueId ++
90+ }
91+ const found = valueIds . get ( value )
92+ if ( found !== undefined ) return found
93+ const id = nextValueId ++
94+ valueIds . set ( value , id )
95+ return id
96+ }
97+
98+ function getIdentityKey ( ast : SchemaAST . AST ) : string {
99+ let identity = ast . _tag
100+ for ( const [ key , value ] of Object . entries ( ast ) ) {
101+ if ( key !== "_tag" && key !== "context" ) identity += `:${ getValueId ( value ) } `
102+ }
103+ return identity
104+ }
105+
106+ function getCanonicalAST ( ast : SchemaAST . AST ) : SchemaAST . AST {
107+ const key = getIdentityKey ( ast )
108+ const canonical = canonicalByKey . get ( key )
109+ if ( canonical === undefined ) {
110+ canonicalByKey . set ( key , ast )
111+ return ast
112+ }
113+ return canonical
114+ }
115+
116+ function annotateReference (
117+ ast : SchemaAST . AST ,
118+ referenceIdentifier : ReferenceIdentifier ,
119+ reference : string
120+ ) : SchemaAST . AST {
121+ const fallback = referenceIdentifier . fallback
122+ if ( fallback !== undefined ) {
123+ return InternalAnnotations . resolveIdentifierFallback ( ast ) === fallback
124+ ? ast
125+ : SchemaAST . annotate ( ast , {
126+ [ InternalAnnotations . IDENTIFIER_FALLBACK_KEY ] : fallback
127+ } )
128+ }
129+ return reference === referenceIdentifier . identifier
130+ ? ast
131+ : SchemaAST . annotate ( ast , { identifier : reference } )
132+ }
133+
134+ function makeReference ( reference : string , ast : SchemaAST . AST ) : SchemaRepresentation . Reference {
135+ if ( ! Object . hasOwn ( references , reference ) && ! buildingReferences . has ( reference ) ) {
136+ buildingReferences . add ( reference )
137+ const representation = on ( ast )
138+ buildingReferences . delete ( reference )
139+ InternalRecord . assignProperty ( references , reference , representation )
140+ }
141+ return { _tag : "Reference" , $ref : reference }
142+ }
143+
88144 function visit ( input : SchemaAST . AST ) : void {
89145 const ast = SchemaAST . getLastEncoding ( input )
90- if ( visited . has ( ast ) ) {
91- if ( isShareable ( ast ) ) shared . add ( ast )
146+ const owner = getCanonicalAST ( ast )
147+ if ( visited . has ( owner ) ) {
148+ if ( isShareable ( ast ) ) shared . add ( owner )
92149 return
93150 }
94- visited . add ( ast )
95- const referenceIdentifier = resolveReferenceIdentifier ( ast )
96- if ( referenceIdentifier !== undefined && ! referenceIdentifier . isFallback ) {
97- const owner = referenceOwners . get ( referenceIdentifier . identifier )
98- if ( owner === undefined ) {
99- referenceOwners . set ( referenceIdentifier . identifier , ast )
100- } else if (
101- referenceMap . get ( ast ) !== referenceIdentifier . identifier &&
102- ! hasSameReferenceOwner ( owner , ast )
103- ) {
104- throw new Error ( `Duplicate identifier: ${ JSON . stringify ( referenceIdentifier . identifier ) } ` )
105- }
106- }
151+ visited . add ( owner )
107152 visitChecks ( ast . checks )
108153 switch ( ast . _tag ) {
109154 case "Declaration" :
@@ -131,45 +176,34 @@ function lowerASTs(
131176 } )
132177 }
133178
134- function recur ( ast : SchemaAST . AST ) : SchemaRepresentation . Representation {
135- const found = referenceMap . get ( ast )
136- if ( found !== undefined ) {
137- return { _tag : "Reference" , $ref : found }
138- }
139-
140- const projected = SchemaAST . getLastEncoding ( ast )
141- if ( projected !== ast ) {
142- return recur ( projected )
143- }
144-
145- const referenceIdentifier = resolveReferenceIdentifier ( ast )
179+ function recur ( input : SchemaAST . AST ) : SchemaRepresentation . Representation {
180+ const ast = SchemaAST . getLastEncoding ( input )
181+ const owner = getCanonicalAST ( ast )
182+ const referenceIdentifier = resolveReferenceIdentifier ( input , ast )
146183 if ( referenceIdentifier !== undefined ) {
147- const reference = getReference ( referenceIdentifier , ast )
148- referenceMap . set ( ast , reference )
149- if ( ! Object . hasOwn ( references , reference ) ) {
150- InternalRecord . assignProperty ( references , reference , on ( ast ) )
151- }
152- return { _tag : "Reference" , $ref : reference }
184+ const reference = getReference ( referenceIdentifier . identifier , owner )
185+ return makeReference ( reference , annotateReference ( ast , referenceIdentifier , reference ) )
153186 }
154187
155- if ( shared . has ( ast ) ) {
156- const reference = generateReference ( `${ ast . _tag } _` , ast )
157- referenceMap . set ( ast , reference )
158- InternalRecord . assignProperty ( references , reference , on ( ast ) )
159- return { _tag : "Reference" , $ref : reference }
188+ const found = anonymousReferences . get ( owner )
189+ if ( found !== undefined ) {
190+ return { _tag : "Reference" , $ref : found }
160191 }
161192
162- if ( visiting . has ( ast ) ) {
163- const reference = generateReference ( `${ ast . _tag } _` , ast )
164- referenceMap . set ( ast , reference )
165- return { _tag : "Reference" , $ref : reference }
193+ const isShared = shared . has ( owner )
194+ if ( isShared || visiting . has ( owner ) ) {
195+ const reference = getReference ( `${ ast . _tag } _` , owner , "" )
196+ anonymousReferences . set ( owner , reference )
197+ return isShared
198+ ? makeReference ( reference , ast )
199+ : { _tag : "Reference" , $ref : reference }
166200 }
167201
168- visiting . add ( ast )
202+ visiting . add ( owner )
169203 const representation = on ( ast )
170- visiting . delete ( ast )
204+ visiting . delete ( owner )
171205
172- const reference = referenceMap . get ( ast )
206+ const reference = anonymousReferences . get ( owner )
173207 if ( reference !== undefined ) {
174208 InternalRecord . assignProperty ( references , reference , representation )
175209 return { _tag : "Reference" , $ref : reference }
@@ -178,18 +212,6 @@ function lowerASTs(
178212 return representation
179213 }
180214
181- function getReference ( referenceIdentifier : ReferenceIdentifier , ast : SchemaAST . AST ) : string {
182- if ( ! referenceIdentifier . isFallback ) {
183- return referenceIdentifier . identifier
184- }
185- for ( const [ owner , reference ] of fallbackReferences ) {
186- if ( hasSameReferenceOwner ( owner , ast ) ) return reference
187- }
188- const reference = generateReference ( referenceIdentifier . identifier , ast )
189- fallbackReferences . push ( [ ast , reference ] )
190- return reference
191- }
192-
193215 function on ( ast : SchemaAST . AST ) : SchemaRepresentation . Representation {
194216 const checks = fromChecks ( ast . checks )
195217 switch ( ast . _tag ) {
0 commit comments