From 12810a6a80a7aac986d17a85da05840fb894525f Mon Sep 17 00:00:00 2001 From: Christopher Radek Date: Wed, 9 Apr 2025 13:32:18 -0700 Subject: [PATCH 1/4] resolve default template parameters from parent containers --- packages/compiler/src/core/checker.ts | 19 ++++++++--- .../compiler/test/checker/templates.test.ts | 34 ++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 583946d9bf0..fada7be7038 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -947,7 +947,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } } - return mapper ? mapper.getMappedType(type) : type; + if (mapper) { + let mappedType = mapper.getMappedType(type); + // recursively resolve the mapped type so long as it is + // still a TemplateParameter, since there may be multiple + // levels of mapping (e.g. when referencing defaults) + while ( + mappedType.entityKind === "Type" && + mappedType.kind === "TemplateParameter" && + mapper.getMappedType(mappedType) !== mappedType + ) { + mappedType = mapper.getMappedType(mappedType); + } + return mappedType; + } + return type; } function getResolvedTypeParameterDefault( @@ -4316,9 +4330,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type.name, SymbolFlags.Interface | SymbolFlags.LateBound, ); - if (isTemplateInstance(type) && type.name === "Foo") { - getSymbolLinks(type.symbol); - } mutate(type.symbol).type = type; break; case "Union": diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index a0e0068895f..6289ea519a4 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -1,7 +1,7 @@ import { deepStrictEqual, fail, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { getSourceLocation } from "../../src/core/diagnostics.js"; -import { Diagnostic, Model, StringLiteral, Type } from "../../src/core/types.js"; +import { Diagnostic, Model, Operation, StringLiteral, Type } from "../../src/core/types.js"; import { isUnknownType } from "../../src/index.js"; import { BasicTestRunner, @@ -400,6 +400,38 @@ describe("compiler: templates", () => { strictEqual(t.value, "bye"); }); + it("can reference parent parameters in default", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test interface A { + op foo(): U; + } + alias B = A; + @test op MyOp is B.foo; + `, + ); + const { MyOp } = (await testHost.compile("main.tsp")) as { MyOp: Operation }; + strictEqual(MyOp.returnType.kind, "Scalar"); + strictEqual(MyOp.returnType.name, "string"); + }); + + it("can override default provided by parent parameters", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test interface A { + op foo(): U; + } + alias B = A; + @test op MyOp is B.foo; + `, + ); + const { MyOp } = (await testHost.compile("main.tsp")) as { MyOp: Operation }; + strictEqual(MyOp.returnType.kind, "Scalar"); + strictEqual(MyOp.returnType.name, "bytes"); + }); + it("emit diagnostics if referencing itself", async () => { testHost.addTypeSpecFile( "main.tsp", From db7dcd97c7bf4550ca7886f0167b657fdbcb5cf3 Mon Sep 17 00:00:00 2001 From: Christopher Radek Date: Wed, 9 Apr 2025 13:53:58 -0700 Subject: [PATCH 2/4] add changelog --- ...-templ-param-alias-is-2025-3-9-13-34-54.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .chronus/changes/fix-templ-param-alias-is-2025-3-9-13-34-54.md diff --git a/.chronus/changes/fix-templ-param-alias-is-2025-3-9-13-34-54.md b/.chronus/changes/fix-templ-param-alias-is-2025-3-9-13-34-54.md new file mode 100644 index 00000000000..cf8ac677542 --- /dev/null +++ b/.chronus/changes/fix-templ-param-alias-is-2025-3-9-13-34-54.md @@ -0,0 +1,22 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Fixes template argument resolution when a default template parameter value is resolved by a parent container (e.g. interface) +For example: +```tsp +interface Resource { + read(): U; +} + +model Foo { + type: "foo"; +} + +alias FooResource = Resource; + +op readFoo is FooResource.read; +``` +The `returnType` for `readFoo` would be model `Foo`. Previously the `returnType` resolved to a `TemplateParameter`. From fdecc93cf98f5ba44db3bf9c962d67d81615755d Mon Sep 17 00:00:00 2001 From: Christopher Radek Date: Fri, 11 Apr 2025 12:04:02 -0700 Subject: [PATCH 3/4] revise strategy --- packages/compiler/src/core/checker.ts | 25 +++++++------------ .../compiler/test/checker/templates.test.ts | 6 ++++- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fada7be7038..f1a86918ad1 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -947,21 +947,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } } - if (mapper) { - let mappedType = mapper.getMappedType(type); - // recursively resolve the mapped type so long as it is - // still a TemplateParameter, since there may be multiple - // levels of mapping (e.g. when referencing defaults) - while ( - mappedType.entityKind === "Type" && - mappedType.kind === "TemplateParameter" && - mapper.getMappedType(mappedType) !== mappedType - ) { - mappedType = mapper.getMappedType(mappedType); - } - return mappedType; - } - return type; + return mapper ? mapper.getMappedType(type) : type; } function getResolvedTypeParameterDefault( @@ -1148,6 +1134,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker args: readonly TemplateArgumentNode[], decls: readonly TemplateParameterDeclarationNode[], mapper: TypeMapper | undefined, + parentMapper?: TypeMapper, ): Map { const params = new Map(); const positional: TemplateParameter[] = []; @@ -1257,7 +1244,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } if (init === null) { - const argumentMapper = createTypeMapper(mapperParams, mapperArgs, { node, mapper }); + const argumentMapper = createTypeMapper( + mapperParams, + mapperArgs, + { node, mapper }, + parentMapper, + ); const defaultValue = getResolvedTypeParameterDefault(param, decl, argumentMapper); if (defaultValue) { commit(param, defaultValue); @@ -1439,6 +1431,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker argumentNodes, templateParameters, mapper, + declaredType.templateMapper, ); baseType = getOrInstantiateTemplate( diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 6289ea519a4..48c710e3826 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -405,13 +405,17 @@ describe("compiler: templates", () => { "main.tsp", ` @test interface A { - op foo(): U; + op foo(params: P): R; } alias B = A; @test op MyOp is B.foo; `, ); const { MyOp } = (await testHost.compile("main.tsp")) as { MyOp: Operation }; + const params = MyOp.parameters.properties.get("params"); + ok(params, "Expected params to be defined"); + strictEqual(params.type.kind, "Scalar"); + strictEqual(params.type.name, "string"); strictEqual(MyOp.returnType.kind, "Scalar"); strictEqual(MyOp.returnType.name, "string"); }); From 3eb2bcd54a5b5103752845893da64d4d5cda2aba Mon Sep 17 00:00:00 2001 From: Christopher Radek Date: Thu, 24 Apr 2025 12:46:52 -0700 Subject: [PATCH 4/4] add test case using interface default --- .../compiler/test/checker/templates.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/compiler/test/checker/templates.test.ts b/packages/compiler/test/checker/templates.test.ts index 48c710e3826..b65521c28b0 100644 --- a/packages/compiler/test/checker/templates.test.ts +++ b/packages/compiler/test/checker/templates.test.ts @@ -420,6 +420,26 @@ describe("compiler: templates", () => { strictEqual(MyOp.returnType.name, "string"); }); + it("can use parent parameters default in default", async () => { + testHost.addTypeSpecFile( + "main.tsp", + ` + @test interface MyInterface { + op foo(params: P): R; + } + alias AliasedInterface = MyInterface; + @test op MyOp is AliasedInterface.foo; + `, + ); + const { MyOp } = (await testHost.compile("main.tsp")) as { MyOp: Operation }; + const params = MyOp.parameters.properties.get("params"); + ok(params, "Expected params to be defined"); + strictEqual(params.type.kind, "Scalar"); + strictEqual(params.type.name, "string"); + strictEqual(MyOp.returnType.kind, "Scalar"); + strictEqual(MyOp.returnType.name, "string"); + }); + it("can override default provided by parent parameters", async () => { testHost.addTypeSpecFile( "main.tsp",