microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-diff-highlight-issue

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/asset-emitter/src/asset-emitter.ts

978lines · modecode

1import {
2 type EmitContext,
3 type Model,
4 type Namespace,
5 type Program,
6 type Type,
7 compilerAssert,
8 getTypeName,
9 isTemplateDeclaration,
10 joinPaths,
11} from "@typespec/compiler";
12import { $ } from "@typespec/compiler/typekit";
13import { CustomKeyMap } from "./custom-key-map.js";
14import { Placeholder } from "./placeholder.js";
15import { resolveDeclarationReferenceScope } from "./ref-scope.js";
16import { ReferenceCycle } from "./reference-cycle.js";
17import { TypeEmitter } from "./type-emitter.js";
18import {
19 type AssetEmitter,
20 CircularEmit,
21 type ContextState,
22 Declaration,
23 type EmitEntity,
24 type EmitTypeReferenceOptions,
25 EmitterResult,
26 type EmitterState,
27 type LexicalTypeStackEntry,
28 type NamespaceScope,
29 NoEmit,
30 RawCode,
31 type Scope,
32 type SourceFile,
33 type SourceFileScope,
34 type TypeEmitterMethod,
35 type TypeSpecDeclaration,
36} from "./types.js";
37
38/**
39 * Represent an entry in the reference chain.
40 */
41interface ReferenceChainEntry {
42 method: string;
43 type: Type;
44 context: ContextState;
45}
46
47export function createAssetEmitter<T, TOptions extends object>(
48 program: Program,
49 TypeEmitterClass: typeof TypeEmitter<T, TOptions>,
50 emitContext: EmitContext<TOptions>,
51): AssetEmitter<T, TOptions> {
52 const sourceFiles: SourceFile<T>[] = [];
53
54 const options = {
55 noEmit: program.compilerOptions.dryRun ?? false,
56 emitterOutputDir: emitContext.emitterOutputDir,
57 ...emitContext.options,
58 };
59 const typeId = CustomKeyMap.objectKeyer();
60 const contextId = CustomKeyMap.objectKeyer();
61 const entryId = CustomKeyMap.objectKeyer();
62
63 // This is effectively a seen set, ensuring that we don't emit the same
64 // type with the same context twice. So the map stores a triple of:
65 //
66 // 1. the method of TypeEmitter we would call
67 // 2. the tsp type we're emitting.
68 // 3. the current context.
69 //
70 // Note that in order for this to work, context needs to be interned so
71 // contexts with the same values inside are treated as identical in the
72 // map. See createInterner for more details.
73 const typeToEmitEntity = new CustomKeyMap<[string, Type, ContextState], EmitEntity<T>>(
74 ([method, type, context]) => {
75 return `${method}-${typeId.getKey(type)}-${contextId.getKey(context)}`;
76 },
77 );
78
79 // When we encounter a circular reference, this map will hold a callback
80 // that should be called when the circularly referenced type has completed
81 // its emit.
82 const waitingCircularRefs = new CustomKeyMap<
83 [string, Type, ContextState],
84 {
85 state: EmitterState;
86 cb: (entity: EmitEntity<T>) => EmitEntity<T>;
87 }[]
88 >(([method, type, context]) => {
89 return `${method}-${typeId.getKey(type)}-${contextId.getKey(context)}`;
90 });
91
92 // Similar to `typeToEmitEntity`, this ensures we don't recompute context
93 // for types that we already have context for. Note that context is
94 // dependent on the context of the context call, e.g. if a model is
95 // referenced with reference context set we need to get its declaration
96 // context again. So we use the context's context as a key. Context must
97 // be interned, see createInterner for more details.
98 const knownContexts = new CustomKeyMap<[LexicalTypeStackEntry, ContextState], ContextState>(
99 ([entry, context]) => {
100 return `${entryId.getKey(entry)}-${contextId.getKey(context)}`;
101 },
102 );
103
104 // The stack of types that the currently emitted type is lexically
105 // contained in. This gets pushed to when we visit a type that is
106 // lexically contained in the current type, and is reset when we jump via
107 // reference to another type in a different lexical context. Note that
108 // this does not correspond to tsp's lexical nesting, e.g. in the case of
109 // an alias to a model expression, the alias is lexically outside the
110 // model, but in the type graph we will consider it to be lexically inside
111 // whatever references the alias.
112 let lexicalTypeStack: LexicalTypeStackEntry[] = [];
113 let referenceTypeChain: ReferenceChainEntry[] = [];
114
115 // Internally, context is is split between lexicalContext and
116 // referenceContext because when a reference is made, we carry over
117 // referenceContext but leave lexical context behind. When context is
118 // accessed by the user, they are merged by getContext().
119 let context: ContextState = {
120 lexicalContext: {},
121 referenceContext: {},
122 };
123 let programContext: ContextState | null = null;
124
125 // Incoming reference context is reference context that comes from emitting a
126 // type reference. Incoming reference context is only set on the
127 // incomingReferenceContextTarget and types lexically contained within it. For
128 // example, when referencing a model with reference context set, we may need
129 // to get context from the referenced model's namespaces, and such namespaces
130 // will not see the reference context. However, the reference context will be
131 // available for the model, its properties, and any types nested within it
132 // (e.g. anonymous models).
133 let incomingReferenceContext: Record<string, string> | null = null;
134 let incomingReferenceContextTarget: Type | null = null;
135 const stateInterner = createInterner();
136 const stackEntryInterner = createInterner();
137
138 const assetEmitter: AssetEmitter<T, TOptions> = {
139 getContext() {
140 return {
141 ...context.lexicalContext,
142 ...context.referenceContext,
143 };
144 },
145
146 getOptions() {
147 return options;
148 },
149
150 getProgram() {
151 return program;
152 },
153
154 result: {
155 declaration(name, value) {
156 const scope = currentScope();
157 compilerAssert(
158 scope,
159 "Emit context must have a scope set in order to create declarations. Consider setting scope to a new source file's global scope in the `programContext` method of `TypeEmitter`.",
160 );
161 return new Declaration(name, scope, value);
162 },
163 rawCode(value) {
164 return new RawCode(value);
165 },
166 none() {
167 return new NoEmit();
168 },
169 },
170 createScope(block, name, parentScope: Scope<T> | null = null) {
171 let newScope: Scope<T>;
172 if (!parentScope) {
173 // create source file scope
174 newScope = {
175 kind: "sourceFile",
176 name,
177 sourceFile: block,
178 parentScope,
179 childScopes: [],
180 declarations: [],
181 } as SourceFileScope<T>;
182 } else {
183 newScope = {
184 kind: "namespace",
185 name,
186 namespace: block,
187 childScopes: [],
188 declarations: [],
189 parentScope,
190 } as NamespaceScope<T>;
191 }
192
193 parentScope?.childScopes.push(newScope);
194 return newScope as any; // the overload of createScope causes type weirdness
195 },
196
197 createSourceFile(path): SourceFile<T> {
198 const basePath = options.emitterOutputDir;
199 const sourceFile = {
200 globalScope: undefined as any,
201 path: joinPaths(basePath, path),
202 imports: new Map(),
203 meta: {},
204 };
205 sourceFile.globalScope = this.createScope(sourceFile, "");
206 sourceFiles.push(sourceFile);
207 return sourceFile;
208 },
209
210 emitTypeReference(target, options?: EmitTypeReferenceOptions): EmitEntity<T> {
211 return withPatchedReferenceContext(options?.referenceContext, () => {
212 const oldIncomingReferenceContext = incomingReferenceContext;
213 const oldIncomingReferenceContextTarget = incomingReferenceContextTarget;
214
215 incomingReferenceContext = context.referenceContext ?? null;
216 incomingReferenceContextTarget = incomingReferenceContext ? target : null;
217
218 let result;
219 if (target.kind === "ModelProperty") {
220 result = invokeTypeEmitter("modelPropertyReference", target);
221 } else if (target.kind === "EnumMember") {
222 result = invokeTypeEmitter("enumMemberReference", target);
223 }
224
225 if (result) {
226 incomingReferenceContext = oldIncomingReferenceContext;
227 incomingReferenceContextTarget = oldIncomingReferenceContextTarget;
228 return result;
229 }
230
231 const entity = this.emitType(target);
232
233 incomingReferenceContext = oldIncomingReferenceContext;
234 incomingReferenceContextTarget = oldIncomingReferenceContextTarget;
235
236 let placeholder: Placeholder<T> | null = null;
237
238 if (entity.kind === "circular") {
239 let waiting = waitingCircularRefs.get(entity.emitEntityKey);
240 if (!waiting) {
241 waiting = [];
242 waitingCircularRefs.set(entity.emitEntityKey, waiting);
243 }
244
245 const typeChainSnapshot = referenceTypeChain;
246 waiting.push({
247 state: {
248 lexicalTypeStack,
249 context,
250 },
251 cb: (resolvedEntity) =>
252 invokeReference(
253 this,
254 resolvedEntity,
255 true,
256 resolveReferenceCycle(typeChainSnapshot, entity, typeToEmitEntity as any),
257 ),
258 });
259
260 placeholder = new Placeholder();
261 return this.result.rawCode(placeholder);
262 } else {
263 return invokeReference(this, entity, false);
264 }
265
266 function invokeReference(
267 assetEmitter: AssetEmitter<T, TOptions>,
268 entity: EmitEntity<T>,
269 circular: boolean,
270 cycle?: ReferenceCycle,
271 ): EmitEntity<T> {
272 let ref;
273 const scope = currentScope();
274
275 if (circular) {
276 ref = typeEmitter.circularReference(entity, scope, cycle!);
277 } else {
278 if (entity.kind !== "declaration") {
279 return entity;
280 }
281 compilerAssert(
282 scope,
283 "Emit context must have a scope set in order to create references to declarations.",
284 );
285 const { pathUp, pathDown, commonScope } = resolveDeclarationReferenceScope(
286 entity,
287 scope,
288 );
289 ref = typeEmitter.reference(entity, pathUp, pathDown, commonScope);
290 }
291
292 if (!(ref instanceof EmitterResult)) {
293 ref = assetEmitter.result.rawCode(ref) as RawCode<T>;
294 }
295
296 if (placeholder) {
297 // this should never happen as this function shouldn't be called until
298 // the target declaration is finished being emitted.
299 compilerAssert(
300 ref.kind !== "circular",
301 "TypeEmitter `reference` returned circular emit",
302 );
303
304 // this could presumably be allowed if we want.
305 compilerAssert(
306 ref.kind === "none" || !(ref.value instanceof Placeholder),
307 "TypeEmitter's `reference` method cannot return a placeholder.",
308 );
309
310 switch (ref.kind) {
311 case "code":
312 case "declaration":
313 placeholder.setValue(ref.value as T);
314 break;
315 case "none":
316 // this cast is incorrect, think about what should happen
317 // if reference returns noEmit...
318 placeholder.setValue("" as T);
319 break;
320 }
321 }
322
323 return ref;
324 }
325 });
326 },
327
328 emitDeclarationName(type): string | undefined {
329 return typeEmitter.declarationName!(type);
330 },
331
332 async writeOutput() {
333 return typeEmitter.writeOutput(sourceFiles);
334 },
335
336 getSourceFiles() {
337 return sourceFiles;
338 },
339
340 emitType(type, context?: ContextState) {
341 if (context?.referenceContext) {
342 incomingReferenceContext = context?.referenceContext ?? incomingReferenceContext;
343 incomingReferenceContextTarget = type ?? incomingReferenceContextTarget;
344 }
345
346 const declName =
347 isDeclaration(type) && type.kind !== "Namespace" ? typeEmitter.declarationName(type) : null;
348 const key = typeEmitterKey(type);
349 let args: any[];
350 switch (key) {
351 case "scalarDeclaration":
352 case "scalarInstantiation":
353 case "modelDeclaration":
354 case "modelInstantiation":
355 case "operationDeclaration":
356 case "interfaceDeclaration":
357 case "interfaceOperationDeclaration":
358 case "enumDeclaration":
359 case "unionDeclaration":
360 case "unionInstantiation":
361 args = [declName];
362 break;
363
364 case "arrayDeclaration":
365 const arrayDeclElement = (type as Model).indexer!.value;
366 args = [declName, arrayDeclElement];
367 break;
368 case "arrayLiteral":
369 const arrayLiteralElement = (type as Model).indexer!.value;
370 args = [arrayLiteralElement];
371 break;
372 case "intrinsic":
373 args = [declName];
374 break;
375 default:
376 args = [];
377 }
378
379 const result = (invokeTypeEmitter as any)(key, type, ...args);
380
381 return result;
382 },
383
384 emitProgram(options) {
385 const namespace = program.getGlobalNamespaceType();
386 if (options?.emitGlobalNamespace) {
387 this.emitType(namespace);
388 return;
389 }
390
391 for (const ns of namespace.namespaces.values()) {
392 if (ns.name === "TypeSpec" && !options?.emitTypeSpecNamespace) continue;
393 this.emitType(ns);
394 }
395
396 for (const model of namespace.models.values()) {
397 if (!isTemplateDeclaration(model)) {
398 this.emitType(model);
399 }
400 }
401
402 for (const operation of namespace.operations.values()) {
403 if (!isTemplateDeclaration(operation)) {
404 this.emitType(operation);
405 }
406 }
407
408 for (const enumeration of namespace.enums.values()) {
409 this.emitType(enumeration);
410 }
411
412 for (const union of namespace.unions.values()) {
413 if (!isTemplateDeclaration(union)) {
414 this.emitType(union);
415 }
416 }
417
418 for (const iface of namespace.interfaces.values()) {
419 if (!isTemplateDeclaration(iface)) {
420 this.emitType(iface);
421 }
422 }
423
424 for (const scalar of namespace.scalars.values()) {
425 this.emitType(scalar);
426 }
427 },
428
429 emitModelProperties(model) {
430 const res = invokeTypeEmitter("modelProperties", model);
431 if (res instanceof EmitterResult) {
432 return res as any;
433 } else {
434 return this.result.rawCode(res);
435 }
436 },
437
438 emitModelProperty(property) {
439 return invokeTypeEmitter("modelPropertyLiteral", property);
440 },
441
442 emitOperationParameters(operation) {
443 return invokeTypeEmitter("operationParameters", operation, operation.parameters);
444 },
445
446 emitOperationReturnType(operation) {
447 return invokeTypeEmitter("operationReturnType", operation, operation.returnType);
448 },
449
450 emitInterfaceOperations(iface) {
451 return invokeTypeEmitter("interfaceDeclarationOperations", iface);
452 },
453
454 emitInterfaceOperation(operation) {
455 const name = typeEmitter.declarationName(operation);
456 if (name === undefined) {
457 // the general approach of invoking the expression form doesn't work here
458 // because TypeSpec doesn't have operation expressions.
459 compilerAssert(false, "Unnamed operations are not supported");
460 }
461 return invokeTypeEmitter("interfaceOperationDeclaration", operation, name);
462 },
463
464 emitEnumMembers(en) {
465 return invokeTypeEmitter("enumMembers", en);
466 },
467
468 emitUnionVariants(union) {
469 return invokeTypeEmitter("unionVariants", union);
470 },
471
472 emitTupleLiteralValues(tuple) {
473 return invokeTypeEmitter("tupleLiteralValues", tuple);
474 },
475
476 async emitSourceFile(sourceFile) {
477 return await typeEmitter.sourceFile(sourceFile);
478 },
479 };
480
481 const typeEmitter = new TypeEmitterClass(assetEmitter);
482 return assetEmitter;
483
484 /**
485 * This function takes care of calling a method on the TypeEmitter to
486 * convert it to some emitted output. It will return a cached type if we
487 * have seen it before (and the context is the same). It will establish
488 * the emit context by calling the appropriate methods before getting the
489 * emit result. Also if a type emitter returns just a T or a
490 * Placeholder<T>, it will convert that to a RawCode result.
491 */
492 function invokeTypeEmitter<TMethod extends TypeEmitterMethod>(
493 method: TMethod,
494 ...args: Parameters<TypeEmitter<T, TOptions>[TMethod]>
495 ): EmitEntity<T> {
496 const type = args[0];
497 let entity: EmitEntity<T>;
498 let emitEntityKey: [string, Type, ContextState];
499 let cached = false;
500
501 withTypeContext(method, args, () => {
502 emitEntityKey = [method, type, context];
503 const seenEmitEntity = typeToEmitEntity.get(emitEntityKey);
504
505 if (seenEmitEntity) {
506 entity = seenEmitEntity;
507 cached = true;
508 return;
509 }
510
511 typeToEmitEntity.set(emitEntityKey, new CircularEmit(emitEntityKey));
512 compilerAssert(typeEmitter[method], `TypeEmitter doesn't have a method named ${method}.`);
513 entity = liftToRawCode((typeEmitter[method] as any)(...args));
514 });
515
516 if (cached) {
517 return entity!;
518 }
519
520 if (entity! instanceof Placeholder) {
521 entity.onValue((v) => handleCompletedEntity(v));
522 return entity;
523 }
524
525 handleCompletedEntity(entity!);
526
527 return entity!;
528
529 function handleCompletedEntity(entity: EmitEntity<T>) {
530 typeToEmitEntity.set(emitEntityKey!, entity!);
531 const waitingRefCbs = waitingCircularRefs.get(emitEntityKey!);
532 if (waitingRefCbs) {
533 for (const record of waitingRefCbs) {
534 withContext(record.state, () => {
535 record.cb(entity);
536 });
537 }
538 waitingCircularRefs.set(emitEntityKey!, []);
539 }
540
541 if (entity!.kind === "declaration") {
542 entity!.scope.declarations.push(entity!);
543 }
544 }
545
546 function liftToRawCode(value: EmitEntity<T> | Placeholder<T> | T): EmitEntity<T> {
547 if (value instanceof EmitterResult) {
548 return value;
549 }
550
551 return assetEmitter.result.rawCode(value);
552 }
553 }
554
555 function isInternalMethod(
556 method: TypeEmitterMethod,
557 ): method is Exclude<
558 TypeEmitterMethod,
559 | "interfaceDeclarationOperations"
560 | "interfaceOperationDeclaration"
561 | "operationParameters"
562 | "operationReturnType"
563 | "modelProperties"
564 | "enumMembers"
565 | "tupleLiteralValues"
566 | "unionVariants"
567 > {
568 return (
569 method === "interfaceDeclarationOperations" ||
570 method === "interfaceOperationDeclaration" ||
571 method === "operationParameters" ||
572 method === "operationReturnType" ||
573 method === "modelProperties" ||
574 method === "enumMembers" ||
575 method === "tupleLiteralValues" ||
576 method === "unionVariants"
577 );
578 }
579 /**
580 * This helper takes a type and sets the `context` state to what it should
581 * be in order to invoke the type emitter method for that type. This needs
582 * to take into account the current context and any incoming reference
583 * context.
584 */
585 function setContextForType<TMethod extends TypeEmitterMethod>(
586 method: TMethod,
587 args: Parameters<TypeEmitter<T, TOptions>[TMethod]>,
588 ) {
589 const type = args[0];
590 let newTypeStack: LexicalTypeStackEntry[];
591
592 // Check if this is an unspeakable template instantiation (name is undefined).
593 // Unspeakable instantiations should not reset the type stack because they are
594 // emitted inline and need to maintain the outer scope for reference resolution.
595 const isUnspeakableInstantiation =
596 (method === "modelInstantiation" || method === "unionInstantiation") && args[1] === undefined;
597
598 // if we've walked into a new declaration, reset the lexical type stack
599 // to the lexical containers of the current type.
600 if (
601 isDeclaration(type) &&
602 type.kind !== "Intrinsic" &&
603 !isInternalMethod(method) &&
604 !isUnspeakableInstantiation
605 ) {
606 newTypeStack = [stackEntryInterner.intern({ method, args: stackEntryInterner.intern(args) })];
607 let ns = type.namespace;
608 while (ns) {
609 if (ns.name === "") break;
610 newTypeStack.unshift(
611 stackEntryInterner.intern({ method: "namespace", args: stackEntryInterner.intern([ns]) }),
612 );
613 ns = ns.namespace;
614 }
615 } else {
616 newTypeStack = [
617 ...lexicalTypeStack,
618 stackEntryInterner.intern({ method, args: stackEntryInterner.intern(args) }),
619 ];
620 }
621
622 lexicalTypeStack = newTypeStack;
623
624 if (!programContext) {
625 programContext = stateInterner.intern({
626 lexicalContext: typeEmitter.programContext(program),
627 referenceContext: stateInterner.intern({}),
628 });
629 }
630
631 // Establish our context by starting from program and walking up the type stack
632 // and merging in context for each of the lexical containers.
633 context = programContext;
634
635 for (const entry of lexicalTypeStack) {
636 if (incomingReferenceContext && entry.args[0] === incomingReferenceContextTarget) {
637 // bring in any reference context so it is available for any types nested beneath this type.
638 context = stateInterner.intern({
639 lexicalContext: context.lexicalContext,
640 referenceContext: stateInterner.intern({
641 ...context.referenceContext,
642 ...incomingReferenceContext,
643 }),
644 });
645 }
646
647 const seenContext = knownContexts.get([entry, context]);
648 if (seenContext) {
649 context = seenContext;
650 continue;
651 }
652
653 const lexicalKey = entry.method + "Context";
654 const referenceKey = entry.method + "ReferenceContext";
655
656 if (keyHasContext(entry.method)) {
657 compilerAssert(
658 (typeEmitter as any)[lexicalKey],
659 `TypeEmitter doesn't have a method named ${lexicalKey}`,
660 );
661 }
662
663 if (keyHasReferenceContext(entry.method)) {
664 compilerAssert(
665 (typeEmitter as any)[referenceKey],
666 `TypeEmitter doesn't have a method named ${referenceKey}`,
667 );
668 }
669
670 const newContext = keyHasContext(entry.method)
671 ? (typeEmitter as any)[lexicalKey](...entry.args)
672 : {};
673
674 const newReferenceContext = keyHasReferenceContext(entry.method)
675 ? (typeEmitter as any)[referenceKey](...entry.args)
676 : {};
677
678 // assemble our new reference and lexical contexts.
679 const newContextState = stateInterner.intern({
680 lexicalContext: stateInterner.intern({
681 ...context.lexicalContext,
682 ...newContext,
683 }),
684 referenceContext: stateInterner.intern({
685 ...context.referenceContext,
686 ...newReferenceContext,
687 }),
688 });
689
690 knownContexts.set([entry, context], newContextState);
691 context = newContextState;
692 }
693
694 if (!isInternalMethod(method)) {
695 referenceTypeChain = [
696 ...referenceTypeChain,
697 stackEntryInterner.intern({
698 method,
699 type,
700 context,
701 }),
702 ];
703 }
704 }
705
706 /**
707 * Invoke the callback with the proper context for a given type.
708 */
709 function withTypeContext<TMethod extends TypeEmitterMethod>(
710 method: TMethod,
711 args: Parameters<TypeEmitter<T, TOptions>[TMethod]>,
712 cb: () => void,
713 ) {
714 const oldContext = context;
715 const oldTypeStack = lexicalTypeStack;
716 const oldRefTypeStack = referenceTypeChain;
717
718 setContextForType(method, args);
719
720 cb();
721
722 context = oldContext;
723 lexicalTypeStack = oldTypeStack;
724 referenceTypeChain = oldRefTypeStack;
725 }
726
727 function withPatchedReferenceContext<T>(
728 referenceContext: Record<string, any> | undefined,
729 cb: () => T,
730 ): T {
731 if (referenceContext !== undefined) {
732 const oldContext = context;
733
734 context = stateInterner.intern({
735 lexicalContext: context.lexicalContext,
736 referenceContext: stateInterner.intern({
737 ...context.referenceContext,
738 ...referenceContext,
739 }),
740 });
741
742 const result = cb();
743 context = oldContext;
744 return result;
745 } else {
746 return cb();
747 }
748 }
749
750 /**
751 * Invoke the callback with the given context.
752 */
753 function withContext(newContext: EmitterState, cb: () => void) {
754 const oldContext = context;
755 const oldTypeStack = lexicalTypeStack;
756 context = newContext.context;
757 lexicalTypeStack = newContext.lexicalTypeStack;
758
759 cb();
760
761 context = oldContext;
762 lexicalTypeStack = oldTypeStack;
763 }
764
765 function typeEmitterKey(type: Type) {
766 switch (type.kind) {
767 case "Model":
768 if ($(program).array.is(type) && type.name === "Array") {
769 // likely an array literal, though could be a bare reference to Array maybe?
770 return "arrayLiteral";
771 }
772
773 if (type.name === "") {
774 return "modelLiteral";
775 }
776
777 if (type.templateMapper) {
778 return "modelInstantiation";
779 }
780
781 if (type.indexer && type.indexer.key!.name === "integer") {
782 return "arrayDeclaration";
783 }
784
785 return "modelDeclaration";
786
787 case "Namespace":
788 return "namespace";
789 case "ModelProperty":
790 return "modelPropertyLiteral";
791 case "StringTemplate":
792 return "stringTemplate";
793 case "Boolean":
794 return "booleanLiteral";
795 case "String":
796 return "stringLiteral";
797 case "Number":
798 return "numericLiteral";
799 case "Operation":
800 if (type.interface) {
801 return "interfaceOperationDeclaration";
802 } else {
803 return "operationDeclaration";
804 }
805 case "Interface":
806 return "interfaceDeclaration";
807 case "Enum":
808 return "enumDeclaration";
809 case "EnumMember":
810 return "enumMember";
811 case "Union":
812 if (!type.name) {
813 return "unionLiteral";
814 }
815
816 if (type.templateMapper) {
817 return "unionInstantiation";
818 }
819
820 return "unionDeclaration";
821 case "UnionVariant":
822 return "unionVariant";
823 case "Tuple":
824 return "tupleLiteral";
825 case "Scalar":
826 if (type.templateMapper) {
827 return "scalarInstantiation";
828 } else {
829 return "scalarDeclaration";
830 }
831
832 case "Intrinsic":
833 return "intrinsic";
834 default:
835 compilerAssert(false, `Encountered type ${type.kind} which we don't know how to emit.`);
836 }
837 }
838 function currentScope() {
839 return context.referenceContext?.scope ?? context.lexicalContext?.scope ?? null;
840 }
841}
842
843/**
844 * Returns true if the given type is a declaration or an instantiation of a declaration.
845 * @param type
846 * @returns
847 */
848function isDeclaration(type: Type): type is TypeSpecDeclaration | Namespace {
849 switch (type.kind) {
850 case "Namespace":
851 case "Interface":
852 case "Enum":
853 case "Operation":
854 case "Scalar":
855 case "Intrinsic":
856 return true;
857
858 case "Model":
859 return type.name ? type.name !== "" && type.name !== "Array" : false;
860 case "Union":
861 return type.name ? type.name !== "" : false;
862 default:
863 return false;
864 }
865}
866
867/**
868 * An interner takes an object and returns either that same object, or a
869 * previously seen object that has the identical shape.
870 */
871function createInterner() {
872 type PlainObject = Record<string, any>;
873 const emptyObject = {};
874 // Root map: key = property count, value = Map of property names
875 const root = new Map();
876
877 function intern<T extends PlainObject>(object: T): T {
878 if (object === null || typeof object !== "object") return object;
879 const keys = Object.keys(object);
880 if (keys.length === 0) return emptyObject as any;
881
882 // Use property count as first-level key for efficiency
883 let node = root.get(keys.length);
884 if (!node) {
885 node = new Map();
886 root.set(keys.length, node);
887 }
888
889 // Sort keys for stable structure
890 const sortedKeys = keys.sort();
891 let curr = node;
892 for (const key of sortedKeys) {
893 if (!curr.has(key)) curr.set(key, new Map());
894 curr = curr.get(key);
895 }
896
897 // Now curr is a map from values to interned objects
898 // Use WeakMap for object values, Map for primitives
899 let valueNode = curr.valueNode;
900 if (!valueNode) {
901 valueNode = new Map();
902 curr.valueNode = valueNode;
903 }
904
905 // Build a tuple of values for this key order
906 const values = sortedKeys.map((k) => object[k]);
907 let leaf = valueNode;
908 for (let i = 0; i < values.length; i++) {
909 const v = values[i];
910 const isObj = v && typeof v === "object";
911 let next;
912 if (isObj) {
913 if (!leaf.has("obj")) leaf.set("obj", new WeakMap());
914 next = leaf.get("obj");
915 if (!next.has(v)) next.set(v, new Map());
916 next = next.get(v);
917 } else {
918 if (!leaf.has("prim")) leaf.set("prim", new Map());
919 next = leaf.get("prim");
920 if (!next.has(v)) next.set(v, new Map());
921 next = next.get(v);
922 }
923 leaf = next;
924 }
925
926 // At the leaf, check for existing interned object
927 if (leaf.has("interned")) {
928 return leaf.get("interned");
929 }
930 leaf.set("interned", object);
931 return object;
932 }
933
934 return { intern };
935}
936
937const noContext = new Set<string>(["modelPropertyReference", "enumMemberReference"]);
938
939function keyHasContext(key: keyof TypeEmitter<any, any>) {
940 return !noContext.has(key);
941}
942const noReferenceContext = new Set<string>([
943 ...noContext,
944 "booleanLiteral",
945 "stringTemplate",
946 "stringLiteral",
947 "numericLiteral",
948 "scalarInstantiation",
949 "enumMember",
950 "enumMembers",
951 "intrinsic",
952]);
953
954function keyHasReferenceContext(key: keyof TypeEmitter<any, any>): boolean {
955 return !noReferenceContext.has(key);
956}
957
958function resolveReferenceCycle(
959 stack: ReferenceChainEntry[],
960 entity: CircularEmit,
961 typeToEmitEntity: CustomKeyMap<[string, Type, ContextState], EmitEntity<unknown>>,
962): ReferenceCycle {
963 for (let i = stack.length - 1; i >= 0; i--) {
964 if (stack[i].type === entity.emitEntityKey[1]) {
965 return new ReferenceCycle(
966 stack.slice(i).map((x) => {
967 return {
968 type: x.type,
969 entity: typeToEmitEntity.get([x.method, x.type, x.context])!,
970 };
971 }),
972 );
973 }
974 }
975 throw new Error(
976 `Couldn't resolve the circular reference stack for ${getTypeName(entity.emitEntityKey[1])}`,
977 );
978}
979