From ed664a384de8e2cf810a45c635e50f841af5f821 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Aug 2025 13:54:05 -0400 Subject: [PATCH 01/30] compiler: wip extern alias --- packages/compiler/lib/std/main.tsp | 4 + packages/compiler/src/core/binder.ts | 26 ++++- packages/compiler/src/core/checker.ts | 107 +++++++++++++++----- packages/compiler/src/core/messages.ts | 21 ++++ packages/compiler/src/core/name-resolver.ts | 7 +- packages/compiler/src/core/parser.ts | 63 +++++++----- packages/compiler/src/core/types.ts | 17 +++- packages/compiler/src/index.ts | 2 + packages/compiler/src/lib/tsp-index.ts | 22 ++++ packages/compiler/test/parser.test.ts | 4 +- 10 files changed, 216 insertions(+), 57 deletions(-) diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index 89954904be3..3fb6adf1570 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -3,3 +3,7 @@ import "./types.tsp"; import "./decorators.tsp"; import "./reflection.tsp"; import "./visibility.tsp"; + +namespace TypeSpec { +extern alias Example; +} diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 906218f99b5..47b0ff045ec 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -33,6 +33,7 @@ import { SymbolFlags, SymbolTable, SyntaxKind, + TemplateImplementations, TemplateParameterDeclarationNode, TypeSpecScriptNode, UnionStatementNode, @@ -133,7 +134,7 @@ export function createBinder(program: Program): Binder { for (const [key, member] of Object.entries(sourceFile.esmExports)) { let name: string; - let kind: "decorator" | "function"; + let kind: "decorator" | "function" | "template"; if (key === "$flags") { const context = getLocationContext(program, sourceFile); if (context.type === "library" || context.type === "project") { @@ -152,6 +153,19 @@ export function createBinder(program: Program): Binder { ); } } + } else if (key === "$templates") { + const value: TemplateImplementations = member as any; + for (const [namespaceName, templates] of Object.entries(value)) { + for (const [templateName, template] of Object.entries(templates)) { + bindFunctionImplementation( + namespaceName === "" ? [] : namespaceName.split("."), + "template", + templateName, + template, + sourceFile, + ); + } + } } else if (typeof member === "function") { // lots of 'any' casts here because control flow narrowing `member` to Function // isn't particularly useful it turns out. @@ -182,7 +196,7 @@ export function createBinder(program: Program): Binder { function bindFunctionImplementation( nsParts: string[], - kind: "decorator" | "function", + kind: "decorator" | "function" | "template", name: string, fn: (...args: any[]) => any, sourceFile: JsSourceFileNode, @@ -240,6 +254,14 @@ export function createBinder(program: Program): Binder { SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, containerSymbol, ); + } else if (kind === "template") { + tracer.trace("template", `Bound template "${name}" in namespace "${nsParts.join(".")}".`); + sym = createSymbol( + sourceFile, + name, + SymbolFlags.Alias | SymbolFlags.Declaration | SymbolFlags.Implementation, + containerSymbol, + ); } else { tracer.trace("function", `Bound function "${name}" in namespace "${nsParts.join(".")}".`); sym = createSymbol( diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 3802783dc4f..aa282ca9d29 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1630,13 +1630,17 @@ export function createChecker(program: Program, resolver: NameResolver): Checker args: (Type | Value | IndeterminateEntity)[], source: TypeMapper["source"], parentMapper: TypeMapper | undefined, - instantiateTempalates = true, + instantiateTemplates = true, ): Type { const symbolLinks = templateNode.kind === SyntaxKind.OperationStatement && templateNode.parent!.kind === SyntaxKind.InterfaceStatement ? getSymbolLinksForMember(templateNode as MemberNode) - : getSymbolLinks(templateNode.symbol); + : getSymbolLinks( + templateNode.kind === SyntaxKind.AliasStatement + ? getMergedSymbol(templateNode.symbol) + : templateNode.symbol, + ); compilerAssert( symbolLinks, @@ -1661,7 +1665,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker if (cached) { return cached; } - if (instantiateTempalates) { + if (instantiateTemplates) { return instantiateTemplate(symbolLinks.instantiations, templateNode, params, mapper); } else { return errorType; @@ -3510,7 +3514,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkTemplateDeclaration(node: TemplateableNode, mapper: TypeMapper | undefined) { // If mapper is undefined it means we are checking the declaration of the template. - if (mapper === undefined) { + if (mapper === undefined && node.templateParameters) { for (const templateParameter of node.templateParameters) { checkTemplateParameterDeclaration(templateParameter, undefined); } @@ -5150,7 +5154,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker createDiagnostic({ code: "augment-decorator-target", messageId: - aliasNode.value.kind === SyntaxKind.UnionExpression ? "noUnionExpression" : "default", + aliasNode.value?.kind === SyntaxKind.UnionExpression ? "noUnionExpression" : "default", target: node.targetType, }), ); @@ -5361,43 +5365,92 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: AliasStatementNode, mapper: TypeMapper | undefined, ): Type | IndeterminateEntity { - const links = getSymbolLinks(node.symbol); + const symbol = getMergedSymbol(node.symbol); + const links = getSymbolLinks(symbol); if (links.declaredType && mapper === undefined) { + // We are not instantiating this alias and it's already checked. return links.declaredType; } checkTemplateDeclaration(node, mapper); const aliasSymId = getNodeSym(node); - if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { - if (mapper === undefined) { + + const isExtern = node.modifiers & ModifierFlags.Extern; + + if (isExtern && node.value) { + // Illegal combination. Extern aliases cannot have a value. + reportCheckerDiagnostic( + createDiagnostic({ + code: "alias-extern-value", + target: node.value, + format: { typeName: symbol.name }, + }), + ); + } else if (!isExtern && !node.value) { + // Illegal combination. Non-extern aliases must have a value. + reportCheckerDiagnostic( + createDiagnostic({ + code: "alias-no-value", + target: node, + format: { typeName: symbol.name }, + }), + ); + } + + if (isExtern) { + pendingResolutions.start(aliasSymId, ResolutionKind.Type); + let type: Type; + if (symbol.value === undefined) { reportCheckerDiagnostic( createDiagnostic({ - code: "circular-alias-type", - format: { typeName: node.id.sv }, + code: "alias-extern-no-impl", target: node, + format: { typeName: symbol.name }, }), ); + type = errorType; + } else { + if (mapper) type = symbol.value(program, mapper); + // We are checking the template itself, so we will just put a never type here + else type = neverType; + } + links.declaredType = type; + linkType(links, type, mapper); + pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + return type; + } else { + if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { + if (mapper === undefined) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "circular-alias-type", + format: { typeName: symbol.name }, + target: node, + }), + ); + } + links.declaredType = errorType; + return errorType; } - links.declaredType = errorType; - return errorType; - } - pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = checkNode(node.value, mapper); - if (type === null) { - links.declaredType = errorType; - return errorType; - } - if (isValue(type)) { - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node.value })); - links.declaredType = errorType; - return errorType; - } - linkType(links, type as any, mapper); - pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + pendingResolutions.start(aliasSymId, ResolutionKind.Type); - return type; + const type = checkNode(node.value!, mapper); + if (type === null) { + links.declaredType = errorType; + return errorType; + } + if (isValue(type)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node.value! })); + links.declaredType = errorType; + return errorType; + } + linkType(links, type as any, mapper); + pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + + return type; + } } function checkConst(node: ConstStatementNode): Value | null { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5c42d275d59..dc2d810a0f7 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1014,6 +1014,27 @@ const diagnostics = { }, }, + "alias-no-value": { + severity: "error", + messages: { + default: paramMessage`Alias type '${"typeName"}' must have a value or be marked as 'extern'.`, + }, + }, + + "alias-extern-value": { + severity: "error", + messages: { + default: paramMessage`Alias type '${"typeName"}' cannot have a value when marked as 'extern'.`, + }, + }, + + "alias-extern-no-impl": { + severity: "error", + messages: { + default: paramMessage`Alias type '${"typeName"}' marked as 'extern' must have an associated JS implementation.`, + }, + }, + // #region Visibility "visibility-sealed": { severity: "error", diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 4af6b59be47..dcdb24f7e17 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -592,7 +592,7 @@ export function createResolver(program: Program): NameResolver { }; } - if (node.value.kind === SyntaxKind.TypeReference) { + if (node.value?.kind === SyntaxKind.TypeReference) { const result = resolveTypeReference(node.value); if (result.finalSymbol && result.finalSymbol.flags & SymbolFlags.Alias) { const aliasLinks = getSymbolLinks(result.finalSymbol); @@ -610,7 +610,7 @@ export function createResolver(program: Program): NameResolver { resolutionResult: slinks.aliasResolutionResult, isTemplateInstantiation: result.isTemplateInstantiation, }; - } else if (node.value.symbol) { + } else if (node.value?.symbol) { // a type literal slinks.aliasedSymbol = node.value.symbol; slinks.aliasResolutionResult = ResolutionResultFlags.Resolved; @@ -1100,6 +1100,8 @@ export function createResolver(program: Program): NameResolver { mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Decorator); } else if (sourceBinding.flags & SymbolFlags.Function) { mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Function); + } else if (sourceBinding.flags & SymbolFlags.Alias) { + mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Alias); } else { target.set(key, sourceBinding); } @@ -1117,6 +1119,7 @@ export function createResolver(program: Program): NameResolver { target.set(key, sourceBinding); return; } + const isSourceImplementation = sourceBinding.flags & SymbolFlags.Implementation; const isTargetImplementation = targetBinding.flags & SymbolFlags.Implementation; if (!isTargetImplementation && isSourceImplementation) { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 62b1c3d7b23..ebb41529965 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -456,10 +456,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.EnumKeyword: item = parseEnumStatement(pos, decorators); break; - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; case Token.ConstKeyword: reportInvalidDecorators(decorators, "const statement"); item = parseConstStatement(pos); @@ -476,7 +472,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ExternKeyword: case Token.FnKeyword: case Token.DecKeyword: - item = parseDeclaration(pos); + case Token.AliasKeyword: + item = parseDeclaration(pos, decorators); break; default: item = parseInvalidStatement(pos, decorators); @@ -556,10 +553,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.EnumKeyword: item = parseEnumStatement(pos, decorators); break; - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; case Token.ConstKeyword: reportInvalidDecorators(decorators, "const statement"); item = parseConstStatement(pos); @@ -571,7 +564,8 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.ExternKeyword: case Token.FnKeyword: case Token.DecKeyword: - item = parseDeclaration(pos); + case Token.AliasKeyword: + item = parseDeclaration(pos, decorators); break; case Token.EndOfFile: parseExpected(Token.CloseBrace); @@ -1222,22 +1216,37 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function parseAliasStatement(pos: number): AliasStatementNode { + function parseAliasStatement(pos: number, modifiers: Modifier[]): AliasStatementNode { + const modifierFlags = modifiersToFlags(modifiers); parseExpected(Token.AliasKeyword); const id = parseIdentifier(); const { items: templateParameters, range: templateParametersRange } = parseTemplateParameterList(); - parseExpected(Token.Equals); - const value = parseExpression(); - parseExpected(Token.Semicolon); - return { - kind: SyntaxKind.AliasStatement, - id, - templateParameters, - templateParametersRange, - value, - ...finishNode(pos), - }; + + const nextTok = parseExpectedOneOf(Token.Equals, Token.Semicolon); + + if (nextTok === Token.Semicolon) { + return { + kind: SyntaxKind.AliasStatement, + id, + templateParameters, + templateParametersRange, + modifiers: modifierFlags, + ...finishNode(pos), + }; + } else { + const value = parseExpression(); + parseExpected(Token.Semicolon); + return { + kind: SyntaxKind.AliasStatement, + id, + templateParameters, + templateParametersRange, + value, + modifiers: modifierFlags, + ...finishNode(pos), + }; + } } function parseConstStatement(pos: number): ConstStatementNode { @@ -1985,13 +1994,21 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseDeclaration( pos: number, - ): DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode | InvalidStatementNode { + decorators: DecoratorExpressionNode[], + ): + | DecoratorDeclarationStatementNode + | FunctionDeclarationStatementNode + | AliasStatementNode + | InvalidStatementNode { const modifiers = parseModifiers(); switch (token()) { case Token.DecKeyword: return parseDecoratorDeclarationStatement(pos, modifiers); case Token.FnKeyword: return parseFunctionDeclarationStatement(pos, modifiers); + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + return parseAliasStatement(pos, modifiers); } return parseInvalidStatement(pos, []); } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 912701f3a82..8dd4ae03368 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -53,6 +53,10 @@ export interface DecoratorFunction { namespace?: string; } +export interface TemplateFunction { + (program: TemplateContext, mapper: TypeMapper): void; +} + export interface BaseType { readonly entityKind: "Type"; kind: string; @@ -1454,8 +1458,9 @@ export interface EnumSpreadMemberNode extends BaseNode { export interface AliasStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.AliasStatement; - readonly value: Expression; + readonly value?: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; + readonly modifiers: ModifierFlags; } export interface ConstStatementNode extends BaseNode, DeclarationNode { @@ -2309,6 +2314,12 @@ export interface DecoratorImplementations { }; } +export interface TemplateImplementations { + readonly [namespace: string]: { + readonly [name: string]: TemplateFunction; + }; +} + export interface PackageFlags {} export interface LinterDefinition { @@ -2455,6 +2466,10 @@ export interface DecoratorContext { ): R; } +export interface TemplateContext { + program: Program; +} + export interface EmitContext> { /** * TypeSpec Program. diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index b79466982e7..a389e1c40db 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -242,6 +242,8 @@ export const $decorators = { }, }; +export { $templates } from "./lib/tsp-index.js"; + export { ensureTrailingDirectorySeparator, getAnyExtensionFromPath, diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index df7645e8335..a785b4bd14d 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -1,4 +1,7 @@ import { TypeSpecDecorators } from "../../generated-defs/TypeSpec.js"; +import { Program } from "../core/program.js"; +import { Type, TypeMapper } from "../core/types.js"; +import { $ } from "../typekit/index.js"; import { $discriminator, $doc, @@ -123,4 +126,23 @@ export const $decorators = { } satisfies TypeSpecDecorators, }; +let COUNTER = 0; + +export const $templates = { + TypeSpec: { + Example(program: Program, mapper: TypeMapper): Type { + const argEntity = mapper.args[0]; + + if (argEntity.entityKind === "Value") { + throw new Error("Example template must be used with a type argument."); + } + + const argType = argEntity.entityKind === "Indeterminate" ? argEntity.type : argEntity; + return $(program).array.create( + $(program).tuple.create([$(program).literal.create(COUNTER++), argType]), + ); + }, + }, +}; + export const namespace = "TypeSpec"; diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index ed444712d13..5e1ebb84508 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -469,7 +469,7 @@ describe("compiler: parser", () => { (node) => { const statement = node.statements[0]; assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected"); - const value = statement.value; + const value = statement.value!; assert(value.kind === SyntaxKind.StringLiteral, "string literal expected"); assert.strictEqual(value.value, "banana"); }, @@ -622,7 +622,7 @@ describe("compiler: parser", () => { function getNode(astNode: TypeSpecScriptNode): Node { const statement = astNode.statements[0]; strictEqual(statement.kind, SyntaxKind.AliasStatement); - return statement.value; + return statement.value!; } function getStringTemplateNode(astNode: TypeSpecScriptNode): StringTemplateExpressionNode { const node = getNode(astNode); From d625ac32c7d585cc1275332bd7370f4781d9c80d Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 7 Aug 2025 15:59:27 -0400 Subject: [PATCH 02/30] extern fn --- packages/compiler/lib/std/main.tsp | 4 +- packages/compiler/lib/std/reflection.tsp | 1 + packages/compiler/src/core/binder.ts | 14 +++ packages/compiler/src/core/checker.ts | 109 ++++++++++++++++-- packages/compiler/src/core/messages.ts | 1 + packages/compiler/src/core/parser.ts | 10 +- packages/compiler/src/core/semantic-walker.ts | 10 ++ .../src/core/type-relation-checker.ts | 10 +- packages/compiler/src/core/types.ts | 23 +++- packages/compiler/src/index.ts | 2 +- packages/compiler/src/lib/tsp-index.ts | 15 +-- .../compiler/src/server/type-signature.ts | 10 ++ 12 files changed, 172 insertions(+), 37 deletions(-) diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index 3fb6adf1570..047c31484b1 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -5,5 +5,7 @@ import "./reflection.tsp"; import "./visibility.tsp"; namespace TypeSpec { -extern alias Example; + extern fn example(T: Reflection.Type); + + alias Example = example(T); } diff --git a/packages/compiler/lib/std/reflection.tsp b/packages/compiler/lib/std/reflection.tsp index f1142b2e738..12bb602c034 100644 --- a/packages/compiler/lib/std/reflection.tsp +++ b/packages/compiler/lib/std/reflection.tsp @@ -11,3 +11,4 @@ model Scalar {} model Union {} model UnionVariant {} model StringTemplate {} +model Type {} diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 47b0ff045ec..427d47fa8ed 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -13,6 +13,7 @@ import { EnumStatementNode, FileLibraryMetadata, FunctionDeclarationStatementNode, + FunctionImplementations, FunctionParameterNode, InterfaceStatementNode, IntersectionExpressionNode, @@ -166,6 +167,19 @@ export function createBinder(program: Program): Binder { ); } } + } else if (key === "$functions") { + const value: FunctionImplementations = member as any; + for (const [namespaceName, functions] of Object.entries(value)) { + for (const [functionName, fn] of Object.entries(functions)) { + bindFunctionImplementation( + namespaceName === "" ? [] : namespaceName.split("."), + "function", + functionName, + fn, + sourceFile, + ); + } + } } else if (typeof member === "function") { // lots of 'any' casts here because control flow narrowing `member` to Function // isn't particularly useful it turns out. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index aa282ca9d29..579543d14c4 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -70,6 +70,7 @@ import { FunctionDeclarationStatementNode, FunctionParameter, FunctionParameterNode, + FunctionType, IdentifierKind, IdentifierNode, IndeterminateEntity, @@ -1436,13 +1437,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return errorType; } - if (sym.flags & SymbolFlags.Function) { - reportCheckerDiagnostic( - createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: sym }), - ); + // if (sym.flags & SymbolFlags.Function) { + // reportCheckerDiagnostic( + // createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: sym }), + // ); - return errorType; - } + // return errorType; + // } const argumentNodes = node.kind === SyntaxKind.TypeReference ? node.arguments : []; const symbolLinks = getSymbolLinks(sym); @@ -1911,9 +1912,48 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkFunctionDeclaration( node: FunctionDeclarationStatementNode, mapper: TypeMapper | undefined, - ) { - reportCheckerDiagnostic(createDiagnostic({ code: "function-unsupported", target: node })); - return errorType; + ): FunctionType { + const mergedSymbol = getMergedSymbol(node.symbol); + const links = getSymbolLinks(mergedSymbol); + + if (links.declaredType && mapper === undefined) { + // we're not instantiating this operation and we've already checked it + return links.declaredType as FunctionType; + } + + const namespace = getParentNamespaceType(node); + compilerAssert( + namespace, + `Function ${node.id.sv} should have resolved a declared namespace or the global namespace.`, + ); + + const name = node.id.sv; + + if (!(node.modifierFlags & ModifierFlags.Extern)) { + reportCheckerDiagnostic(createDiagnostic({ code: "function-extern", target: node })); + } + + const implementation = mergedSymbol.value; + if (implementation === undefined) { + reportCheckerDiagnostic(createDiagnostic({ code: "missing-implementation", target: node })); + } + + const functionType: FunctionType = createType({ + kind: "Function", + name, + namespace, + node, + parameters: node.parameters.map((x) => checkFunctionParameter(x, mapper, true)), + returnType: node.returnType + ? getParamConstraintEntityForNode(node.returnType, mapper) + : ({ + entityKind: "MixedParameterConstraint", + type: unknownType, + } satisfies MixedParameterConstraint), + implementation: implementation ?? (() => errorType), + }); + + return functionType; } function checkFunctionParameter( @@ -4108,10 +4148,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkCallExpressionTarget( node: CallExpressionNode, mapper: TypeMapper | undefined, - ): ScalarConstructor | Scalar | null { + ): ScalarConstructor | Scalar | FunctionType | null { const target = checkTypeReference(node.target, mapper); - if (target.kind === "Scalar" || target.kind === "ScalarConstructor") { + if ( + target.kind === "Scalar" || + target.kind === "ScalarConstructor" || + target.kind === "Function" + ) { return target; } else { reportCheckerDiagnostic( @@ -4264,13 +4308,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkCallExpression( node: CallExpressionNode, mapper: TypeMapper | undefined, - ): Value | null { + ): Type | Value | IndeterminateEntity | null { const target = checkCallExpressionTarget(node, mapper); if (target === null) { return null; } if (target.kind === "ScalarConstructor") { return createScalarValue(node, mapper, target); + } else if (target.kind === "Function") { + return checkFunctionCall(node, target, mapper); } if (relation.areScalarsRelated(target, getStdType("string"))) { @@ -4291,6 +4337,45 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } } + function checkFunctionCall( + node: CallExpressionNode, + target: FunctionType, + mapper: TypeMapper | undefined, + ): Type | Value | null { + const [hadError, resolvedArgs] = checkFunctionCallArguments(node.arguments, target, mapper); + + const result = hadError ? errorType : target.implementation(program, ...resolvedArgs); + + if (!hadError) checkFunctionReturn(target, result); + + return result; + } + + function checkFunctionCallArguments( + args: Expression[], + target: FunctionType, + mapper: TypeMapper | undefined, + ): [boolean, (Type | Value)[]] { + return [false, args.map((arg) => checkNode(arg, mapper))] as [boolean, (Type | Value)[]]; + } + + function checkFunctionReturn(target: FunctionType, result: Type | Value) { + if (target.returnType.valueType) { + if (result.entityKind !== "Value") { + reportCheckerDiagnostic( + createDiagnostic({ + code: "expect-value", + messageId: "functionReturn", + format: { name: getTypeName(result) }, + target: target, + }), + ); + + return; + } + } + } + function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { const entity = checkNode(node.target, mapper, undefined); if (entity === null) { diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index dc2d810a0f7..524c9853408 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -395,6 +395,7 @@ const diagnostics = { modelExpression: `Is a model expression type, but is being used as a value here. Use #{} to create an object value.`, tuple: `Is a tuple type, but is being used as a value here. Use #[] to create an array value.`, templateConstraint: paramMessage`${"name"} template parameter can be a type but is being used as a value here.`, + functionReturn: paramMessage`Function returned a type, but a value was expected.`, }, }, "non-callable": { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index ebb41529965..b65a8d351e4 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -946,7 +946,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const id = parseIdentifier(); let constraint: Expression | ValueOfExpressionNode | undefined; if (parseOptional(Token.ExtendsKeyword)) { - constraint = parseMixedParameterConstraint(); + constraint = parseMixedConstraint(); } let def: Expression | undefined; if (parseOptional(Token.Equals)) { @@ -965,7 +965,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa if (token() === Token.ValueOfKeyword) { return parseValueOfExpression(); } else if (parseOptional(Token.OpenParen)) { - const expr = parseMixedParameterConstraint(); + const expr = parseMixedConstraint(); parseExpected(Token.CloseParen); return expr; } @@ -973,7 +973,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa return parseIntersectionExpressionOrHigher(); } - function parseMixedParameterConstraint(): Expression | ValueOfExpressionNode { + function parseMixedConstraint(): Expression | ValueOfExpressionNode { const pos = tokenPos(); parseOptional(Token.Bar); const node: Expression = parseValueOfExpressionOrIntersectionOrHigher(); @@ -2076,7 +2076,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const { items: parameters } = parseFunctionParameters(); let returnType; if (parseOptional(Token.Colon)) { - returnType = parseExpression(); + returnType = parseMixedConstraint(); } parseExpected(Token.Semicolon); return { @@ -2125,7 +2125,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa const optional = parseOptional(Token.Question); let type; if (parseOptional(Token.Colon)) { - type = parseMixedParameterConstraint(); + type = parseMixedConstraint(); } return { kind: SyntaxKind.FunctionParameter, diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 6342db3d26a..593389a1210 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -3,6 +3,7 @@ import { isTemplateDeclaration } from "./type-utils.js"; import { Decorator, Enum, + FunctionType, Interface, ListenerFlow, Model, @@ -394,6 +395,13 @@ function navigateScalarConstructor(type: ScalarConstructor, context: NavigationC if (context.emit("scalarConstructor", type) === ListenerFlow.NoRecursion) return; } +function navigateFunctionDeclaration(type: FunctionType, context: NavigationContext) { + if (checkVisited(context.visited, type)) { + return; + } + if (context.emit("function", type) === ListenerFlow.NoRecursion) return; +} + function navigateTypeInternal(type: Type, context: NavigationContext) { switch (type.kind) { case "Model": @@ -426,6 +434,8 @@ function navigateTypeInternal(type: Type, context: NavigationContext) { return navigateDecoratorDeclaration(type, context); case "ScalarConstructor": return navigateScalarConstructor(type, context); + case "Function": + return navigateFunctionDeclaration(type, context); case "FunctionParameter": case "Boolean": case "EnumMember": diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index dc0e02948ae..ccd47da6b91 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -98,11 +98,9 @@ const ReflectionNameToKind = { Tuple: "Tuple", Union: "Union", UnionVariant: "UnionVariant", -} as const; +} as const satisfies Record; -const _assertReflectionNameToKind: Record = ReflectionNameToKind; - -type ReflectionTypeName = keyof typeof ReflectionNameToKind; +type ReflectionTypeName = keyof typeof ReflectionNameToKind | "Type"; export function createTypeRelationChecker(program: Program, checker: Checker): TypeRelation { return { @@ -510,10 +508,10 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T function isSimpleTypeAssignableTo(source: Type, target: Type): boolean | undefined { if (isNeverType(source)) return true; - if (isVoidType(target)) return false; + if (isVoidType(target)) return isVoidType(source); if (isUnknownType(target)) return true; if (isReflectionType(target)) { - return source.kind === ReflectionNameToKind[target.name]; + return target.name === "Type" || source.kind === ReflectionNameToKind[target.name]; } if (target.kind === "Scalar") { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 8dd4ae03368..2bb80b60188 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -57,6 +57,10 @@ export interface TemplateFunction { (program: TemplateContext, mapper: TypeMapper): void; } +export interface FunctionImplementation { + (program: Program, ...args: any[]): Type | Value; +} + export interface BaseType { readonly entityKind: "Type"; kind: string; @@ -135,7 +139,8 @@ export type Type = | TemplateParameter | Tuple | Union - | UnionVariant; + | UnionVariant + | FunctionType; export type StdTypes = { // Models @@ -691,6 +696,16 @@ export interface Decorator extends BaseType { implementation: (...args: unknown[]) => void; } +export interface FunctionType extends BaseType { + kind: "Function"; + node?: FunctionDeclarationStatementNode; + name: string; + namespace?: Namespace; + parameters: MixedFunctionParameter[]; + returnType: MixedParameterConstraint; + implementation: (...args: unknown[]) => Type | Value; +} + export interface FunctionParameterBase extends BaseType { kind: "FunctionParameter"; node?: FunctionParameterNode; @@ -2320,6 +2335,12 @@ export interface TemplateImplementations { }; } +export interface FunctionImplementations { + readonly [namespace: string]: { + readonly [name: string]: FunctionImplementation; + }; +} + export interface PackageFlags {} export interface LinterDefinition { diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index a389e1c40db..55ffed61d31 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -242,7 +242,7 @@ export const $decorators = { }, }; -export { $templates } from "./lib/tsp-index.js"; +export { $functions } from "./lib/tsp-index.js"; export { ensureTrailingDirectorySeparator, diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index a785b4bd14d..215563b037c 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -1,6 +1,6 @@ import { TypeSpecDecorators } from "../../generated-defs/TypeSpec.js"; import { Program } from "../core/program.js"; -import { Type, TypeMapper } from "../core/types.js"; +import { Type } from "../core/types.js"; import { $ } from "../typekit/index.js"; import { $discriminator, @@ -128,18 +128,11 @@ export const $decorators = { let COUNTER = 0; -export const $templates = { +export const $functions = { TypeSpec: { - Example(program: Program, mapper: TypeMapper): Type { - const argEntity = mapper.args[0]; - - if (argEntity.entityKind === "Value") { - throw new Error("Example template must be used with a type argument."); - } - - const argType = argEntity.entityKind === "Indeterminate" ? argEntity.type : argEntity; + example(program: Program, t: Type): Type { return $(program).array.create( - $(program).tuple.create([$(program).literal.create(COUNTER++), argType]), + $(program).tuple.create([$(program).literal.create(COUNTER++), t]), ); }, }, diff --git a/packages/compiler/src/server/type-signature.ts b/packages/compiler/src/server/type-signature.ts index f9d181c6686..4f1c099839e 100644 --- a/packages/compiler/src/server/type-signature.ts +++ b/packages/compiler/src/server/type-signature.ts @@ -9,6 +9,7 @@ import { Decorator, EnumMember, FunctionParameter, + FunctionType, Interface, Model, ModelProperty, @@ -103,6 +104,8 @@ function getTypeSignature(type: Type, options: GetSymbolSignatureOptions): strin return `(union variant)\n${fence(getUnionVariantSignature(type))}`; case "Tuple": return `(tuple)\n[${fence(type.values.map((v) => getTypeSignature(v, options)).join(", "))}]`; + case "Function": + return fence(getFunctionSignature(type)); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); @@ -116,6 +119,13 @@ function getDecoratorSignature(type: Decorator) { return `dec ${ns}${name}(${parameters.join(", ")})`; } +function getFunctionSignature(type: FunctionType) { + const ns = getQualifier(type.namespace); + const parameters = type.parameters.map((p) => getFunctionParameterSignature(p)); + const returnType = getEntityName(type.returnType); + return `fn ${ns}${type.name}(${parameters.join(", ")}): ${returnType}`; +} + function getOperationSignature(type: Operation, includeQualifier: boolean = true) { const parameters = [...type.parameters.properties.values()].map((p) => getModelPropertySignature(p, false /* includeQualifier */), From 1d21ec3aa779d31c03798c1f43e9f86af06be7b6 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 12:57:00 -0400 Subject: [PATCH 03/30] Revert extern alias changes, prefer extern fn --- .../TypeSpec.Prototypes.ts-test.ts | 2 +- packages/compiler/generated-defs/TypeSpec.ts | 7 ++ .../generated-defs/TypeSpec.ts-test.ts | 11 ++- packages/compiler/lib/std/main.tsp | 4 +- packages/compiler/src/core/binder.ts | 24 +---- packages/compiler/src/core/checker.ts | 96 ++++++------------- packages/compiler/src/core/name-resolver.ts | 4 +- packages/compiler/src/core/parser.ts | 61 +++++------- packages/compiler/src/core/semantic-walker.ts | 4 + packages/compiler/src/core/types.ts | 20 ++-- packages/compiler/src/lib/tsp-index.ts | 2 +- packages/compiler/test/parser.test.ts | 2 +- 12 files changed, 85 insertions(+), 152 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts index ba67b433b56..27a99b09c6f 100644 --- a/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.Prototypes.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecPrototypesDecorators } from "./TypeSpec.Prototypes.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecPrototypesDecorators = $decorators["TypeSpec.Prototypes"]; +const _decs: TypeSpecPrototypesDecorators = $decorators["TypeSpec.Prototypes"]; diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index b1b70348129..a9f500be961 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -8,6 +8,7 @@ import type { Namespace, Numeric, Operation, + Program, Scalar, Type, Union, @@ -1156,3 +1157,9 @@ export type TypeSpecDecorators = { withVisibilityFilter: WithVisibilityFilterDecorator; withLifecycleUpdate: WithLifecycleUpdateDecorator; }; + +export type Example2FunctionImplementation = (program: Program, T: Type) => Type; + +export type TypeSpecFunctions = { + example2: Example2FunctionImplementation; +}; diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index 12337f14a8a..568630da9c4 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -1,10 +1,15 @@ // An error in the imports would mean that the decorator is not exported or // doesn't have the right name. -import { $decorators } from "../src/index.js"; -import type { TypeSpecDecorators } from "./TypeSpec.js"; +import { $decorators, $functions } from "../src/index.js"; +import type { TypeSpecDecorators, TypeSpecFunctions } from "./TypeSpec.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecDecorators = $decorators["TypeSpec"]; +const _decs: TypeSpecDecorators = $decorators["TypeSpec"]; + +/** + * An error here would mean that the exported function is not using the same signature. Make sure to have export const $funcName: FuncNameFunction = (...) => ... + */ +const _funcs: TypeSpecFunctions = $functions["TypeSpec"]; diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index 047c31484b1..3d02d37c6c9 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -5,7 +5,7 @@ import "./reflection.tsp"; import "./visibility.tsp"; namespace TypeSpec { - extern fn example(T: Reflection.Type); + extern fn example2(T: Reflection.Type); - alias Example = example(T); + alias Example2 = example2(T); } diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index 427d47fa8ed..057f3463434 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -34,7 +34,6 @@ import { SymbolFlags, SymbolTable, SyntaxKind, - TemplateImplementations, TemplateParameterDeclarationNode, TypeSpecScriptNode, UnionStatementNode, @@ -154,19 +153,6 @@ export function createBinder(program: Program): Binder { ); } } - } else if (key === "$templates") { - const value: TemplateImplementations = member as any; - for (const [namespaceName, templates] of Object.entries(value)) { - for (const [templateName, template] of Object.entries(templates)) { - bindFunctionImplementation( - namespaceName === "" ? [] : namespaceName.split("."), - "template", - templateName, - template, - sourceFile, - ); - } - } } else if (key === "$functions") { const value: FunctionImplementations = member as any; for (const [namespaceName, functions] of Object.entries(value)) { @@ -210,7 +196,7 @@ export function createBinder(program: Program): Binder { function bindFunctionImplementation( nsParts: string[], - kind: "decorator" | "function" | "template", + kind: "decorator" | "function", name: string, fn: (...args: any[]) => any, sourceFile: JsSourceFileNode, @@ -268,14 +254,6 @@ export function createBinder(program: Program): Binder { SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, containerSymbol, ); - } else if (kind === "template") { - tracer.trace("template", `Bound template "${name}" in namespace "${nsParts.join(".")}".`); - sym = createSymbol( - sourceFile, - name, - SymbolFlags.Alias | SymbolFlags.Declaration | SymbolFlags.Implementation, - containerSymbol, - ); } else { tracer.trace("function", `Bound function "${name}" in namespace "${nsParts.join(".")}".`); sym = createSymbol( diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 579543d14c4..8169b2396f0 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1953,6 +1953,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker implementation: implementation ?? (() => errorType), }); + namespace.functionDeclarations.set(name, functionType); + + linkType(links, functionType, mapper); + return functionType; } @@ -5239,7 +5243,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker createDiagnostic({ code: "augment-decorator-target", messageId: - aliasNode.value?.kind === SyntaxKind.UnionExpression ? "noUnionExpression" : "default", + aliasNode.value.kind === SyntaxKind.UnionExpression ? "noUnionExpression" : "default", target: node.targetType, }), ); @@ -5450,8 +5454,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: AliasStatementNode, mapper: TypeMapper | undefined, ): Type | IndeterminateEntity { - const symbol = getMergedSymbol(node.symbol); - const links = getSymbolLinks(symbol); + const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { // We are not instantiating this alias and it's already checked. @@ -5461,81 +5464,36 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const aliasSymId = getNodeSym(node); - const isExtern = node.modifiers & ModifierFlags.Extern; - - if (isExtern && node.value) { - // Illegal combination. Extern aliases cannot have a value. - reportCheckerDiagnostic( - createDiagnostic({ - code: "alias-extern-value", - target: node.value, - format: { typeName: symbol.name }, - }), - ); - } else if (!isExtern && !node.value) { - // Illegal combination. Non-extern aliases must have a value. - reportCheckerDiagnostic( - createDiagnostic({ - code: "alias-no-value", - target: node, - format: { typeName: symbol.name }, - }), - ); - } - - if (isExtern) { - pendingResolutions.start(aliasSymId, ResolutionKind.Type); - let type: Type; - if (symbol.value === undefined) { + if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { + if (mapper === undefined) { reportCheckerDiagnostic( createDiagnostic({ - code: "alias-extern-no-impl", + code: "circular-alias-type", + format: { typeName: node.id.sv }, target: node, - format: { typeName: symbol.name }, }), ); - type = errorType; - } else { - if (mapper) type = symbol.value(program, mapper); - // We are checking the template itself, so we will just put a never type here - else type = neverType; - } - links.declaredType = type; - linkType(links, type, mapper); - pendingResolutions.finish(aliasSymId, ResolutionKind.Type); - return type; - } else { - if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { - if (mapper === undefined) { - reportCheckerDiagnostic( - createDiagnostic({ - code: "circular-alias-type", - format: { typeName: symbol.name }, - target: node, - }), - ); - } - links.declaredType = errorType; - return errorType; } + links.declaredType = errorType; + return errorType; + } - pendingResolutions.start(aliasSymId, ResolutionKind.Type); + pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = checkNode(node.value!, mapper); - if (type === null) { - links.declaredType = errorType; - return errorType; - } - if (isValue(type)) { - reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node.value! })); - links.declaredType = errorType; - return errorType; - } - linkType(links, type as any, mapper); - pendingResolutions.finish(aliasSymId, ResolutionKind.Type); - - return type; + const type = checkNode(node.value, mapper); + if (type === null) { + links.declaredType = errorType; + return errorType; + } + if (isValue(type)) { + reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node.value })); + links.declaredType = errorType; + return errorType; } + linkType(links, type as any, mapper); + pendingResolutions.finish(aliasSymId, ResolutionKind.Type); + + return type; } function checkConst(node: ConstStatementNode): Value | null { diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index dcdb24f7e17..4590a9cbb90 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -592,7 +592,7 @@ export function createResolver(program: Program): NameResolver { }; } - if (node.value?.kind === SyntaxKind.TypeReference) { + if (node.value.kind === SyntaxKind.TypeReference) { const result = resolveTypeReference(node.value); if (result.finalSymbol && result.finalSymbol.flags & SymbolFlags.Alias) { const aliasLinks = getSymbolLinks(result.finalSymbol); @@ -610,7 +610,7 @@ export function createResolver(program: Program): NameResolver { resolutionResult: slinks.aliasResolutionResult, isTemplateInstantiation: result.isTemplateInstantiation, }; - } else if (node.value?.symbol) { + } else if (node.value.symbol) { // a type literal slinks.aliasedSymbol = node.value.symbol; slinks.aliasResolutionResult = ResolutionResultFlags.Resolved; diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index b65a8d351e4..fc535561ea1 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -468,12 +468,15 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "empty statement"); item = parseEmptyStatement(pos); break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + item = parseAliasStatement(pos); + break; // Start of declaration with modifiers case Token.ExternKeyword: case Token.FnKeyword: case Token.DecKeyword: - case Token.AliasKeyword: - item = parseDeclaration(pos, decorators); + item = parseDeclaration(pos); break; default: item = parseInvalidStatement(pos, decorators); @@ -561,11 +564,14 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(pos); break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + item = parseAliasStatement(pos); + break; case Token.ExternKeyword: case Token.FnKeyword: case Token.DecKeyword: - case Token.AliasKeyword: - item = parseDeclaration(pos, decorators); + item = parseDeclaration(pos); break; case Token.EndOfFile: parseExpected(Token.CloseBrace); @@ -1216,37 +1222,24 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa }; } - function parseAliasStatement(pos: number, modifiers: Modifier[]): AliasStatementNode { - const modifierFlags = modifiersToFlags(modifiers); + function parseAliasStatement(pos: number): AliasStatementNode { parseExpected(Token.AliasKeyword); const id = parseIdentifier(); const { items: templateParameters, range: templateParametersRange } = parseTemplateParameterList(); - const nextTok = parseExpectedOneOf(Token.Equals, Token.Semicolon); + parseExpected(Token.Equals); - if (nextTok === Token.Semicolon) { - return { - kind: SyntaxKind.AliasStatement, - id, - templateParameters, - templateParametersRange, - modifiers: modifierFlags, - ...finishNode(pos), - }; - } else { - const value = parseExpression(); - parseExpected(Token.Semicolon); - return { - kind: SyntaxKind.AliasStatement, - id, - templateParameters, - templateParametersRange, - value, - modifiers: modifierFlags, - ...finishNode(pos), - }; - } + const value = parseExpression(); + parseExpected(Token.Semicolon); + return { + kind: SyntaxKind.AliasStatement, + id, + templateParameters, + templateParametersRange, + value, + ...finishNode(pos), + }; } function parseConstStatement(pos: number): ConstStatementNode { @@ -1994,21 +1987,13 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa function parseDeclaration( pos: number, - decorators: DecoratorExpressionNode[], - ): - | DecoratorDeclarationStatementNode - | FunctionDeclarationStatementNode - | AliasStatementNode - | InvalidStatementNode { + ): DecoratorDeclarationStatementNode | FunctionDeclarationStatementNode | InvalidStatementNode { const modifiers = parseModifiers(); switch (token()) { case Token.DecKeyword: return parseDecoratorDeclarationStatement(pos, modifiers); case Token.FnKeyword: return parseFunctionDeclarationStatement(pos, modifiers); - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - return parseAliasStatement(pos, modifiers); } return parseInvalidStatement(pos, []); } diff --git a/packages/compiler/src/core/semantic-walker.ts b/packages/compiler/src/core/semantic-walker.ts index 593389a1210..803631db738 100644 --- a/packages/compiler/src/core/semantic-walker.ts +++ b/packages/compiler/src/core/semantic-walker.ts @@ -202,6 +202,10 @@ function navigateNamespaceType(namespace: Namespace, context: NavigationContext) navigateDecoratorDeclaration(decorator, context); } + for (const func of namespace.functionDeclarations.values()) { + navigateFunctionDeclaration(func, context); + } + context.emit("exitNamespace", namespace); } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 2bb80b60188..b15e3cdc390 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -53,10 +53,6 @@ export interface DecoratorFunction { namespace?: string; } -export interface TemplateFunction { - (program: TemplateContext, mapper: TypeMapper): void; -} - export interface FunctionImplementation { (program: Program, ...args: any[]): Type | Value; } @@ -585,6 +581,13 @@ export interface Namespace extends BaseType, DecoratedType { * Order is implementation-defined and may change. */ decoratorDeclarations: Map; + + /** + * The functions declared in the namespace. + * + * Order is implementation-defined and may change. + */ + functionDeclarations: Map; } export type LiteralType = StringLiteral | NumericLiteral | BooleanLiteral; @@ -1473,9 +1476,8 @@ export interface EnumSpreadMemberNode extends BaseNode { export interface AliasStatementNode extends BaseNode, DeclarationNode, TemplateDeclarationNode { readonly kind: SyntaxKind.AliasStatement; - readonly value?: Expression; + readonly value: Expression; readonly parent?: TypeSpecScriptNode | NamespaceStatementNode; - readonly modifiers: ModifierFlags; } export interface ConstStatementNode extends BaseNode, DeclarationNode { @@ -2329,12 +2331,6 @@ export interface DecoratorImplementations { }; } -export interface TemplateImplementations { - readonly [namespace: string]: { - readonly [name: string]: TemplateFunction; - }; -} - export interface FunctionImplementations { readonly [namespace: string]: { readonly [name: string]: FunctionImplementation; diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index 215563b037c..69314081710 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -130,7 +130,7 @@ let COUNTER = 0; export const $functions = { TypeSpec: { - example(program: Program, t: Type): Type { + example2(program: Program, t: Type): Type { return $(program).array.create( $(program).tuple.create([$(program).literal.create(COUNTER++), t]), ); diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 5e1ebb84508..6b7e7a32ede 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -469,7 +469,7 @@ describe("compiler: parser", () => { (node) => { const statement = node.statements[0]; assert(statement.kind === SyntaxKind.AliasStatement, "alias statement expected"); - const value = statement.value!; + const value = statement.value; assert(value.kind === SyntaxKind.StringLiteral, "string literal expected"); assert.strictEqual(value.value, "banana"); }, From 3aeeca9806684983f2b6a8307f32031fca3d811e Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 12:57:25 -0400 Subject: [PATCH 04/30] Implement extern signatures for function declarations. --- .../components/decorator-signature-tests.tsx | 29 -- .../components/dollar-functions-type.tsx | 32 ++ .../components/entity-signature-tests.tsx | 53 +++ ...s-signatures.tsx => entity-signatures.tsx} | 87 +++-- .../components/function-signature-type.tsx | 357 ++++++++++++++++++ .../external-packages/compiler.ts | 1 + .../gen-extern-signatures.ts | 48 ++- .../tspd/src/gen-extern-signatures/types.ts | 18 +- .../tspd/src/ref-doc/utils/type-signature.ts | 9 + 9 files changed, 562 insertions(+), 72 deletions(-) delete mode 100644 packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx create mode 100644 packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx create mode 100644 packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx rename packages/tspd/src/gen-extern-signatures/components/{decorators-signatures.tsx => entity-signatures.tsx} (52%) create mode 100644 packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx diff --git a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx deleted file mode 100644 index 8f40feac957..00000000000 --- a/packages/tspd/src/gen-extern-signatures/components/decorator-signature-tests.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Refkey } from "@alloy-js/core"; -import * as ts from "@alloy-js/typescript"; - -export interface DecoratorSignatureTests { - namespaceName: string; - dollarDecoratorRefKey: Refkey; - dollarDecoratorsTypeRefKey: Refkey; -} - -export function DecoratorSignatureTests({ - namespaceName, - dollarDecoratorRefKey, - dollarDecoratorsTypeRefKey, -}: Readonly) { - return ( - <> - - - - {dollarDecoratorRefKey} - {`["${namespaceName}"]`} - - - ); -} diff --git a/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx b/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx new file mode 100644 index 00000000000..4f686d02343 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/dollar-functions-type.tsx @@ -0,0 +1,32 @@ +import { For, Refkey } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { FunctionSignature } from "../types.js"; + +export interface DollarFunctionsTypeProps { + namespaceName: string; + functions: FunctionSignature[]; + refkey: Refkey; +} + +/** Type for the $functions variable for the given namespace */ +export function DollarFunctionsType(props: Readonly) { + return ( + + + + {(signature) => { + return ; + }} + + + + ); +} + +function getFunctionsRecordForNamespaceName(namespaceName: string) { + return `${namespaceName.replaceAll(".", "")}Functions`; +} diff --git a/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx new file mode 100644 index 00000000000..939b289db50 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signature-tests.tsx @@ -0,0 +1,53 @@ +import { Refkey, Show } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { EntitySignature } from "../types.js"; + +export interface EntitySignatureTests { + namespaceName: string; + entities: EntitySignature[]; + dollarDecoratorRefKey: Refkey; + dollarDecoratorsTypeRefKey: Refkey; + dollarFunctionsRefKey: Refkey; + dollarFunctionsTypeRefKey: Refkey; +} + +export function EntitySignatureTests({ + namespaceName, + entities, + dollarDecoratorRefKey, + dollarDecoratorsTypeRefKey, + dollarFunctionsRefKey, + dollarFunctionsTypeRefKey, +}: Readonly) { + const hasDecorators = entities.some((e) => e.kind === "Decorator"); + const hasFunctions = entities.some((e) => e.kind === "Function"); + + return ( + <> + + + + + {dollarDecoratorRefKey} + {`["${namespaceName}"]`} + + + + + + + {dollarFunctionsRefKey} + {`["${namespaceName}"]`} + + + + ); +} diff --git a/packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx similarity index 52% rename from packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx rename to packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx index 13991a4f9e6..17dfceaba31 100644 --- a/packages/tspd/src/gen-extern-signatures/components/decorators-signatures.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/entity-signatures.tsx @@ -5,48 +5,68 @@ import { Refkey, refkey, render, + Show, StatementList, } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { Program } from "@typespec/compiler"; import { typespecCompiler } from "../external-packages/compiler.js"; -import { DecoratorSignature } from "../types.js"; -import { DecoratorSignatureTests } from "./decorator-signature-tests.jsx"; -import { - DecoratorSignatureType, - ValueOfModelTsInterfaceBody, -} from "./decorator-signature-type.jsx"; -import { DollarDecoratorsType } from "./dollar-decorators-type.jsx"; +import { DecoratorSignature, EntitySignature, FunctionSignature } from "../types.js"; +import { DecoratorSignatureType, ValueOfModelTsInterfaceBody } from "./decorator-signature-type.js"; +import { DollarDecoratorsType } from "./dollar-decorators-type.js"; +import { DollarFunctionsType } from "./dollar-functions-type.jsx"; +import { EntitySignatureTests } from "./entity-signature-tests.jsx"; +import { FunctionSignatureType } from "./function-signature-type.jsx"; import { createTspdContext, TspdContext, useTspd } from "./tspd-context.js"; -export interface DecoratorSignaturesProps { - decorators: DecoratorSignature[]; +export interface EntitySignaturesProps { + entities: EntitySignature[]; namespaceName: string; dollarDecoratorsRefKey: Refkey; + dollarFunctionsRefKey: Refkey; } -export function DecoratorSignatures({ +export function EntitySignatures({ namespaceName, - decorators, + entities, dollarDecoratorsRefKey: dollarDecoratorsRefkey, -}: DecoratorSignaturesProps) { + dollarFunctionsRefKey: dollarFunctionsRefkey, +}: EntitySignaturesProps) { + const decorators = entities.filter((e): e is DecoratorSignature => e.kind === "Decorator"); + + const functions = entities.filter((e): e is FunctionSignature => e.kind === "Function"); + return ( - - - - {(signature) => { - return ; - }} - - - - + 0}> + + + + {(signature) => } + + + + + + 0}> + + + + {(signature) => } + + + + + ); } @@ -70,19 +90,20 @@ export function LocalTypes() { export function generateSignatures( program: Program, - decorators: DecoratorSignature[], + entities: EntitySignature[], libraryName: string, namespaceName: string, ): OutputDirectory { const context = createTspdContext(program); const base = namespaceName === "" ? "__global__" : namespaceName; const $decoratorsRef = refkey(); + const $functionsRef = refkey(); const userLib = ts.createPackage({ name: libraryName, version: "0.0.0", descriptor: { ".": { - named: ["$decorators"], + named: ["$decorators", "$functions"], }, }, }); @@ -91,10 +112,11 @@ export function generateSignatures( - {!base.includes(".Private") && ( @@ -102,10 +124,13 @@ export function generateSignatures( path={`${base}.ts-test.ts`} headerComment="An error in the imports would mean that the decorator is not exported or doesn't have the right name." > - )} diff --git a/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx new file mode 100644 index 00000000000..00ae0a8b782 --- /dev/null +++ b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx @@ -0,0 +1,357 @@ +import { For, join, List, refkey } from "@alloy-js/core"; +import * as ts from "@alloy-js/typescript"; +import { + getSourceLocation, + IntrinsicScalarName, + isArrayModelType, + MixedParameterConstraint, + Model, + Program, + Scalar, + type Type, +} from "@typespec/compiler"; +import { DocTag, SyntaxKind } from "@typespec/compiler/ast"; +import { typespecCompiler } from "../external-packages/compiler.js"; +import { FunctionSignature } from "../types.js"; +import { useTspd } from "./tspd-context.js"; + +export interface FunctionSignatureProps { + signature: FunctionSignature; +} + +/** Render the type of function implementation */ +export function FunctionSignatureType(props: Readonly) { + const { program } = useTspd(); + const func = props.signature.tspFunction; + const parameters: ts.ParameterDescriptor[] = [ + { + name: "program", + type: typespecCompiler.Program, + }, + ...func.parameters.map( + (param): ts.ParameterDescriptor => ({ + // https://github.com/alloy-framework/alloy/issues/144 + name: param.rest ? `...${param.name}` : param.name, + type: param.rest ? ( + <> + ( + {param.type ? ( + + ) : undefined} + )[] + + ) : ( + + ), + optional: param.optional, + }), + ), + ]; + + const returnType = ; + + return ( + + + + ); +} + +/** For a rest param of constraint T[] or valueof T[] return the T or valueof T */ +function extractRestParamConstraint( + program: Program, + constraint: MixedParameterConstraint, +): MixedParameterConstraint | undefined { + let valueType: Type | undefined; + let type: Type | undefined; + if (constraint.valueType) { + if (constraint.valueType.kind === "Model" && isArrayModelType(program, constraint.valueType)) { + valueType = constraint.valueType.indexer.value; + } else { + return undefined; + } + } + if (constraint.type) { + if (constraint.type.kind === "Model" && isArrayModelType(program, constraint.type)) { + type = constraint.type.indexer.value; + } else { + return undefined; + } + } + + return { + entityKind: "MixedParameterConstraint", + type, + valueType, + }; +} + +export interface ParameterTsTypeProps { + constraint: MixedParameterConstraint; +} +export function ParameterTsType({ constraint }: ParameterTsTypeProps) { + if (constraint.type && constraint.valueType) { + return ( + <> + + {" | "} + + + ); + } + if (constraint.valueType) { + return ; + } else if (constraint.type) { + return ; + } + + return typespecCompiler.Type; +} + +function TypeConstraintTSType({ type }: { type: Type }) { + if (type.kind === "Model" && isReflectionType(type)) { + return (typespecCompiler as any)[type.name]; + } else if (type.kind === "Union") { + const variants = [...type.variants.values()].map((x) => x.type); + + if (variants.every((x) => isReflectionType(x))) { + return join( + [...new Set(variants)].map((x) => getCompilerType((x as Model).name)), + { + joiner: " | ", + }, + ); + } else { + return typespecCompiler.Type; + } + } + return typespecCompiler.Type; +} + +function getCompilerType(name: string) { + return (typespecCompiler as any)[name]; +} + +function ValueTsType({ type }: { type: Type }) { + const { program } = useTspd(); + switch (type.kind) { + case "Boolean": + return `${type.value}`; + case "String": + return `"${type.value}"`; + case "Number": + return `${type.value}`; + case "Scalar": + return ; + case "Union": + return join( + [...type.variants.values()].map((x) => ), + { joiner: " | " }, + ); + case "Model": + if (isArrayModelType(program, type)) { + return ( + <> + readonly ( + )[] + + ); + } else if (isReflectionType(type)) { + return getValueOfReflectionType(type); + } else { + // If its exactly the record type use Record instead of the model name. + if (type.indexer && type.name === "Record" && type.namespace?.name === "TypeSpec") { + return ( + <> + Record{" + {">"} + + ); + } + if (type.name) { + return ; + } else { + return ; + } + } + } + return "unknown"; +} + +function LocalTypeReference({ type }: { type: Model }) { + const { addLocalType } = useTspd(); + addLocalType(type); + return ; +} +function ValueOfModelTsType({ model }: { model: Model }) { + return ( + + + + ); +} + +export function ValueOfModelTsInterfaceBody({ model }: { model: Model }) { + return ( + + {model.indexer?.value && ( + } + /> + )} + + {(x) => ( + } + /> + )} + + + ); +} + +function ScalarTsType({ scalar }: { scalar: Scalar }) { + const { program } = useTspd(); + const isStd = program.checker.isStdType(scalar); + if (isStd) { + return getStdScalarTSType(scalar); + } else if (scalar.baseScalar) { + return ; + } else { + return "unknown"; + } +} + +function getStdScalarTSType(scalar: Scalar & { name: IntrinsicScalarName }) { + switch (scalar.name) { + case "numeric": + case "decimal": + case "decimal128": + case "float": + case "integer": + case "int64": + case "uint64": + return typespecCompiler.Numeric; + case "int8": + case "int16": + case "int32": + case "safeint": + case "uint8": + case "uint16": + case "uint32": + case "float64": + case "float32": + return "number"; + case "string": + case "url": + return "string"; + case "boolean": + return "boolean"; + case "plainDate": + case "utcDateTime": + case "offsetDateTime": + case "plainTime": + case "duration": + case "bytes": + return "unknown"; + default: + const _assertNever: never = scalar.name; + return "unknown"; + } +} + +function isReflectionType(type: Type): type is Model & { namespace: { name: "Reflection" } } { + return ( + type.kind === "Model" && + type.namespace?.name === "Reflection" && + type.namespace?.namespace?.name === "TypeSpec" + ); +} + +function getValueOfReflectionType(type: Model) { + switch (type.name) { + case "EnumMember": + case "Enum": + return typespecCompiler.EnumValue; + case "Model": + return "Record"; + default: + return "unknown"; + } +} + +function getDocComment(type: Type): string { + const docs = type.node?.docs; + if (docs === undefined || docs.length === 0) { + return ""; + } + + const mainContentLines: string[] = []; + const tagLines = []; + for (const doc of docs) { + for (const content of doc.content) { + for (const line of content.text.split("\n")) { + mainContentLines.push(line); + } + } + for (const tag of doc.tags) { + tagLines.push(); + + let first = true; + const hasContentFirstLine = checkIfTagHasDocOnSameLine(tag); + const tagStart = + tag.kind === SyntaxKind.DocParamTag || tag.kind === SyntaxKind.DocTemplateTag + ? `@${tag.tagName.sv} ${tag.paramName.sv}` + : `@${tag.tagName.sv}`; + for (const content of tag.content) { + for (const line of content.text.split("\n")) { + const cleaned = sanitizeDocComment(line); + if (first) { + if (hasContentFirstLine) { + tagLines.push(`${tagStart} ${cleaned}`); + } else { + tagLines.push(tagStart, cleaned); + } + + first = false; + } else { + tagLines.push(cleaned); + } + } + } + } + } + + const docLines = [...mainContentLines, ...(tagLines.length > 0 ? [""] : []), ...tagLines]; + return docLines.join("\n"); +} + +function sanitizeDocComment(doc: string): string { + // Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679 + return doc.replaceAll("@internal", `@_internal`); +} + +function checkIfTagHasDocOnSameLine(tag: DocTag): boolean { + const start = tag.content[0]?.pos; + const end = tag.content[0]?.end; + const file = getSourceLocation(tag.content[0]).file; + + let hasFirstLine = false; + for (let i = start; i < end; i++) { + const ch = file.text[i]; + if (ch === "\n") { + break; + } + // Todo reuse compiler whitespace logic or have a way to get this info from the parser. + if (ch !== " ") { + hasFirstLine = true; + } + } + return hasFirstLine; +} diff --git a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts index 2c8789f48e8..05ee8da2416 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -6,6 +6,7 @@ export const typespecCompiler = createPackage({ descriptor: { ".": { named: [ + "Program", "DecoratorContext", "Type", "Namespace", diff --git a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts index 8e5d57fe102..87d8ec7271b 100644 --- a/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts +++ b/packages/tspd/src/gen-extern-signatures/gen-extern-signatures.ts @@ -19,9 +19,10 @@ import { resolvePath, } from "@typespec/compiler"; import prettier from "prettier"; +import { FunctionType } from "../../../compiler/src/core/types.js"; import { createDiagnostic } from "../ref-doc/lib.js"; -import { generateSignatures } from "./components/decorators-signatures.js"; -import { DecoratorSignature } from "./types.js"; +import { generateSignatures } from "./components/entity-signatures.js"; +import { DecoratorSignature, EntitySignature, FunctionSignature } from "./types.js"; function createSourceLocation(path: string): SourceLocation { return { file: createSourceFile("", path), pos: 0, end: 0 }; @@ -108,8 +109,7 @@ export async function generateExternDecorators( packageName: string, options?: GenerateExternDecoratorOptions, ): Promise> { - const decorators = new Map(); - + const entities = new Map(); const listener: SemanticNodeListener = { decorator(dec) { if ( @@ -119,12 +119,28 @@ export async function generateExternDecorators( return; } const namespaceName = getTypeName(dec.namespace); - let decoratorForNamespace = decorators.get(namespaceName); - if (!decoratorForNamespace) { - decoratorForNamespace = []; - decorators.set(namespaceName, decoratorForNamespace); + let entitiesForNamespace = entities.get(namespaceName); + if (!entitiesForNamespace) { + entitiesForNamespace = []; + entities.set(namespaceName, entitiesForNamespace); } - decoratorForNamespace.push(resolveDecoratorSignature(dec)); + entitiesForNamespace.push(resolveDecoratorSignature(dec)); + }, + function(func) { + if ( + (packageName !== "@typespec/compiler" && + getLocationContext(program, func).type !== "project") || + func.namespace === undefined + ) { + return; + } + const namespaceName = getTypeName(func.namespace); + let entitiesForNamespace = entities.get(namespaceName); + if (!entitiesForNamespace) { + entitiesForNamespace = []; + entities.set(namespaceName, entitiesForNamespace); + } + entitiesForNamespace.push(resolveFunctionSignature(func)); }, }; if (options?.namespaces) { @@ -150,8 +166,8 @@ export async function generateExternDecorators( } const files: Record = {}; - for (const [ns, nsDecorators] of decorators.entries()) { - const output = generateSignatures(program, nsDecorators, packageName, ns); + for (const [ns, nsEntities] of entities.entries()) { + const output = generateSignatures(program, nsEntities, packageName, ns); const rawFiles: OutputFile[] = []; traverseOutput(output, { visitDirectory: () => {}, @@ -169,9 +185,19 @@ export async function generateExternDecorators( function resolveDecoratorSignature(decorator: Decorator): DecoratorSignature { return { + kind: "Decorator", decorator, name: decorator.name, jsName: "$" + decorator.name.slice(1), typeName: decorator.name[1].toUpperCase() + decorator.name.slice(2) + "Decorator", }; } + +function resolveFunctionSignature(func: FunctionType): FunctionSignature { + return { + kind: "Function", + tspFunction: func, + name: func.name, + typeName: func.name[0].toUpperCase() + func.name.slice(1) + "FunctionImplementation", + }; +} diff --git a/packages/tspd/src/gen-extern-signatures/types.ts b/packages/tspd/src/gen-extern-signatures/types.ts index f40343d7e01..9e35b1fe901 100644 --- a/packages/tspd/src/gen-extern-signatures/types.ts +++ b/packages/tspd/src/gen-extern-signatures/types.ts @@ -1,6 +1,10 @@ -import type { Decorator } from "../../../compiler/src/core/types.js"; +import type { Decorator, FunctionType } from "../../../compiler/src/core/types.js"; + +export type EntitySignature = DecoratorSignature | FunctionSignature; export interface DecoratorSignature { + kind: Decorator["kind"]; + /** Decorator name ()`@example `@foo`) */ name: string; @@ -12,3 +16,15 @@ export interface DecoratorSignature { decorator: Decorator; } + +export interface FunctionSignature { + kind: FunctionType["kind"]; + + /** Function name */ + name: string; + + /** TypeScript type name (@example `FooFunction`) */ + typeName: string; + + tspFunction: FunctionType; +} diff --git a/packages/tspd/src/ref-doc/utils/type-signature.ts b/packages/tspd/src/ref-doc/utils/type-signature.ts index cd4e42984c0..eb30d76aba7 100644 --- a/packages/tspd/src/ref-doc/utils/type-signature.ts +++ b/packages/tspd/src/ref-doc/utils/type-signature.ts @@ -14,6 +14,7 @@ import { UnionVariant, } from "@typespec/compiler"; import { TemplateParameterDeclarationNode } from "@typespec/compiler/ast"; +import { FunctionType } from "../../../../compiler/src/core/types.js"; /** @internal */ export function getTypeSignature(type: Type): string { @@ -59,6 +60,8 @@ export function getTypeSignature(type: Type): string { return `(union variant) ${getUnionVariantSignature(type)}`; case "Tuple": return `(tuple) [${type.values.map(getTypeSignature).join(", ")}]`; + case "Function": + return getFunctionSignature(type); default: const _assertNever: never = type; compilerAssert(false, "Unexpected type kind"); @@ -84,6 +87,12 @@ function getDecoratorSignature(type: Decorator) { return signature; } +function getFunctionSignature(type: FunctionType) { + const ns = getQualifier(type.namespace); + const parameters = [...type.parameters].map((x) => getFunctionParameterSignature(x)); + return `fn ${ns}${type.name}(${parameters.join(", ")}): ${getEntityName(type.returnType)}`; +} + function getInterfaceSignature(type: Interface) { const ns = getQualifier(type.namespace); From 0ed17d6b5570b0c8378d2275d8ccced4da6dc3d2 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 12:57:41 -0400 Subject: [PATCH 05/30] Fix exhaustiveness check in html-program-viewer --- packages/html-program-viewer/src/react/type-config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/html-program-viewer/src/react/type-config.ts b/packages/html-program-viewer/src/react/type-config.ts index 1066c137a37..b5f0d0f5dba 100644 --- a/packages/html-program-viewer/src/react/type-config.ts +++ b/packages/html-program-viewer/src/react/type-config.ts @@ -118,6 +118,11 @@ export const TypeConfig: TypeGraphConfig = buildConfig({ implementation: "skip", target: "ref", }, + Function: { + parameters: "nested-items", + returnType: "ref", + implementation: "skip", + }, ScalarConstructor: { scalar: "parent", parameters: "nested-items", From bf0214cb7eab8cbb0fa8c91bfbf1dff2902ab80b Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 13:08:25 -0400 Subject: [PATCH 06/30] Again fix html-program-viewer --- packages/html-program-viewer/src/react/type-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/html-program-viewer/src/react/type-config.ts b/packages/html-program-viewer/src/react/type-config.ts index b5f0d0f5dba..3b5d93f4f53 100644 --- a/packages/html-program-viewer/src/react/type-config.ts +++ b/packages/html-program-viewer/src/react/type-config.ts @@ -57,6 +57,7 @@ export const TypeConfig: TypeGraphConfig = buildConfig({ unions: "nested-items", enums: "nested-items", decoratorDeclarations: "nested-items", + functionDeclarations: "nested-items", }, Interface: { operations: "nested-items", From fb367e37b2cbf60e99baca66443c865bf250240c Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 13:15:13 -0400 Subject: [PATCH 07/30] Finish removing remnants of extern alias --- packages/compiler/src/core/checker.ts | 13 +++---------- packages/compiler/src/core/messages.ts | 21 --------------------- packages/compiler/src/core/name-resolver.ts | 3 --- packages/compiler/src/core/parser.ts | 16 ++++++++-------- packages/compiler/test/parser.test.ts | 2 +- 5 files changed, 12 insertions(+), 43 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 8169b2396f0..fcc87ee3340 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1637,11 +1637,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker templateNode.kind === SyntaxKind.OperationStatement && templateNode.parent!.kind === SyntaxKind.InterfaceStatement ? getSymbolLinksForMember(templateNode as MemberNode) - : getSymbolLinks( - templateNode.kind === SyntaxKind.AliasStatement - ? getMergedSymbol(templateNode.symbol) - : templateNode.symbol, - ); + : getSymbolLinks(templateNode.symbol); compilerAssert( symbolLinks, @@ -3558,7 +3554,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker */ function checkTemplateDeclaration(node: TemplateableNode, mapper: TypeMapper | undefined) { // If mapper is undefined it means we are checking the declaration of the template. - if (mapper === undefined && node.templateParameters) { + if (mapper === undefined) { for (const templateParameter of node.templateParameters) { checkTemplateParameterDeclaration(templateParameter, undefined); } @@ -4312,7 +4308,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkCallExpression( node: CallExpressionNode, mapper: TypeMapper | undefined, - ): Type | Value | IndeterminateEntity | null { + ): Type | Value | null { const target = checkCallExpressionTarget(node, mapper); if (target === null) { return null; @@ -5457,13 +5453,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const links = getSymbolLinks(node.symbol); if (links.declaredType && mapper === undefined) { - // We are not instantiating this alias and it's already checked. return links.declaredType; } checkTemplateDeclaration(node, mapper); const aliasSymId = getNodeSym(node); - if (pendingResolutions.has(aliasSymId, ResolutionKind.Type)) { if (mapper === undefined) { reportCheckerDiagnostic( @@ -5479,7 +5473,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } pendingResolutions.start(aliasSymId, ResolutionKind.Type); - const type = checkNode(node.value, mapper); if (type === null) { links.declaredType = errorType; diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 524c9853408..3b492adb505 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1015,27 +1015,6 @@ const diagnostics = { }, }, - "alias-no-value": { - severity: "error", - messages: { - default: paramMessage`Alias type '${"typeName"}' must have a value or be marked as 'extern'.`, - }, - }, - - "alias-extern-value": { - severity: "error", - messages: { - default: paramMessage`Alias type '${"typeName"}' cannot have a value when marked as 'extern'.`, - }, - }, - - "alias-extern-no-impl": { - severity: "error", - messages: { - default: paramMessage`Alias type '${"typeName"}' marked as 'extern' must have an associated JS implementation.`, - }, - }, - // #region Visibility "visibility-sealed": { severity: "error", diff --git a/packages/compiler/src/core/name-resolver.ts b/packages/compiler/src/core/name-resolver.ts index 4590a9cbb90..4af6b59be47 100644 --- a/packages/compiler/src/core/name-resolver.ts +++ b/packages/compiler/src/core/name-resolver.ts @@ -1100,8 +1100,6 @@ export function createResolver(program: Program): NameResolver { mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Decorator); } else if (sourceBinding.flags & SymbolFlags.Function) { mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Function); - } else if (sourceBinding.flags & SymbolFlags.Alias) { - mergeDeclarationOrImplementation(key, sourceBinding, target, SymbolFlags.Alias); } else { target.set(key, sourceBinding); } @@ -1119,7 +1117,6 @@ export function createResolver(program: Program): NameResolver { target.set(key, sourceBinding); return; } - const isSourceImplementation = sourceBinding.flags & SymbolFlags.Implementation; const isTargetImplementation = targetBinding.flags & SymbolFlags.Implementation; if (!isTargetImplementation && isSourceImplementation) { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index fc535561ea1..2814b16c07a 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -456,6 +456,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.EnumKeyword: item = parseEnumStatement(pos, decorators); break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + item = parseAliasStatement(pos); + break; case Token.ConstKeyword: reportInvalidDecorators(decorators, "const statement"); item = parseConstStatement(pos); @@ -468,10 +472,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "empty statement"); item = parseEmptyStatement(pos); break; - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; // Start of declaration with modifiers case Token.ExternKeyword: case Token.FnKeyword: @@ -556,6 +556,10 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa case Token.EnumKeyword: item = parseEnumStatement(pos, decorators); break; + case Token.AliasKeyword: + reportInvalidDecorators(decorators, "alias statement"); + item = parseAliasStatement(pos); + break; case Token.ConstKeyword: reportInvalidDecorators(decorators, "const statement"); item = parseConstStatement(pos); @@ -564,10 +568,6 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa reportInvalidDecorators(decorators, "using statement"); item = parseUsingStatement(pos); break; - case Token.AliasKeyword: - reportInvalidDecorators(decorators, "alias statement"); - item = parseAliasStatement(pos); - break; case Token.ExternKeyword: case Token.FnKeyword: case Token.DecKeyword: diff --git a/packages/compiler/test/parser.test.ts b/packages/compiler/test/parser.test.ts index 6b7e7a32ede..ed444712d13 100644 --- a/packages/compiler/test/parser.test.ts +++ b/packages/compiler/test/parser.test.ts @@ -622,7 +622,7 @@ describe("compiler: parser", () => { function getNode(astNode: TypeSpecScriptNode): Node { const statement = astNode.statements[0]; strictEqual(statement.kind, SyntaxKind.AliasStatement); - return statement.value!; + return statement.value; } function getStringTemplateNode(astNode: TypeSpecScriptNode): StringTemplateExpressionNode { const node = getNode(astNode); From dea4f00bf8fb7521336b103585c8b4fc8fc016ea Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 17:26:24 -0400 Subject: [PATCH 08/30] Implement arg/return checking. --- packages/compiler/src/core/checker.ts | 206 ++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fcc87ee3340..c6143e9b277 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -10,7 +10,7 @@ import { createTupleToArrayValueCodeFix, } from "./compiler-code-fixes/convert-to-value.codefix.js"; import { getDeprecationDetails, markDeprecated } from "./deprecation.js"; -import { compilerAssert, ignoreDiagnostics } from "./diagnostics.js"; +import { compilerAssert, createDiagnosticCollector, ignoreDiagnostics } from "./diagnostics.js"; import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator-utils.js"; import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { typeReferenceToString } from "./helpers/syntax-utils.js"; @@ -57,6 +57,7 @@ import { DecoratorDeclarationStatementNode, DecoratorExpressionNode, Diagnostic, + DiagnosticResult, DiagnosticTarget, DocContent, Entity, @@ -4342,11 +4343,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: FunctionType, mapper: TypeMapper | undefined, ): Type | Value | null { - const [hadError, resolvedArgs] = checkFunctionCallArguments(node.arguments, target, mapper); + const [satisfied, resolvedArgs] = checkFunctionCallArguments(node.arguments, target, mapper); - const result = hadError ? errorType : target.implementation(program, ...resolvedArgs); + const result = !satisfied ? errorType : target.implementation(program, ...resolvedArgs); - if (!hadError) checkFunctionReturn(target, result); + if (satisfied) checkFunctionReturn(target, result, node); return result; } @@ -4355,25 +4356,200 @@ export function createChecker(program: Program, resolver: NameResolver): Checker args: Expression[], target: FunctionType, mapper: TypeMapper | undefined, - ): [boolean, (Type | Value)[]] { - return [false, args.map((arg) => checkNode(arg, mapper))] as [boolean, (Type | Value)[]]; + ): [boolean, (Type | Value | undefined)[]] { + if (args.length < target.parameters.filter((p) => !p.optional && !p.rest).length) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + messageId: "atLeast", + format: { actual: args.length.toString(), expected: target.parameters.length.toString() }, + target: target.node!, + }), + ); + return [false, []]; + } + + const collector = createDiagnosticCollector(); + + const resolvedArgs: (Type | Value | undefined)[] = []; + let satisfied = true; + + let idx = 0; + + for (const param of target.parameters) { + if (param.rest) { + const constraint = extractRestParamConstraint(param.type); + + if (!constraint) { + satisfied = false; + continue; + } + + const restArgs = args + .slice(idx) + .map((arg) => getTypeOrValueForNode(arg, mapper, { kind: "argument", constraint })); + + if (restArgs.some((x) => x === null)) { + satisfied = false; + continue; + } + + resolvedArgs.push(...(restArgs as (Value | Type)[])); + } else { + const arg = args[idx++]; + + if (!arg) { + if (param.optional) { + resolvedArgs.push(undefined); + continue; + } else { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument", + messageId: "default", + // TODO: render constraint + format: { value: "undefined", expected: "TODO" }, + target: target.node!, + }), + ); + satisfied = false; + continue; + } + } + + // Normal param + const checkedArg = getTypeOrValueForNode(arg, mapper, { + kind: "argument", + constraint: param.type, + }); + + if (!checkedArg) { + satisfied = false; + continue; + } + + const resolved = collector.pipe( + checkEntityAssignableToConstraint(checkedArg, param.type, args[idx]), + ); + + if (!resolved) { + satisfied = false; + continue; + } + + resolvedArgs.push(resolved); + } + } + + reportCheckerDiagnostics(collector.diagnostics); + + return [satisfied, resolvedArgs]; } - function checkFunctionReturn(target: FunctionType, result: Type | Value) { - if (target.returnType.valueType) { - if (result.entityKind !== "Value") { - reportCheckerDiagnostic( + function checkFunctionReturn(target: FunctionType, result: Type | Value, diagnosticTarget: Node) { + const [_, diagnostics] = checkEntityAssignableToConstraint( + result, + target.returnType, + diagnosticTarget, + ); + + reportCheckerDiagnostics(diagnostics); + } + + function checkEntityAssignableToConstraint( + entity: Type | Value | IndeterminateEntity, + constraint: MixedParameterConstraint, + diagnosticTarget: Node, + ): DiagnosticResult { + const constraintIsValue = !!constraint.valueType; + + const collector = createDiagnosticCollector(); + + if (constraintIsValue) { + const normed = collector.pipe(normalizeValue(entity, constraint)); + + // Error should have been reported in normalizeValue + if (!normed) return collector.wrap(null); + + const assignable = collector.pipe( + relation.isValueOfType(normed, constraint.valueType, diagnosticTarget), + ); + + return collector.wrap(assignable ? normed : null); + } else { + // Constraint is a type + + if (entity.entityKind !== "Type") { + collector.add( createDiagnostic({ - code: "expect-value", - messageId: "functionReturn", - format: { name: getTypeName(result) }, - target: target, + code: "value-in-type", + format: { name: getTypeName(entity.type) }, + target: diagnosticTarget, }), ); + return collector.wrap(null); + } - return; + compilerAssert( + constraint.type, + "Expected type constraint to be defined when known not to be a value constraint.", + ); + + const assignable = collector.pipe( + relation.isTypeAssignableTo(entity, constraint.type, diagnosticTarget), + ); + + return collector.wrap(assignable ? entity : null); + } + } + + function normalizeValue( + entity: Type | Value | IndeterminateEntity, + constraint: MixedParameterConstraint, + ): DiagnosticResult { + if (entity.entityKind === "Value") return [entity, []]; + + if (entity.entityKind === "Indeterminate") { + // Coerce to a value + const coerced = getValueFromIndeterminate( + entity.type, + constraint.type && { kind: "argument", type: constraint.type }, + entity.type.node!, + ); + + if (coerced?.entityKind !== "Value") { + return [ + null, + [ + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity.type) }, + target: entity.type, + }), + ], + ]; } + + return [coerced, []]; } + + if (entity.entityKind === "Type") { + return [ + null, + [ + createDiagnostic({ + code: "expect-value", + format: { name: getTypeName(entity) }, + target: entity, + }), + ], + ]; + } + + compilerAssert( + false, + `Unreachable: unexpected entity kind '${(entity satisfies never as Entity).entityKind}'`, + ); } function checkTypeOfExpression(node: TypeOfExpressionNode, mapper: TypeMapper | undefined): Type { From c59461491b470efea29358f062e3ac653f8a3056 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 17:26:57 -0400 Subject: [PATCH 09/30] Rebuild all .ts-test.ts files. --- packages/events/generated-defs/TypeSpec.Events.ts-test.ts | 2 +- packages/http/generated-defs/TypeSpec.Http.ts-test.ts | 2 +- .../json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts | 2 +- packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts | 2 +- packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts | 2 +- packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts | 2 +- packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts | 2 +- packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts | 2 +- packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts | 2 +- packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts | 2 +- .../versioning/generated-defs/TypeSpec.Versioning.ts-test.ts | 2 +- packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/events/generated-defs/TypeSpec.Events.ts-test.ts b/packages/events/generated-defs/TypeSpec.Events.ts-test.ts index b49c6638a51..e284767b985 100644 --- a/packages/events/generated-defs/TypeSpec.Events.ts-test.ts +++ b/packages/events/generated-defs/TypeSpec.Events.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecEventsDecorators } from "./TypeSpec.Events.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecEventsDecorators = $decorators["TypeSpec.Events"]; +const _decs: TypeSpecEventsDecorators = $decorators["TypeSpec.Events"]; diff --git a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts index b7f2d8fa8a1..66ef18c6a14 100644 --- a/packages/http/generated-defs/TypeSpec.Http.ts-test.ts +++ b/packages/http/generated-defs/TypeSpec.Http.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecHttpDecorators } from "./TypeSpec.Http.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecHttpDecorators = $decorators["TypeSpec.Http"]; +const _decs: TypeSpecHttpDecorators = $decorators["TypeSpec.Http"]; diff --git a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts index 067cfb7932b..000bbe0594e 100644 --- a/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts +++ b/packages/json-schema/generated-defs/TypeSpec.JsonSchema.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecJsonSchemaDecorators } from "./TypeSpec.JsonSchema.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecJsonSchemaDecorators = $decorators["TypeSpec.JsonSchema"]; +const _decs: TypeSpecJsonSchemaDecorators = $decorators["TypeSpec.JsonSchema"]; diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts index 8f8ecdbf06c..1d30a7518b3 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecOpenAPIDecorators } from "./TypeSpec.OpenAPI.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; +const _decs: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; diff --git a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts index b115637c518..70efc1b2e22 100644 --- a/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts +++ b/packages/openapi3/generated-defs/TypeSpec.OpenAPI.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecOpenAPIDecorators } from "./TypeSpec.OpenAPI.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; +const _decs: TypeSpecOpenAPIDecorators = $decorators["TypeSpec.OpenAPI"]; diff --git a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts index 8439d354b21..6c62a82e7c1 100644 --- a/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts +++ b/packages/protobuf/generated-defs/TypeSpec.Protobuf.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecProtobufDecorators } from "./TypeSpec.Protobuf.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecProtobufDecorators = $decorators["TypeSpec.Protobuf"]; +const _decs: TypeSpecProtobufDecorators = $decorators["TypeSpec.Protobuf"]; diff --git a/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts b/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts index ab1192be9f6..d9238050e6c 100644 --- a/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts +++ b/packages/rest/generated-defs/TypeSpec.Rest.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecRestDecorators } from "./TypeSpec.Rest.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecRestDecorators = $decorators["TypeSpec.Rest"]; +const _decs: TypeSpecRestDecorators = $decorators["TypeSpec.Rest"]; diff --git a/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts b/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts index 515f6e257c9..cc404423ef3 100644 --- a/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts +++ b/packages/spector/generated-defs/TypeSpec.Spector.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecSpectorDecorators } from "./TypeSpec.Spector.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecSpectorDecorators = $decorators["TypeSpec.Spector"]; +const _decs: TypeSpecSpectorDecorators = $decorators["TypeSpec.Spector"]; diff --git a/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts b/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts index dc02b5a7be3..ffd24790e3e 100644 --- a/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts +++ b/packages/sse/generated-defs/TypeSpec.SSE.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecSSEDecorators } from "./TypeSpec.SSE.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecSSEDecorators = $decorators["TypeSpec.SSE"]; +const _decs: TypeSpecSSEDecorators = $decorators["TypeSpec.SSE"]; diff --git a/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts b/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts index db99152fa8d..672d8f183b8 100644 --- a/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts +++ b/packages/streams/generated-defs/TypeSpec.Streams.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecStreamsDecorators } from "./TypeSpec.Streams.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecStreamsDecorators = $decorators["TypeSpec.Streams"]; +const _decs: TypeSpecStreamsDecorators = $decorators["TypeSpec.Streams"]; diff --git a/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts b/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts index dd682efbc45..5e7d70dad0a 100644 --- a/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts +++ b/packages/versioning/generated-defs/TypeSpec.Versioning.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecVersioningDecorators } from "./TypeSpec.Versioning.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecVersioningDecorators = $decorators["TypeSpec.Versioning"]; +const _decs: TypeSpecVersioningDecorators = $decorators["TypeSpec.Versioning"]; diff --git a/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts b/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts index 73d1eb64388..445937659a8 100644 --- a/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts +++ b/packages/xml/generated-defs/TypeSpec.Xml.ts-test.ts @@ -7,4 +7,4 @@ import type { TypeSpecXmlDecorators } from "./TypeSpec.Xml.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ -const _: TypeSpecXmlDecorators = $decorators["TypeSpec.Xml"]; +const _decs: TypeSpecXmlDecorators = $decorators["TypeSpec.Xml"]; From dfe197a45475dc5d2469d688c26a38150d9becba Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 8 Aug 2025 18:49:58 -0400 Subject: [PATCH 10/30] Remove some redundant checks. --- packages/compiler/src/core/checker.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c6143e9b277..322b4fe25d6 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4428,16 +4428,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker continue; } - const resolved = collector.pipe( - checkEntityAssignableToConstraint(checkedArg, param.type, args[idx]), - ); + // const resolved = collector.pipe( + // checkEntityAssignableToConstraint(checkedArg, param.type, args[idx]), + // ); - if (!resolved) { - satisfied = false; - continue; - } + // if (!resolved) { + // satisfied = false; + // continue; + // } - resolvedArgs.push(resolved); + resolvedArgs.push(checkedArg); } } @@ -4466,7 +4466,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const collector = createDiagnosticCollector(); if (constraintIsValue) { - const normed = collector.pipe(normalizeValue(entity, constraint)); + const normed = collector.pipe(normalizeValue(entity, constraint, diagnosticTarget)); // Error should have been reported in normalizeValue if (!normed) return collector.wrap(null); @@ -4506,6 +4506,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function normalizeValue( entity: Type | Value | IndeterminateEntity, constraint: MixedParameterConstraint, + diagnosticTarget: Node, ): DiagnosticResult { if (entity.entityKind === "Value") return [entity, []]; @@ -4524,7 +4525,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker createDiagnostic({ code: "expect-value", format: { name: getTypeName(entity.type) }, - target: entity.type, + target: diagnosticTarget, }), ], ]; @@ -4540,7 +4541,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker createDiagnostic({ code: "expect-value", format: { name: getTypeName(entity) }, - target: entity, + target: diagnosticTarget, }), ], ]; From 01fe5c052842f5faaa3937c5481f24fd1cd1e66c Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 12 Aug 2025 13:08:44 -0400 Subject: [PATCH 11/30] Improved testing, value assignability logic --- packages/compiler/generated-defs/TypeSpec.ts | 3 + packages/compiler/lib/std/main.tsp | 2 + packages/compiler/src/core/checker.ts | 160 +++++++++-- .../src/core/helpers/type-name-utils.ts | 2 + packages/compiler/src/core/js-marshaller.ts | 116 +++++++- packages/compiler/src/core/messages.ts | 10 + packages/compiler/src/core/parser.ts | 2 +- packages/compiler/src/core/types.ts | 12 +- packages/compiler/src/lib/examples.ts | 32 ++- packages/compiler/src/lib/tsp-index.ts | 3 + .../compiler/test/checker/functions.test.ts | 270 ++++++++++++++++++ 11 files changed, 572 insertions(+), 40 deletions(-) create mode 100644 packages/compiler/test/checker/functions.test.ts diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index a9f500be961..a70ad0a888b 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -1160,6 +1160,9 @@ export type TypeSpecDecorators = { export type Example2FunctionImplementation = (program: Program, T: Type) => Type; +export type Foo2FunctionImplementation = (program: Program, v: string) => string; + export type TypeSpecFunctions = { example2: Example2FunctionImplementation; + foo2: Foo2FunctionImplementation; }; diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index 3d02d37c6c9..a23ecde6256 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -7,5 +7,7 @@ import "./visibility.tsp"; namespace TypeSpec { extern fn example2(T: Reflection.Type); + extern fn foo2(v: valueof string): valueof string; + alias Example2 = example2(T); } diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 322b4fe25d6..962d0353185 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -15,7 +15,7 @@ import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator- import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getTypeName } from "./helpers/type-name-utils.js"; -import { marshallTypeForJS } from "./js-marshaller.js"; +import { marshallTypeForJS, unmarshalJsToValue } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { NameResolver } from "./name-resolver.js"; import { Numeric } from "./numeric.js"; @@ -157,6 +157,7 @@ import { UnionVariant, UnionVariantNode, UnknownType, + UnknownValue, UsingStatementNode, Value, ValueWithTemplate, @@ -287,6 +288,9 @@ export interface Checker { /** @internal */ readonly anyType: UnknownType; + /** @internal */ + readonly unknownEntity: IndeterminateEntity; + /** @internal */ stats: CheckerStats; } @@ -351,6 +355,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const unknownType = createAndFinishType({ kind: "Intrinsic", name: "unknown" } as const); const nullType = createAndFinishType({ kind: "Intrinsic", name: "null" } as const); + const unknownEntity: IndeterminateEntity = { + entityKind: "Indeterminate", + type: unknownType, + }; + /** * Set keeping track of node pending type resolution. * Key is the SymId of a node. It can be retrieved with getNodeSymId(node) @@ -380,6 +389,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker errorType, nullType, anyType: unknownType, + unknownEntity, voidType, typePrototype, createType, @@ -672,6 +682,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker switch (type.name) { case "null": return checkNullValue(type as any, constraint, node); + case "unknown": + return checkUnknownValue(type as UnknownType, constraint); } return type; default: @@ -780,6 +792,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // If there were diagnostic reported but we still got a value this means that the value might be invalid. reportCheckerDiagnostics(valueDiagnostics); return result; + } else { + const canBeType = constraint?.constraint.type !== undefined; + // If the node _must_ resolve to a value, we will return it unconstrained, so that we will at least produce + // a value. If it _can_ be a type, we already failed the value constraint, so we return the type as is. + return canBeType ? entity.type : getValueFromIndeterminate(entity.type, undefined, node); } } @@ -864,7 +881,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case SyntaxKind.NeverKeyword: return neverType; case SyntaxKind.UnknownKeyword: - return unknownType; + return unknownEntity; case SyntaxKind.ObjectLiteral: return checkObjectValue(node, mapper, valueConstraint); case SyntaxKind.ArrayLiteral: @@ -1947,7 +1964,8 @@ export function createChecker(program: Program, resolver: NameResolver): Checker entityKind: "MixedParameterConstraint", type: unknownType, } satisfies MixedParameterConstraint), - implementation: implementation ?? (() => errorType), + implementation: + implementation ?? Object.assign(() => errorType, { isDefaultFunctionImplementation: true }), }); namespace.functionDeclarations.set(name, functionType); @@ -4126,6 +4144,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); } + function checkUnknownValue( + unknownType: UnknownType, + constraint: CheckValueConstraint | undefined, + ): UnknownValue | null { + return createValue( + { + entityKind: "Value", + + valueKind: "UnknownValue", + type: neverType, + }, + constraint ? constraint.type : neverType, + ); + } + function checkEnumValue( literalType: EnumMember, constraint: CheckValueConstraint | undefined, @@ -4159,13 +4192,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ) { return target; } else { - reportCheckerDiagnostic( - createDiagnostic({ - code: "non-callable", - format: { type: target.kind }, - target: node.target, - }), - ); + if (!isErrorType(target)) + reportCheckerDiagnostic( + createDiagnostic({ + code: "non-callable", + format: { type: target.kind }, + target: node.target, + }), + ); return null; } } @@ -4345,33 +4379,81 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ): Type | Value | null { const [satisfied, resolvedArgs] = checkFunctionCallArguments(node.arguments, target, mapper); - const result = !satisfied ? errorType : target.implementation(program, ...resolvedArgs); + const canCall = satisfied && !(target.implementation as any).isDefaultFunctionImplementation; + + const functionReturn = canCall + ? target.implementation(program, ...resolvedArgs) + : getDefaultFunctionResult(target.returnType); + + const returnIsTypeOrValue = + typeof functionReturn === "object" && + functionReturn !== null && + "entityKind" in functionReturn && + (functionReturn.entityKind === "Type" || functionReturn.entityKind === "Value"); + + const result = returnIsTypeOrValue + ? (functionReturn as Type | Value) + : unmarshalJsToValue(program, functionReturn, function onInvalid(value) { + // TODO: diagnostic for invalid return value + }); if (satisfied) checkFunctionReturn(target, result, node); return result; } + function getDefaultFunctionResult(constraint: MixedParameterConstraint): Type | Value { + if (constraint.valueType) { + return createValue( + { + valueKind: "UnknownValue", + entityKind: "Value", + type: constraint.valueType, + }, + constraint.valueType, + ); + } else { + compilerAssert( + constraint.type, + "Expected function to have a return type when it did not have a value type constraint", + ); + return constraint.type; + } + } + function checkFunctionCallArguments( args: Expression[], target: FunctionType, mapper: TypeMapper | undefined, - ): [boolean, (Type | Value | undefined)[]] { - if (args.length < target.parameters.filter((p) => !p.optional && !p.rest).length) { + ): [boolean, any[]] { + const minArgs = target.parameters.filter((p) => !p.optional && !p.rest).length; + const maxArgs = target.parameters[target.parameters.length - 1]?.rest + ? undefined + : target.parameters.length; + + if (args.length < minArgs) { reportCheckerDiagnostic( createDiagnostic({ code: "invalid-argument-count", messageId: "atLeast", - format: { actual: args.length.toString(), expected: target.parameters.length.toString() }, + format: { actual: args.length.toString(), expected: minArgs.toString() }, target: target.node!, }), ); return [false, []]; + } else if (maxArgs !== undefined && args.length > maxArgs) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "invalid-argument-count", + format: { actual: args.length.toString(), expected: maxArgs.toString() }, + target: target.node!, + }), + ); } const collector = createDiagnosticCollector(); - const resolvedArgs: (Type | Value | undefined)[] = []; + const resolvedArgs: any[] = []; let satisfied = true; let idx = 0; @@ -4394,7 +4476,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker continue; } - resolvedArgs.push(...(restArgs as (Value | Type)[])); + resolvedArgs.push( + ...restArgs.map((v) => + v !== null && isValue(v) + ? marshallTypeForJS(v, undefined, function onUnknown() { + // TODO: diagnostic for unknown value + }) + : v, + ), + ); } else { const arg = args[idx++]; @@ -4428,16 +4518,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker continue; } - // const resolved = collector.pipe( - // checkEntityAssignableToConstraint(checkedArg, param.type, args[idx]), - // ); + const resolved = collector.pipe( + checkEntityAssignableToConstraint(checkedArg, param.type, arg), + ); - // if (!resolved) { - // satisfied = false; - // continue; - // } + satisfied &&= !!resolved; - resolvedArgs.push(checkedArg); + resolvedArgs.push( + resolved + ? isValue(resolved) + ? marshallTypeForJS(resolved, undefined, function onUnknown() { + // TODO: diagnostic for unknown value + }) + : resolved + : undefined, + ); } } @@ -5246,12 +5341,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return { value: arg, node: argNode, - jsValue: resolveDecoratorArgJsValue( + jsValue: resolveArgumentJsValue( arg, extractValueOfConstraints({ kind: "argument", constraint: perParamType, }), + argNode, ), }; } else { @@ -5325,13 +5421,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return type.kind === "Model" ? type.indexer?.value : undefined; } - function resolveDecoratorArgJsValue( + function resolveArgumentJsValue( value: Type | Value, valueConstraint: CheckValueConstraint | undefined, + diagnosticTarget: Node, ) { if (valueConstraint !== undefined) { if (isValue(value)) { - return marshallTypeForJS(value, valueConstraint.type); + return marshallTypeForJS(value, valueConstraint.type, function onUnknown() { + reportCheckerDiagnostic( + createDiagnostic({ + code: "unknown-value", + messageId: "in-js-argument", + target: diagnosticTarget, + }), + ); + }); } else { return value; } @@ -5711,6 +5816,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case "EnumValue": case "NullValue": case "ScalarValue": + case "UnknownValue": return value; } } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 6b99a90fd60..2624b058f7d 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -82,6 +82,8 @@ function getValuePreview(value: Value, options?: TypeNameOptions): string { return "null"; case "ScalarValue": return `${getTypeName(value.type, options)}.${value.value.name}(${value.value.args.map((x) => getValuePreview(x, options)).join(", ")}})`; + case "UnknownValue": + return "unknown"; } } diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index b6548b8630d..1ff80878e63 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -1,19 +1,24 @@ +import { $ } from "../typekit/index.js"; import { compilerAssert } from "./diagnostics.js"; import { numericRanges } from "./numeric-ranges.js"; import { Numeric } from "./numeric.js"; +import { Program } from "./program.js"; import type { ArrayValue, MarshalledValue, NumericValue, ObjectValue, + ObjectValuePropertyDescriptor, Scalar, Type, + UnknownValue, Value, } from "./types.js"; export function marshallTypeForJS( value: T, valueConstraint: Type | undefined, + onUnknown: (value: UnknownValue) => void, ): MarshalledValue { switch (value.valueKind) { case "BooleanValue": @@ -22,15 +27,18 @@ export function marshallTypeForJS( case "NumericValue": return numericValueToJs(value, valueConstraint) as any; case "ObjectValue": - return objectValueToJs(value) as any; + return objectValueToJs(value, onUnknown) as any; case "ArrayValue": - return arrayValueToJs(value) as any; + return arrayValueToJs(value, onUnknown) as any; case "EnumValue": return value as any; case "NullValue": return null as any; case "ScalarValue": return value as any; + case "UnknownValue": + onUnknown(value); + return null as any; } } @@ -75,13 +83,109 @@ function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined) return type.value; } -function objectValueToJs(type: ObjectValue) { +function objectValueToJs( + type: ObjectValue, + onUnknown: (value: UnknownValue) => void, +): Record { const result: Record = {}; for (const [key, value] of type.properties) { - result[key] = marshallTypeForJS(value.value, undefined); + result[key] = marshallTypeForJS(value.value, undefined, onUnknown); } return result; } -function arrayValueToJs(type: ArrayValue) { - return type.values.map((x) => marshallTypeForJS(x, undefined)); +function arrayValueToJs(type: ArrayValue, onUnknown: (value: UnknownValue) => void) { + return type.values.map((x) => marshallTypeForJS(x, undefined, onUnknown)); +} + +export function unmarshalJsToValue( + program: Program, + value: unknown, + onInvalid: (value: unknown) => void, +): Value { + if ( + typeof value === "object" && + value !== null && + "entityKind" in value && + value.entityKind === "Value" + ) { + return value as Value; + } + + if (value === null || value === undefined) { + return { + entityKind: "Value", + valueKind: "NullValue", + value: null, + type: program.checker.nullType, + }; + } else if (typeof value === "boolean") { + const boolean = program.checker.getStdType("boolean"); + return { + entityKind: "Value", + valueKind: "BooleanValue", + value, + type: boolean, + scalar: boolean, + }; + } else if (typeof value === "string") { + const string = program.checker.getStdType("string"); + return { + entityKind: "Value", + valueKind: "StringValue", + value, + type: string, + scalar: string, + }; + } else if (typeof value === "number") { + const numeric = Numeric(String(value)); + const numericType = program.checker.getStdType("numeric"); + return { + entityKind: "Value", + valueKind: "NumericValue", + value: numeric, + type: $(program).literal.create(value), + scalar: numericType, + }; + } else if (Array.isArray(value)) { + const values: Value[] = []; + const uniqueTypes = new Set(); + + for (const item of value) { + const itemValue = unmarshalJsToValue(program, item, onInvalid); + values.push(itemValue); + uniqueTypes.add(itemValue.type); + } + + return { + entityKind: "Value", + valueKind: "ArrayValue", + type: $(program).array.create($(program).union.create([...uniqueTypes])), + values, + }; + } else if (typeof value === "object" && !("entityKind" in value)) { + const properties: Map = new Map(); + for (const [key, val] of Object.entries(value)) { + properties.set(key, { name: key, value: unmarshalJsToValue(program, val, onInvalid) }); + } + return { + entityKind: "Value", + valueKind: "ObjectValue", + properties, + type: $(program).model.create({ + properties: Object.fromEntries( + [...properties.entries()].map( + ([k, v]) => + [k, $(program).modelProperty.create({ name: k, type: v.value.type })] as const, + ), + ), + }), + }; + } else { + onInvalid(value); + return { + entityKind: "Value", + valueKind: "UnknownValue", + type: program.checker.neverType, + }; + } } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 3b492adb505..8ffa8f3ab87 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1015,6 +1015,16 @@ const diagnostics = { }, }, + "unknown-value": { + severity: "error", + messages: { + default: "The 'unknown' value cannot be used here.", + "in-json": "The 'unknown' value cannot be serialized to JSON.", + "in-js-argument": + "The 'unknown' value cannot be used as an argument to a function or decorator.", + }, + }, + // #region Visibility "visibility-sealed": { severity: "error", diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 2814b16c07a..70261685f0a 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2083,7 +2083,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa let foundOptional = false; for (const [index, item] of parameters.items.entries()) { - if (!item.optional && foundOptional) { + if (!(item.optional || item.rest) && foundOptional) { error({ code: "required-parameter-first", target: item }); continue; } diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index b15e3cdc390..e13b72e6a9b 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -15,6 +15,7 @@ Value extends StringValue ? string : Value extends EnumValue ? EnumMember : Value extends NullValue ? null : Value extends ScalarValue ? Value + : Value extends UnknownValue ? null : Value /** @@ -171,7 +172,8 @@ export interface IndeterminateEntity { | BooleanLiteral | EnumMember | UnionVariant - | NullType; + | NullType + | UnknownType; } export interface IntrinsicType extends BaseType { @@ -325,7 +327,8 @@ export type Value = | ObjectValue | ArrayValue | EnumValue - | NullValue; + | NullValue + | UnknownValue; /** @internal */ export type ValueWithTemplate = Value | TemplateValue; @@ -392,6 +395,9 @@ export interface NullValue extends BaseValue { valueKind: "NullValue"; value: null; } +export interface UnknownValue extends BaseValue { + valueKind: "UnknownValue"; +} /** * This is an internal type that represent a value while in a template declaration. @@ -706,7 +712,7 @@ export interface FunctionType extends BaseType { namespace?: Namespace; parameters: MixedFunctionParameter[]; returnType: MixedParameterConstraint; - implementation: (...args: unknown[]) => Type | Value; + implementation: (...args: unknown[]) => unknown; } export interface FunctionParameterBase extends BaseType { diff --git a/packages/compiler/src/lib/examples.ts b/packages/compiler/src/lib/examples.ts index e52d845c730..81c6e0d846d 100644 --- a/packages/compiler/src/lib/examples.ts +++ b/packages/compiler/src/lib/examples.ts @@ -1,9 +1,12 @@ import { Temporal } from "temporal-polyfill"; import { ignoreDiagnostics } from "../core/diagnostics.js"; +import { reportDiagnostic } from "../core/messages.js"; import type { Program } from "../core/program.js"; import { getProperty } from "../core/semantic-walker.js"; import { isArrayModelType, isUnknownType } from "../core/type-utils.js"; import { + DiagnosticTarget, + NoTarget, type ObjectValue, type Scalar, type ScalarValue, @@ -21,9 +24,16 @@ export function serializeValueAsJson( value: Value, type: Type, encodeAs?: EncodeData, + diagnosticTarget?: DiagnosticTarget | typeof NoTarget, ): unknown { if (type.kind === "ModelProperty") { - return serializeValueAsJson(program, value, type.type, encodeAs ?? getEncode(program, type)); + return serializeValueAsJson( + program, + value, + type.type, + encodeAs ?? getEncode(program, type), + diagnosticTarget, + ); } switch (value.valueKind) { case "NullValue": @@ -43,12 +53,21 @@ export function serializeValueAsJson( type.kind === "Model" && isArrayModelType(program, type) ? type.indexer.value : program.checker.anyType, + /* encodeAs: */ undefined, + diagnosticTarget, ), ); case "ObjectValue": - return serializeObjectValueAsJson(program, value, type); + return serializeObjectValueAsJson(program, value, type, diagnosticTarget); case "ScalarValue": return serializeScalarValueAsJson(program, value, type, encodeAs); + case "UnknownValue": + reportDiagnostic(program, { + code: "unknown-value", + messageId: "in-json", + target: diagnosticTarget ?? value, + }); + return null; } } @@ -89,6 +108,7 @@ function serializeObjectValueAsJson( program: Program, value: ObjectValue, type: Type, + diagnosticTarget?: DiagnosticTarget | typeof NoTarget, ): Record { type = resolveUnions(program, value, type) ?? type; const obj: Record = {}; @@ -99,7 +119,13 @@ function serializeObjectValueAsJson( definition.kind === "ModelProperty" ? resolveEncodedName(program, definition, "application/json") : propValue.name; - obj[name] = serializeValueAsJson(program, propValue.value, definition); + obj[name] = serializeValueAsJson( + program, + propValue.value, + definition, + /* encodeAs: */ undefined, + propValue.node, + ); } } return obj; diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index 69314081710..ebd733fb07c 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -135,6 +135,9 @@ export const $functions = { $(program).tuple.create([$(program).literal.create(COUNTER++), t]), ); }, + foo2(_: Program, v: string): string { + return v + "_PROCESSED"; + }, }, }; diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts new file mode 100644 index 00000000000..e32693832b0 --- /dev/null +++ b/packages/compiler/test/checker/functions.test.ts @@ -0,0 +1,270 @@ +import { ok, strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { Diagnostic, ModelProperty, Namespace, Type } from "../../src/core/types.js"; +import { Program, setTypeSpecNamespace } from "../../src/index.js"; +import { + BasicTestRunner, + TestHost, + createTestHost, + createTestWrapper, + expectDiagnosticEmpty, + expectDiagnostics, +} from "../../src/testing/index.js"; +import { $ } from "../../src/typekit/index.js"; + +/** Helper to assert a function declaration was bound to the js implementation */ +function expectFunction(ns: Namespace, name: string, impl: any) { + const fn = ns.functionDeclarations.get(name); + ok(fn, `Expected function ${name} to be declared.`); + strictEqual(fn.implementation, impl); +} + +describe("compiler: checker: functions", () => { + let testHost: TestHost; + + beforeEach(async () => { + testHost = await createTestHost(); + }); + + describe("declaration", () => { + let runner: BasicTestRunner; + let testJs: Record; + let testImpl: any; + beforeEach(() => { + testImpl = (_program: Program) => undefined; + testJs = { testFn: testImpl }; + testHost.addJsFile("test.js", testJs); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + describe("bind implementation to declaration", () => { + it("defined at root via direct export", async () => { + await runner.compile(` + extern fn testFn(); + `); + expectFunction(runner.program.getGlobalNamespaceType(), "testFn", testImpl); + }); + + it("in a namespace via direct export", async () => { + setTypeSpecNamespace("Foo.Bar", testImpl); + await runner.compile(` + namespace Foo.Bar { extern fn testFn(); } + `); + const ns = runner.program + .getGlobalNamespaceType() + .namespaces.get("Foo") + ?.namespaces.get("Bar"); + ok(ns); + expectFunction(ns, "testFn", testImpl); + }); + + it("defined at root via $functions map", async () => { + const impl = (_p: Program) => undefined; + testJs.$functions = { "": { otherFn: impl } }; + await runner.compile(`extern fn otherFn();`); + expectFunction(runner.program.getGlobalNamespaceType(), "otherFn", impl); + }); + + it("in namespace via $functions map", async () => { + const impl = (_p: Program) => undefined; + testJs.$functions = { "Foo.Bar": { nsFn: impl } }; + await runner.compile(`namespace Foo.Bar { extern fn nsFn(); }`); + const ns = runner.program + .getGlobalNamespaceType() + .namespaces.get("Foo") + ?.namespaces.get("Bar"); + ok(ns); + expectFunction(ns, "nsFn", impl); + }); + }); + + it("errors if function is missing extern modifier", async () => { + const diagnostics = await runner.diagnose(`fn testFn();`); + expectDiagnostics(diagnostics, { + code: "function-extern", + message: "A function declaration must be prefixed with the 'extern' modifier.", + }); + }); + + it("errors if extern function is missing implementation", async () => { + const diagnostics = await runner.diagnose(`extern fn missing();`); + expectDiagnostics(diagnostics, { + code: "missing-implementation", + message: "Extern declaration must have an implementation in JS file.", + }); + }); + + it("errors if rest parameter type is not array", async () => { + const diagnostics = await runner.diagnose(`extern fn f(...rest: string);`); + expectDiagnostics(diagnostics, [ + { + code: "missing-implementation", + message: "Extern declaration must have an implementation in JS file.", + }, + { + code: "rest-parameter-array", + message: "A rest parameter must be of an array type.", + }, + ]); + }); + }); + + describe("usage", () => { + let runner: BasicTestRunner; + let calledArgs: any[] | undefined; + beforeEach(() => { + calledArgs = undefined; + testHost.addJsFile("test.js", { + testFn(program: Program, a: any, b: any, ...rest: any[]) { + calledArgs = [program, a, b, ...rest]; + return a; // Return first arg + }, + sum(program: Program, ...nums: number[]) { + return nums.reduce((a, b) => a + b, 0); + }, + valFirst(program: Program, v: any) { + return v; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + function expectCalledWith(...args: any[]) { + ok(calledArgs, "Function was not called."); + strictEqual(calledArgs.length, 1 + args.length); + for (const [i, v] of args.entries()) { + strictEqual(calledArgs[1 + i], v); + } + } + + it("errors if function not declared", async () => { + const diagnostics = await runner.diagnose(`const X = missing();`); + expectDiagnostics(diagnostics, { + code: "invalid-ref", + message: "Unknown identifier missing", + }); + }); + + it("calls function with arguments", async () => { + await runner.compile( + `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one", "two", "three");`, + ); + expectCalledWith("one", "two", "three"); // program + args, optional b provided + }); + + it("allows omitting optional param", async () => { + await runner.compile( + `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one");`, + ); + expectCalledWith("one", undefined); + }); + + it("allows zero args for rest-only", async () => { + await runner.compile( + `extern fn sum(...nums: valueof int32[]): valueof int32; const S = sum();`, + ); + }); + + it("errors if not enough args", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: valueof string, b: valueof string): valueof string; const X = testFn("one");`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected at least 2 arguments, but got 1.", + }); + }); + + it("errors if too many args", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: valueof string): valueof string; const X = testFn("one", "two");`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected 1 arguments, but got 2.", + }); + }); + + it("errors if too few with rest", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string, ...rest: string[]); alias X = testFn();`, + ); + expectDiagnostics(diagnostics, { + code: "invalid-argument-count", + message: "Expected at least 1 arguments, but got 0.", + }); + }); + + it("errors if argument type mismatch (value)", async () => { + const diagnostics = await runner.diagnose( + `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(123);`, + ); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '123' is not assignable to type 'string'", + }); + }); + + it("errors if passing type where value expected", async () => { + const diagnostics = await runner.diagnose( + `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(string);`, + ); + expectDiagnostics(diagnostics, { + code: "expect-value", + message: "string refers to a type, but is being used as a value here.", + }); + }); + + it("accepts string literal for type param", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string); alias X = testFn("abc");`, + ); + expectDiagnosticEmpty(diagnostics); + }); + + it("accepts arguments matching rest", async () => { + const diagnostics = await runner.diagnose( + `extern fn testFn(a: string, ...rest: string[]); alias X = testFn("a", "b", "c");`, + ); + expectDiagnosticEmpty(diagnostics); + }); + }); + + describe("referencing result type", () => { + it("can use function result in alias", async () => { + testHost.addJsFile("test.js", { + makeArray(program: Program, t: Type) { + return $(program).array.create(t); + }, + }); + const runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn makeArray(T: Reflection.Type); + + alias X = makeArray(string); + + model M { + @test prop: X; + } + `)) as [{ prop: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + ok(prop.type); + ok($(runner.program).array.is(prop.type)); + + const arrayIndexerType = prop.type.indexer.value; + + ok(arrayIndexerType); + ok($(runner.program).scalar.isString(arrayIndexerType)); + }); + }); +}); From 66b3f82054c14a63f78e0637614b2a155de0b907 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 12 Aug 2025 15:41:12 -0400 Subject: [PATCH 12/30] Removed stub examples from compiler. --- cspell.yaml | 2 ++ packages/compiler/generated-defs/TypeSpec.ts | 10 ---------- .../generated-defs/TypeSpec.ts-test.ts | 9 ++------- packages/compiler/lib/std/main.tsp | 8 -------- packages/compiler/src/core/checker.ts | 8 ++++---- packages/compiler/src/core/js-marshaller.ts | 6 +++--- .../compiler/src/experimental/typekit/index.ts | 2 +- packages/compiler/src/index.ts | 2 -- packages/compiler/src/lib/tsp-index.ts | 18 ------------------ .../compiler/test/checker/functions.test.ts | 9 +++++---- 10 files changed, 17 insertions(+), 57 deletions(-) diff --git a/cspell.yaml b/cspell.yaml index df8339252f4..ae8c813fdea 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -134,6 +134,7 @@ words: - lzutf - MACVMIMAGE - MACVMIMAGEM + - marshal - mday - mgmt - mgmtplane @@ -257,6 +258,7 @@ words: - Ungroup - uninstantiated - unioned + - unmarshal - unparented - unprefixed - unprojected diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index a70ad0a888b..b1b70348129 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -8,7 +8,6 @@ import type { Namespace, Numeric, Operation, - Program, Scalar, Type, Union, @@ -1157,12 +1156,3 @@ export type TypeSpecDecorators = { withVisibilityFilter: WithVisibilityFilterDecorator; withLifecycleUpdate: WithLifecycleUpdateDecorator; }; - -export type Example2FunctionImplementation = (program: Program, T: Type) => Type; - -export type Foo2FunctionImplementation = (program: Program, v: string) => string; - -export type TypeSpecFunctions = { - example2: Example2FunctionImplementation; - foo2: Foo2FunctionImplementation; -}; diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index 568630da9c4..4e0ce70e3c3 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -1,15 +1,10 @@ // An error in the imports would mean that the decorator is not exported or // doesn't have the right name. -import { $decorators, $functions } from "../src/index.js"; -import type { TypeSpecDecorators, TypeSpecFunctions } from "./TypeSpec.js"; +import { $decorators } from "../src/index.js"; +import type { TypeSpecDecorators } from "./TypeSpec.js"; /** * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ const _decs: TypeSpecDecorators = $decorators["TypeSpec"]; - -/** - * An error here would mean that the exported function is not using the same signature. Make sure to have export const $funcName: FuncNameFunction = (...) => ... - */ -const _funcs: TypeSpecFunctions = $functions["TypeSpec"]; diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index a23ecde6256..89954904be3 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -3,11 +3,3 @@ import "./types.tsp"; import "./decorators.tsp"; import "./reflection.tsp"; import "./visibility.tsp"; - -namespace TypeSpec { - extern fn example2(T: Reflection.Type); - - extern fn foo2(v: valueof string): valueof string; - - alias Example2 = example2(T); -} diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 962d0353185..fa709244706 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -15,7 +15,7 @@ import { validateInheritanceDiscriminatedUnions } from "./helpers/discriminator- import { explainStringTemplateNotSerializable } from "./helpers/string-template-utils.js"; import { typeReferenceToString } from "./helpers/syntax-utils.js"; import { getEntityName, getTypeName } from "./helpers/type-name-utils.js"; -import { marshallTypeForJS, unmarshalJsToValue } from "./js-marshaller.js"; +import { marshalTypeForJs, unmarshalJsToValue } from "./js-marshaller.js"; import { createDiagnostic } from "./messages.js"; import { NameResolver } from "./name-resolver.js"; import { Numeric } from "./numeric.js"; @@ -4479,7 +4479,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker resolvedArgs.push( ...restArgs.map((v) => v !== null && isValue(v) - ? marshallTypeForJS(v, undefined, function onUnknown() { + ? marshalTypeForJs(v, undefined, function onUnknown() { // TODO: diagnostic for unknown value }) : v, @@ -4527,7 +4527,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker resolvedArgs.push( resolved ? isValue(resolved) - ? marshallTypeForJS(resolved, undefined, function onUnknown() { + ? marshalTypeForJs(resolved, undefined, function onUnknown() { // TODO: diagnostic for unknown value }) : resolved @@ -5428,7 +5428,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ) { if (valueConstraint !== undefined) { if (isValue(value)) { - return marshallTypeForJS(value, valueConstraint.type, function onUnknown() { + return marshalTypeForJs(value, valueConstraint.type, function onUnknown() { reportCheckerDiagnostic( createDiagnostic({ code: "unknown-value", diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index 1ff80878e63..e3b9cad58dc 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -15,7 +15,7 @@ import type { Value, } from "./types.js"; -export function marshallTypeForJS( +export function marshalTypeForJs( value: T, valueConstraint: Type | undefined, onUnknown: (value: UnknownValue) => void, @@ -89,12 +89,12 @@ function objectValueToJs( ): Record { const result: Record = {}; for (const [key, value] of type.properties) { - result[key] = marshallTypeForJS(value.value, undefined, onUnknown); + result[key] = marshalTypeForJs(value.value, undefined, onUnknown); } return result; } function arrayValueToJs(type: ArrayValue, onUnknown: (value: UnknownValue) => void) { - return type.values.map((x) => marshallTypeForJS(x, undefined, onUnknown)); + return type.values.map((x) => marshalTypeForJs(x, undefined, onUnknown)); } export function unmarshalJsToValue( diff --git a/packages/compiler/src/experimental/typekit/index.ts b/packages/compiler/src/experimental/typekit/index.ts index 32408209095..6fd78063a05 100644 --- a/packages/compiler/src/experimental/typekit/index.ts +++ b/packages/compiler/src/experimental/typekit/index.ts @@ -1,4 +1,4 @@ -import { type Typekit, TypekitPrototype } from "../../typekit/define-kit.js"; +import { TypekitPrototype, type Typekit } from "../../typekit/define-kit.js"; import { Realm } from "../realm.js"; /** diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 55ffed61d31..b79466982e7 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -242,8 +242,6 @@ export const $decorators = { }, }; -export { $functions } from "./lib/tsp-index.js"; - export { ensureTrailingDirectorySeparator, getAnyExtensionFromPath, diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index ebd733fb07c..df7645e8335 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -1,7 +1,4 @@ import { TypeSpecDecorators } from "../../generated-defs/TypeSpec.js"; -import { Program } from "../core/program.js"; -import { Type } from "../core/types.js"; -import { $ } from "../typekit/index.js"; import { $discriminator, $doc, @@ -126,19 +123,4 @@ export const $decorators = { } satisfies TypeSpecDecorators, }; -let COUNTER = 0; - -export const $functions = { - TypeSpec: { - example2(program: Program, t: Type): Type { - return $(program).array.create( - $(program).tuple.create([$(program).literal.create(COUNTER++), t]), - ); - }, - foo2(_: Program, v: string): string { - return v + "_PROCESSED"; - }, - }, -}; - export const namespace = "TypeSpec"; diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index e32693832b0..44dbb4be71d 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -122,8 +122,8 @@ describe("compiler: checker: functions", () => { calledArgs = [program, a, b, ...rest]; return a; // Return first arg }, - sum(program: Program, ...nums: number[]) { - return nums.reduce((a, b) => a + b, 0); + sum(program: Program, ...addends: number[]) { + return addends.reduce((a, b) => a + b, 0); }, valFirst(program: Program, v: any) { return v; @@ -166,9 +166,10 @@ describe("compiler: checker: functions", () => { }); it("allows zero args for rest-only", async () => { - await runner.compile( - `extern fn sum(...nums: valueof int32[]): valueof int32; const S = sum();`, + const diagnostics = await runner.diagnose( + `extern fn sum(...addends: valueof int32[]): valueof int32; const S = sum();`, ); + expectDiagnostics(diagnostics, []); }); it("errors if not enough args", async () => { From ed060c98470a87f8c13c63eb839f9b7683b2a870 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 12 Sep 2025 17:48:37 -0400 Subject: [PATCH 13/30] test fixes, start docs --- .../compiler/test/checker/functions.test.ts | 643 ++++++++++++++++++ .../docs/docs/language-basics/functions.md | 124 ++++ 2 files changed, 767 insertions(+) create mode 100644 website/src/content/docs/docs/language-basics/functions.md diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index 44dbb4be71d..e42027177db 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -268,4 +268,647 @@ describe("compiler: checker: functions", () => { ok($(runner.program).scalar.isString(arrayIndexerType)); }); }); + + describe("specific type constraints", () => { + let runner: BasicTestRunner; + let receivedTypes: Type[] = []; + + beforeEach(() => { + receivedTypes = []; + testHost.addJsFile("test.js", { + expectModel(program: Program, model: Type) { + receivedTypes.push(model); + return model; + }, + expectEnum(program: Program, enumType: Type) { + receivedTypes.push(enumType); + return enumType; + }, + expectScalar(program: Program, scalar: Type) { + receivedTypes.push(scalar); + return scalar; + }, + expectUnion(program: Program, union: Type) { + receivedTypes.push(union); + return union; + }, + expectInterface(program: Program, iface: Type) { + receivedTypes.push(iface); + return iface; + }, + expectNamespace(program: Program, ns: Type) { + receivedTypes.push(ns); + return ns; + }, + expectOperation(program: Program, op: Type) { + receivedTypes.push(op); + return op; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("accepts Reflection.Model parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectModel(m: Reflection.Model): Reflection.Model; + model TestModel { x: string; } + alias X = expectModel(TestModel); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Model"); + }); + + it("accepts Reflection.Enum parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectEnum(e: Reflection.Enum): Reflection.Enum; + enum TestEnum { A, B } + alias X = expectEnum(TestEnum); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Enum"); + }); + + it("accepts Reflection.Scalar parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectScalar(s: Reflection.Scalar): Reflection.Scalar; + scalar TestScalar extends string; + alias X = expectScalar(TestScalar); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Scalar"); + }); + + it("accepts Reflection.Union parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectUnion(u: Reflection.Union): Reflection.Union; + alias X = expectUnion(string | int32); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Union"); + }); + + it("accepts Reflection.Interface parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectInterface(i: Reflection.Interface): Reflection.Interface; + interface TestInterface { + testOp(): void; + } + alias X = expectInterface(TestInterface); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Interface"); + }); + + it("accepts Reflection.Namespace parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectNamespace(ns: Reflection.Namespace): Reflection.Namespace; + namespace TestNs {} + alias X = expectNamespace(TestNs); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Namespace"); + }); + + it("accepts Reflection.Operation parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectOperation(oper: Reflection.Operation): Reflection.Operation; + op testOp(): string; + alias X = expectOperation(testOp); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedTypes.length, 1); + strictEqual(receivedTypes[0].kind, "Operation"); + }); + + it("errors when wrong type kind is passed", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectModel(m: Reflection.Model): Reflection.Model; + enum TestEnum { A, B } + alias X = expectModel(TestEnum); + `); + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type 'TestEnum' is not assignable to type 'Model'", + }); + }); + }); + + describe("value marshalling", () => { + let runner: BasicTestRunner; + let receivedValues: any[] = []; + + beforeEach(() => { + receivedValues = []; + testHost.addJsFile("test.js", { + expectString(program: Program, str: string) { + receivedValues.push(str); + return str; + }, + expectNumber(program: Program, num: number) { + receivedValues.push(num); + return num; + }, + expectBoolean(program: Program, bool: boolean) { + receivedValues.push(bool); + return bool; + }, + expectArray(program: Program, arr: any[]) { + receivedValues.push(arr); + return arr; + }, + expectObject(program: Program, obj: Record) { + receivedValues.push(obj); + return obj; + }, + returnInvalidJsValue(program: Program) { + return Symbol("invalid"); // Invalid JS value that can't be unmarshaled + }, + returnComplexObject(program: Program) { + return { + nested: { value: 42 }, + array: [1, "test", true], + mixed: null, + }; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("marshals string values correctly", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectString(s: valueof string): valueof string; + const X = expectString("hello world"); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 1); + strictEqual(receivedValues[0], "hello world"); + }); + + it("marshals numeric values correctly", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectNumber(n: valueof int32): valueof int32; + const X = expectNumber(42); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 1); + strictEqual(receivedValues[0], 42); + }); + + it("marshals boolean values correctly", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectBoolean(b: valueof boolean): valueof boolean; + const X = expectBoolean(true); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 1); + strictEqual(receivedValues[0], true); + }); + + it("marshals array values correctly", async () => { + const diagnostics = await runner.diagnose(` + extern fn expectArray(arr: valueof string[]): valueof string[]; + const X = expectArray(#["a", "b", "c"]); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 1); + ok(Array.isArray(receivedValues[0])); + strictEqual(receivedValues[0].length, 3); + strictEqual(receivedValues[0][0], "a"); + strictEqual(receivedValues[0][1], "b"); + strictEqual(receivedValues[0][2], "c"); + }); + + it("marshals object values correctly", async () => { + // BUG: This test reveals a type system issue where numeric literal 25 is not + // assignable to int32 in object literal context within extern functions. + // The error: Type '{ name: string, age: 25 }' is not assignable to type '{ name: string, age: int32 }' + // Expected: Numeric literal 25 should be assignable to int32 + const diagnostics = await runner.diagnose(` + extern fn expectObject(obj: valueof {name: string, age: int32}): valueof {name: string, age: int32}; + const X = expectObject(#{name: "test", age: 25}); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 1); + strictEqual(typeof receivedValues[0], "object"); + strictEqual(receivedValues[0].name, "test"); + strictEqual(receivedValues[0].age, 25); + }); + + it("handles invalid JS return values gracefully", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnInvalidJsValue(): valueof string; + const X = returnInvalidJsValue(); + `); + // Should not crash, but may produce diagnostics about invalid return value + // The implementation currently has a TODO for this case + }); + + it("unmarshals complex JS objects to values", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnComplexObject(): valueof unknown; + const X = returnComplexObject(); + `); + expectDiagnosticEmpty(_diagnostics); + strictEqual(receivedValues.length, 0); // No input values, only return + }); + }); + + describe("union type constraints", () => { + let runner: BasicTestRunner; + let receivedArgs: any[] = []; + + beforeEach(() => { + receivedArgs = []; + testHost.addJsFile("test.js", { + acceptTypeOrValue(program: Program, arg: any) { + receivedArgs.push(arg); + return arg; + }, + acceptMultipleTypes(program: Program, arg: any) { + receivedArgs.push(arg); + return arg; + }, + acceptMultipleValues(program: Program, arg: any) { + receivedArgs.push(arg); + return arg; + }, + returnTypeOrValue(program: Program, returnType: boolean) { + if (returnType) { + return program.checker.getStdType("string"); + } else { + return "hello"; + } + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("accepts type parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn acceptTypeOrValue(arg: Reflection.Type): Reflection.Type; + + alias TypeResult = acceptTypeOrValue(string); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 1); + }); + + it("accepts value parameter", async () => { + const diagnostics = await runner.diagnose(` + extern fn acceptTypeOrValue(arg: valueof string): valueof string; + + const ValueResult = acceptTypeOrValue("hello"); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 1); + }); + + it("accepts multiple specific types", async () => { + const diagnostics = await runner.diagnose(` + extern fn acceptMultipleTypes(arg: Reflection.Model | Reflection.Enum): Reflection.Model | Reflection.Enum; + + model TestModel {} + enum TestEnum { A } + + alias ModelResult = acceptMultipleTypes(TestModel); + alias EnumResult = acceptMultipleTypes(TestEnum); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 2); + }); + + it("accepts multiple value types", async () => { + const diagnostics = await runner.diagnose(` + extern fn acceptMultipleValues(arg: valueof (string | int32)): valueof (string | int32); + + const StringResult = acceptMultipleValues("test"); + const NumberResult = acceptMultipleValues(42); + `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 2); + strictEqual(receivedArgs[0], "test"); + strictEqual(receivedArgs[1], 42); + }); + + it("errors when argument doesn't match union constraint", async () => { + const diagnostics = await runner.diagnose(` + extern fn acceptMultipleTypes(arg: Reflection.Model | Reflection.Enum): Reflection.Model | Reflection.Enum; + + scalar TestScalar extends string; + alias Result = acceptMultipleTypes(TestScalar); + `); + expectDiagnostics(diagnostics, { + code: "unassignable", + }); + }); + + it("can return type from function", async () => { + const diagnostics = await runner.diagnose(` + extern fn returnTypeOrValue(returnType: valueof boolean): Reflection.Type; + + alias TypeResult = returnTypeOrValue(true); + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("can return value from function", async () => { + const diagnostics = await runner.diagnose(` + extern fn returnTypeOrValue(returnType: valueof boolean): valueof string; + + const ValueResult = returnTypeOrValue(false); + `); + expectDiagnosticEmpty(diagnostics); + }); + }); + + describe("error cases and edge cases", () => { + let runner: BasicTestRunner; + + beforeEach(() => { + testHost.addJsFile("test.js", { + returnWrongEntityKind(program: Program) { + return "string value"; // Returns value when type expected + }, + returnWrongValueType(program: Program) { + return 42; // Returns number when string expected + }, + throwError(program: Program) { + throw new Error("JS error"); + }, + returnUndefined(program: Program) { + return undefined; + }, + returnNull(program: Program) { + return null; + }, + expectNonOptionalAfterOptional(program: Program, opt: any, req: any) { + return req; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./test.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("errors when function returns wrong type kind", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnWrongEntityKind(): Reflection.Type; + alias X = returnWrongEntityKind(); + `); + // Should get diagnostics about type mismatch in return value + // The current implementation has TODO for better error handling + }); + + it("errors when function returns wrong value type", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnWrongValueType(): valueof string; + const X = returnWrongValueType(); + `); + + expectDiagnostics(_diagnostics, { + code: "unassignable", + message: "Type '42' is not assignable to type 'string'", + }); + }); + + it("handles JS function that throws", async () => { + // Wrap in try-catch to handle JS errors gracefully + try { + const _diagnostics = await runner.diagnose(` + extern fn throwError(): Reflection.Type; + alias X = throwError(); + `); + // If we get here, the function didn't throw (unexpected) + } catch (error) { + // Expected - JS function threw an error + ok(error instanceof Error); + strictEqual(error.message, "JS error"); + } + }); + + it("handles undefined return value", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnUndefined(): valueof unknown; + const X = returnUndefined(); + `); + // Should handle undefined appropriately + }); + + it("handles null return value", async () => { + const _diagnostics = await runner.diagnose(` + extern fn returnNull(): valueof unknown; + const X = returnNull(); + `); + // Should handle null appropriately + }); + + it("validates required parameter after optional not allowed in regular param position", async () => { + const _diagnostics = await runner.diagnose(` + extern fn expectNonOptionalAfterOptional(opt?: valueof string, req: valueof string): valueof string; + const X = expectNonOptionalAfterOptional("test"); + `); + // This should be a syntax/declaration error - required params can't follow optional ones + // except when rest parameters are involved + }); + + it("allows rest parameters after optional parameters", async () => { + testHost.addJsFile("rest-after-optional.js", { + restAfterOptional(program: Program, opt: any, ...rest: any[]) { + return rest.length; + }, + }); + const restRunner = createTestWrapper(testHost, { + autoImports: ["./rest-after-optional.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + + const diagnostics = await restRunner.diagnose(` + extern fn restAfterOptional(opt?: valueof string, ...rest: valueof string[]): valueof int32; + const X = restAfterOptional("optional", "rest1", "rest2"); + const Y = restAfterOptional(); + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("errors on empty union constraints", async () => { + // This is likely a parse error, but worth testing behavior + const _diagnostics = await runner.diagnose(` + extern fn emptyUnion(arg: ): unknown; + alias X = emptyUnion(); + `); + // Should get parse error + }); + + it("handles deeply nested type constraints", async () => { + testHost.addJsFile("nested.js", { + processNestedModel(program: Program, model: Type) { + return model; + }, + }); + const nestedRunner = createTestWrapper(testHost, { + autoImports: ["./nested.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + + const _diagnostics = await nestedRunner.diagnose(` + extern fn processNestedModel(m: Reflection.Model): Reflection.Model; + + model Level1 { + level2: { + level3: { + deep: string; + }[]; + }; + } + + alias X = processNestedModel(Level1); + `); + expectDiagnosticEmpty(_diagnostics); + }); + + it("validates function return type matches declared constraint", async () => { + testHost.addJsFile("return-validation.js", { + returnString(program: Program) { + return "hello"; + }, + returnNumber(program: Program) { + return 42; + }, + }); + const returnRunner = createTestWrapper(testHost, { + autoImports: ["./return-validation.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + + const _diagnostics = await returnRunner.diagnose(` + extern fn returnString(): valueof string; + extern fn returnNumber(): valueof string; // Wrong: returns number but declares string + + const X = returnString(); + const Y = returnNumber(); + `); + // Should get diagnostic about return type mismatch for returnNumber + }); + }); + + describe("default function results", () => { + let runner: BasicTestRunner; + + beforeEach(() => { + testHost.addJsFile("missing-impl.js", { + // Intentionally empty - to test default implementations + }); + runner = createTestWrapper(testHost, { + autoImports: ["./missing-impl.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("returns default unknown value for missing value-returning function", async () => { + const diagnostics = await runner.diagnose(` + extern fn missingValueFn(): valueof string; + const X = missingValueFn(); + `); + expectDiagnostics(diagnostics, { + code: "missing-implementation", + }); + }); + + it("returns default type for missing type-returning function", async () => { + const diagnostics = await runner.diagnose(` + extern fn missingTypeFn(): Reflection.Type; + alias X = missingTypeFn(); + `); + expectDiagnostics(diagnostics, { + code: "missing-implementation", + }); + }); + + it("returns appropriate default for union return type", async () => { + const diagnostics = await runner.diagnose(` + extern fn missingUnionFn(): Reflection.Type | valueof string; + const X = missingUnionFn(); + `); + expectDiagnostics(diagnostics, { + code: "missing-implementation", + }); + }); + }); + + describe("template and generic scenarios", () => { + let runner: BasicTestRunner; + + beforeEach(() => { + testHost.addJsFile("templates.js", { + processGeneric(program: Program, type: Type) { + return $(program).array.create(type); + }, + processConstrainedGeneric(program: Program, type: Type) { + return type; + }, + }); + runner = createTestWrapper(testHost, { + autoImports: ["./templates.js"], + autoUsings: ["TypeSpec.Reflection"], + }); + }); + + it("works with template aliases", async () => { + const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn processGeneric(T: Reflection.Type): Reflection.Type; + + alias ArrayOf = processGeneric(T); + + model TestModel { + @test prop: ArrayOf; + } + `)) as [{ prop: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + ok(prop.type); + ok($(runner.program).array.is(prop.type)); + }); + + it("works with constrained templates", async () => { + const diagnostics = await runner.diagnose(` + extern fn processConstrainedGeneric(T: Reflection.Model): Reflection.Model; + + alias ProcessModel = processConstrainedGeneric(T); + + model TestModel {} + alias Result = ProcessModel; + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("errors when template constraint not satisfied", async () => { + const diagnostics = await runner.diagnose(` + extern fn processConstrainedGeneric(T: Reflection.Model): Reflection.Model; + + alias ProcessModel = processConstrainedGeneric(T); + + enum TestEnum { A } + alias Result = ProcessModel; + `); + expectDiagnostics(diagnostics, { + code: "invalid-argument", + }); + }); + }); }); diff --git a/website/src/content/docs/docs/language-basics/functions.md b/website/src/content/docs/docs/language-basics/functions.md new file mode 100644 index 00000000000..ad13fc103f3 --- /dev/null +++ b/website/src/content/docs/docs/language-basics/functions.md @@ -0,0 +1,124 @@ +--- +id: functions +title: Functions +--- + +Functions in TypeSpec allow developers to compute and return types or values based on their inputs. Compared to [decorators](./decorators.md), functions provide an input-output based approach to creating type or value instances, offering more flexibility than decorators for creating new types dynamically. Functions enable complex type manipulation, filtering, and transformation. + +Functions are declared using the `fn` keyword (with the required `extern` modifier, like decorators) and are backed by JavaScript implementations. When a TypeSpec program calls a function, the corresponding JavaScript function is invoked with the provided arguments, and the result is returned as either a Type or a Value depending on the function's declaration. + +## Declaring functions + +Functions are declared using the `extern fn` syntax followed by a name, parameter list, optional return type constraint, and semicolon: + +```typespec +extern fn functionName(param1: Type, param2: valueof string): ReturnType; +``` + +Here are some examples of function declarations: + +```typespec +// No arguments, returns a type (default return constraint is 'unknown') +extern fn createDefaultModel(); + +// Takes a string type, returns a type +extern fn transformModel(input: string); + +// Takes a string value, returns a type +extern fn createFromValue(name: valueof string); + +// Returns a value instead of a type +extern fn getDefaultName(): valueof string; + +// Takes and returns values +extern fn processFilter(filter: valueof Filter): valueof Filter; +``` + +## Calling functions + +Functions are called using standard function call syntax with parentheses. They can be used in type expressions, aliases, and anywhere a type or value is expected: + +```typespec +// Call a function in an alias +alias ProcessedModel = transformModel("input"); + +// Call a function for a default value +model Example { + name: string = getDefaultName(); +} + +// Use in template constraints +alias Filtered = applyFilter(T, F); +``` + +## Return types and constraints + +Functions can return either types or values, controlled by the return type constraint: + +- **No return type specified**: Returns a `Type` (implicitly constrained to `unknown`) +- **`valueof SomeType`**: Returns a value of the specified type +- **Mixed constraints**: `Type | valueof Type` allows returning either types or values + +```typespec +// Returns a type +extern fn makeModel(): Model; + +// Returns a string value +extern fn getName(): valueof string; + +// Can return either a type or value +extern fn flexible(): unknown | (valueof unknown); +``` + +## Parameter types + +Function parameters follow the same rules as decorator parameters: + +- **Type parameters**: Accept TypeScript types (e.g., `param: string`) +- **Value parameters**: Accept runtime values using `valueof` (e.g., `param: valueof string`) +- **Mixed parameters**: Can accept both types and values with union syntax + +```typespec +extern fn process( + model: Model, // Type parameter + name: valueof string, // Value parameter + optional?: string, // Optional type parameter + ...rest: valueof string[] // Rest parameter with values +); +``` + +## Practical examples + +### Type transformation + +```typespec +// Transform a model based on a filter +extern fn applyVisibility(input: Model, visibility: valueof VisibilityFilter): Model; + +const PublicFilter: VisibilityFilter = #{ all: #[Public] }; + +alias PublicModel = applyVisibility(UserModel, PublicFilter); +``` + +### Value computation + +```typespec +// Compute a default value +extern fn computeDefault(fieldType: string): valueof unknown; + +model Config { + timeout: int32 = computeDefault("timeout"); +} +``` + +### Template enhancement + +```typespec +// Replace mutating template patterns with pure functions +extern fn createFilteredType( + input: T, + filter: valueof F +): T; + +alias SafeUser = createFilteredType(User, Filter); +``` From 4431906f7e9665100e31ceb5f263a697ea77d5cb Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 28 Oct 2025 16:05:23 -0400 Subject: [PATCH 14/30] Fixed type checking bug in model relations. --- packages/compiler/src/core/js-marshaller.ts | 2 +- packages/compiler/src/core/type-relation-checker.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index e3b9cad58dc..24b1f56ba98 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -76,7 +76,7 @@ function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined) const asNumber = type.value.asNumber(); compilerAssert( asNumber !== null, - `Numeric value '${type.value.toString()}' is not a able to convert to a number without loosing precision.`, + `Numeric value '${type.value.toString()}' is not a able to convert to a number without losing precision.`, ); return asNumber; } diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index ccd47da6b91..55cb20ed80a 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -704,10 +704,9 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } } - return [ - errors.length === 0 ? Related.true : Related.false, - wrapUnassignableErrors(source, target, errors), - ]; + return errors.length === 0 + ? [Related.true, []] + : [Related.false, wrapUnassignableErrors(source, target, errors)]; } /** If we should check for excess properties on the given model. */ From bd323190c2019ed9956568cbcd989131cfe11323 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 28 Oct 2025 16:33:11 -0400 Subject: [PATCH 15/30] Tweak relation checker logic --- packages/compiler/src/core/type-relation-checker.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index 55cb20ed80a..ae8bb93da5d 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -704,9 +704,11 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T } } - return errors.length === 0 - ? [Related.true, []] - : [Related.false, wrapUnassignableErrors(source, target, errors)]; + if (errors.length === 0) { + return [Related.true, []]; + } else { + return [Related.false, wrapUnassignableErrors(source, target, errors)]; + } } /** If we should check for excess properties on the given model. */ From 19ba562b9e598de519d741c7eb23be3e797e9995 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 30 Oct 2025 10:12:18 -0400 Subject: [PATCH 16/30] WIP unknown value tests --- packages/compiler/src/core/checker.ts | 42 +++++++++- packages/compiler/src/core/types.ts | 84 ++++++++++++++++--- .../compiler/test/checker/functions.test.ts | 12 +++ .../test/checker/values/unknown-value.test.ts | 74 ++++++++++++++++ .../docs/docs/language-basics/functions.md | 27 +++--- .../docs/docs/language-basics/values.md | 17 ++++ 6 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 packages/compiler/test/checker/values/unknown-value.test.ts diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index fa709244706..d12acef9097 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -360,6 +360,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type: unknownType, }; + // Special value representing `valueof void` undefined function returns + const voidValue: Value = { + entityKind: "Value", + valueKind: "NullValue", + value: null, + type: voidType, + }; + /** * Set keeping track of node pending type resolution. * Key is the SymId of a node. It can be retrieved with getNodeSymId(node) @@ -4391,6 +4399,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker "entityKind" in functionReturn && (functionReturn.entityKind === "Type" || functionReturn.entityKind === "Value"); + // special case for when the return value is `undefined` and the return type is `void` or `valueof void`. + if (functionReturn === undefined && isVoidReturn(target.returnType)) { + if (target.returnType.valueType) { + return voidValue; + } else { + return voidType; + } + } + const result = returnIsTypeOrValue ? (functionReturn as Type | Value) : unmarshalJsToValue(program, functionReturn, function onInvalid(value) { @@ -4402,6 +4419,22 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return result; } + function isVoidReturn(constraint: MixedParameterConstraint): boolean { + if (constraint.valueType) { + if (!isVoidType(constraint.valueType)) return false; + } + + if (constraint.type) { + if (!isVoidType(constraint.type)) return false; + } + + return true; + + function isVoidType(type: Type): type is VoidType { + return type.kind === "Intrinsic" && type.name === "void"; + } + } + function getDefaultFunctionResult(constraint: MixedParameterConstraint): Type | Value { if (constraint.valueType) { return createValue( @@ -4528,7 +4561,14 @@ export function createChecker(program: Program, resolver: NameResolver): Checker resolved ? isValue(resolved) ? marshalTypeForJs(resolved, undefined, function onUnknown() { - // TODO: diagnostic for unknown value + satisfied = false; + reportCheckerDiagnostic( + createDiagnostic({ + code: "unknown-value", + messageId: "in-js-argument", + target: arg, + }), + ); }) : resolved : undefined, diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index c290aab96fb..cd735feb931 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -50,12 +50,12 @@ export interface DecoratorApplication { } export interface DecoratorFunction { - (program: DecoratorContext, target: any, ...customArgs: any[]): void; + (context: DecoratorContext, target: any, ...args: any[]): void; namespace?: string; } export interface FunctionImplementation { - (program: Program, ...args: any[]): Type | Value; + (context: FunctionContext, ...args: any[]): Type | Value; } export interface BaseType { @@ -2469,28 +2469,88 @@ export interface DecoratorContext { decoratorTarget: DiagnosticTarget; /** - * Function that can be used to retrieve the target for a parameter at the given index. - * @param paramIndex Parameter index in the typespec - * @example @foo("bar", 123) -> $foo(context, target, arg0: string, arg1: number); - * getArgumentTarget(0) -> target for arg0 - * getArgumentTarget(1) -> target for arg1 + * Helper to get the target for a given argument index. + * @param argIndex Argument index in the decorator call. + * @example + * ```tsp + * @dec("hello", 123) + * model MyModel { } + * ``` + * - `getArgumentTarget(0)` -> target for "hello" + * - `getArgumentTarget(1)` -> target for 123 */ - getArgumentTarget(paramIndex: number): DiagnosticTarget | undefined; + getArgumentTarget(argIndex: number): DiagnosticTarget | undefined; /** - * Helper to call out to another decorator - * @param decorator Other decorator function - * @param args Args to pass to other decorator function + * Helper to call a decorator implementation from within another decorator implementation. + * @param decorator The decorator function to call. + * @param target The target to which the decorator is applied. + * @param args Arguments to pass to the decorator. */ call( decorator: (context: DecoratorContext, target: T, ...args: A) => R, target: T, ...args: A ): R; + + /** + * Helper to call a function implementation from within a decorator implementation. + * @param func The function implementation to call. + * @param args Arguments to pass to the function. + */ + callFunction( + func: (context: FunctionContext, ...args: A) => R, + ...args: A + ): R; } -export interface TemplateContext { +/** + * Context passed to function implementations. + */ +export interface FunctionContext { + /** + * The TypeSpec Program in which the function is evaluated. + */ program: Program; + + /** + * The function call diagnostic target. + */ + functionCallTarget: DiagnosticTarget; + + /** + * Helper to get the target for a given argument index. + * @param argIndex Argument index in the function call. + * @example + * ```tsp + * foo("bar", 123): + * ``` + * - `getArgumentTarget(0)` -> target for "bar" + * - `getArgumentTarget(1)` -> target for 123 + */ + getArgumentTarget(argIndex: number): DiagnosticTarget | undefined; + + /** + * Helper to call a decorator implementation from within a function implementation. + * @param decorator The decorator function to call. + * @param target The target to which the decorator is applied. + * @param args Arguments to pass to the decorator. + */ + callDecorator( + decorator: (context: DecoratorContext, target: T, ...args: A) => R, + target: T, + ...args: A + ): R; + + /** + * Helper to call a function implementation from within another function implementation. + * @param func The function implementation to call. + * @param args Arguments to pass to the function. + */ + callFunction( + func: (context: FunctionContext, ...args: A) => R, + ...args: A + ): R; } export interface EmitContext> { diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index e42027177db..a23d57707e9 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -128,6 +128,10 @@ describe("compiler: checker: functions", () => { valFirst(program: Program, v: any) { return v; }, + voidFn(program: Program, arg: any) { + calledArgs = [program, arg]; + // No return value + }, }); runner = createTestWrapper(testHost, { autoImports: ["./test.js"], @@ -172,6 +176,14 @@ describe("compiler: checker: functions", () => { expectDiagnostics(diagnostics, []); }); + it("accepts function with explicit void return type", async () => { + const diagnostics = await runner.diagnose( + `extern fn voidFn(a: valueof string): valueof void; const X: void = voidFn("test");`, + ); + expectDiagnostics(diagnostics, []); + expectCalledWith("test"); + }); + it("errors if not enough args", async () => { const diagnostics = await runner.diagnose( `extern fn testFn(a: valueof string, b: valueof string): valueof string; const X = testFn("one");`, diff --git a/packages/compiler/test/checker/values/unknown-value.test.ts b/packages/compiler/test/checker/values/unknown-value.test.ts new file mode 100644 index 00000000000..7b1f46a594f --- /dev/null +++ b/packages/compiler/test/checker/values/unknown-value.test.ts @@ -0,0 +1,74 @@ +import { strictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { DecoratorContext, Program, Type, Value } from "../../../src/index.js"; +import { expectDiagnostics } from "../../../src/testing/expect.js"; +import { createTestHost, createTestRunner } from "../../../src/testing/test-host.js"; +import { BasicTestRunner, TestHost } from "../../../src/testing/types.js"; + +describe("invalid uses of unknown value", () => { + let host: TestHost; + let runner: BasicTestRunner; + let observedValue: Value | null = null; + + beforeEach(async () => { + host = await createTestHost(); + host.addJsFile("lib.js", { + $collect: (_context: DecoratorContext, _target: Type, value: Value) => { + observedValue = value; + }, + $functions: { + Items: { + echo: (_: Program, value: Value) => { + observedValue = value; + + return value; + }, + }, + }, + }); + runner = await createTestRunner(host); + }); + + it("cannot be passed to a decorator", async () => { + const diags = await runner.diagnose(` + import "./lib.js"; + + extern dec collect(target: Reflection.Model, value: valueof unknown); + + @collect(unknown) + model Test {} + `); + + strictEqual(observedValue, null); + + expectDiagnostics(diags, [ + { + code: "unknown-value", + message: "The 'unknown' value cannot be used as an argument to a function or decorator.", + severity: "error", + }, + ]); + }); + + it("cannot be passed to a function", async () => { + const diags = await runner.diagnose(` + import "./lib.js"; + + namespace Items { + extern fn echo(value: valueof unknown): valueof unknown; + } + + const x = Items.echo(unknown); + `); + + strictEqual(observedValue, null); + + expectDiagnostics(diags, [ + { + code: "unknown-value", + message: "The 'unknown' value cannot be used as an argument to a function or decorator.", + severity: "error", + }, + ]); + }); +}); diff --git a/website/src/content/docs/docs/language-basics/functions.md b/website/src/content/docs/docs/language-basics/functions.md index ad13fc103f3..0c0c0d0d14f 100644 --- a/website/src/content/docs/docs/language-basics/functions.md +++ b/website/src/content/docs/docs/language-basics/functions.md @@ -80,7 +80,7 @@ Function parameters follow the same rules as decorator parameters: ```typespec extern fn process( - model: Model, // Type parameter + model: Model, // Type parameter name: valueof string, // Value parameter optional?: string, // Optional type parameter ...rest: valueof string[] // Rest parameter with values @@ -95,9 +95,14 @@ extern fn process( // Transform a model based on a filter extern fn applyVisibility(input: Model, visibility: valueof VisibilityFilter): Model; -const PublicFilter: VisibilityFilter = #{ all: #[Public] }; +const PUBLIC_FILTER: VisibilityFilter = #{ any: #[Public] }; -alias PublicModel = applyVisibility(UserModel, PublicFilter); +// Using a template to call a function can be beneficial because templates cache +// their instances. A function _never_ caches its results, so each time `applyVisibility` +// is called, it will run the underlying JavaScript function. By using a template to call +// the function, it ensures that the function is only called once per unique instance +// of the template. +alias PublicModel = applyVisibility(UserModel, PUBLIC_FILTER); ``` ### Value computation @@ -111,14 +116,10 @@ model Config { } ``` -### Template enhancement +## Implementation notes -```typespec -// Replace mutating template patterns with pure functions -extern fn createFilteredType( - input: T, - filter: valueof F -): T; - -alias SafeUser = createFilteredType(User, Filter); -``` +- Function results are _never_ cached, unlike template instances. Calling the same function with the same arguments + multiple times will result in multiple function calls. +- Functions _may_ have side-effects when called; they are not guaranteed to be "pure" functions. Be careful when writing + functions to avoid manipulating the type graph or storing undesirable state (though there is no rule that will prevent + you from doing so). diff --git a/website/src/content/docs/docs/language-basics/values.md b/website/src/content/docs/docs/language-basics/values.md index 879c95bf949..48b17902464 100644 --- a/website/src/content/docs/docs/language-basics/values.md +++ b/website/src/content/docs/docs/language-basics/values.md @@ -103,6 +103,23 @@ const value: string | null = null; The `null` value, like the `null` type, doesn't have any special behavior in the TypeSpec language. It is just the value `null` like that in JSON. +#### The unknown value + +TypeSpec provides a special `unknown` value, which can be used to represent a value that is not known. + +For example, `unknown` can be specified as the default value of a model property. An unknown default value means "there is a default value, but it is _unspecified_" (because its value may depend on service configuration, may change in the future, may be autogenerated in some inexpressible way, or because you simply don't wish to specify what it is). + +```tsp +model Example { + // `field` has a default value that the server will use if it's not specified, + // but what it actually is will be determined by the server in an unspecified + // manner. + field: string = unknown; +} +``` + +When working with values, emitters should treat the `unknown` value as if it could be any value that satisfies the type constraint of its context (in the above example, it may be any string value). + ## Const declarations Const declarations allow storing values in a variable for later reference. Const declarations have an optional type annotation. When the type annotation is absent, the type is inferred from the value by constructing an exact type from the initializer. From 2bdbe0229ef3b3c44ce2c915a25115e9d4f2f8cc Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 09:23:55 -0500 Subject: [PATCH 17/30] Many improvements, fixes, documentation changes, etc. etc. --- packages/compiler/src/core/binder.ts | 2 +- packages/compiler/src/core/checker.ts | 153 +++++++++++------ packages/compiler/src/core/messages.ts | 5 +- packages/compiler/src/core/types.ts | 4 +- .../compiler/test/checker/functions.test.ts | 118 +++++++------ .../test/checker/values/unknown-value.test.ts | 47 ++++++ .../components/function-signature-type.tsx | 4 +- .../external-packages/compiler.ts | 1 + .../extending-typespec/implement-functions.md | 157 ++++++++++++++++++ .../docs/docs/language-basics/functions.md | 29 +++- 10 files changed, 405 insertions(+), 115 deletions(-) create mode 100644 website/src/content/docs/docs/extending-typespec/implement-functions.md diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index eca7673fcde..df08c407ba3 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -134,7 +134,7 @@ export function createBinder(program: Program): Binder { for (const [key, member] of Object.entries(sourceFile.esmExports)) { let name: string; - let kind: "decorator" | "function" | "template"; + let kind: "decorator" | "function"; if (key === "$flags") { const context = getLocationContext(program, sourceFile); if (context.type === "library" || context.type === "project") { diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index d12acef9097..263022e735f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -68,6 +68,7 @@ import { EnumValue, ErrorType, Expression, + FunctionContext, FunctionDeclarationStatementNode, FunctionParameter, FunctionParameterNode, @@ -360,14 +361,6 @@ export function createChecker(program: Program, resolver: NameResolver): Checker type: unknownType, }; - // Special value representing `valueof void` undefined function returns - const voidValue: Value = { - entityKind: "Value", - valueKind: "NullValue", - value: null, - type: voidType, - }; - /** * Set keeping track of node pending type resolution. * Key is the SymId of a node. It can be retrieved with getNodeSymId(node) @@ -4389,8 +4382,10 @@ export function createChecker(program: Program, resolver: NameResolver): Checker const canCall = satisfied && !(target.implementation as any).isDefaultFunctionImplementation; + const ctx = createFunctionContext(program, node); + const functionReturn = canCall - ? target.implementation(program, ...resolvedArgs) + ? target.implementation(ctx, ...resolvedArgs) : getDefaultFunctionResult(target.returnType); const returnIsTypeOrValue = @@ -4401,27 +4396,35 @@ export function createChecker(program: Program, resolver: NameResolver): Checker // special case for when the return value is `undefined` and the return type is `void` or `valueof void`. if (functionReturn === undefined && isVoidReturn(target.returnType)) { - if (target.returnType.valueType) { - return voidValue; - } else { - return voidType; - } + return voidType; } - const result = returnIsTypeOrValue + const unmarshaled = returnIsTypeOrValue ? (functionReturn as Type | Value) : unmarshalJsToValue(program, functionReturn, function onInvalid(value) { - // TODO: diagnostic for invalid return value + let valueSummary = String(value); + if (valueSummary.length > 30) { + valueSummary = valueSummary.slice(0, 27) + "..."; + } + reportCheckerDiagnostic( + createDiagnostic({ + code: "function-return", + messageId: "invalid-value", + format: { value: valueSummary }, + target: node, + }), + ); }); - if (satisfied) checkFunctionReturn(target, result, node); + let result: Type | Value | null = unmarshaled; + if (satisfied) result = checkFunctionReturn(target, unmarshaled, node); return result; } function isVoidReturn(constraint: MixedParameterConstraint): boolean { if (constraint.valueType) { - if (!isVoidType(constraint.valueType)) return false; + return false; } if (constraint.type) { @@ -4459,6 +4462,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: FunctionType, mapper: TypeMapper | undefined, ): [boolean, any[]] { + let satisfied = true; const minArgs = target.parameters.filter((p) => !p.optional && !p.rest).length; const maxArgs = target.parameters[target.parameters.length - 1]?.rest ? undefined @@ -4482,12 +4486,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker target: target.node!, }), ); + // This error doesn't actually prevent us from checking the arguments and evaluating the function. } const collector = createDiagnosticCollector(); const resolvedArgs: any[] = []; - let satisfied = true; let idx = 0; @@ -4500,9 +4504,11 @@ export function createChecker(program: Program, resolver: NameResolver): Checker continue; } - const restArgs = args - .slice(idx) - .map((arg) => getTypeOrValueForNode(arg, mapper, { kind: "argument", constraint })); + const restArgExpressions = args.slice(idx); + + const restArgs = restArgExpressions.map((arg) => + getTypeOrValueForNode(arg, mapper, { kind: "argument", constraint }), + ); if (restArgs.some((x) => x === null)) { satisfied = false; @@ -4510,10 +4516,16 @@ export function createChecker(program: Program, resolver: NameResolver): Checker } resolvedArgs.push( - ...restArgs.map((v) => + ...restArgs.map((v, idx) => v !== null && isValue(v) ? marshalTypeForJs(v, undefined, function onUnknown() { - // TODO: diagnostic for unknown value + reportCheckerDiagnostic( + createDiagnostic({ + code: "unknown-value", + messageId: "in-js-argument", + target: restArgExpressions[idx], + }), + ); }) : v, ), @@ -4526,15 +4538,9 @@ export function createChecker(program: Program, resolver: NameResolver): Checker resolvedArgs.push(undefined); continue; } else { - reportCheckerDiagnostic( - createDiagnostic({ - code: "invalid-argument", - messageId: "default", - // TODO: render constraint - format: { value: "undefined", expected: "TODO" }, - target: target.node!, - }), - ); + // No need to report a diagnostic here because we already reported one for + // invalid argument counts above. + satisfied = false; continue; } @@ -4581,14 +4587,20 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return [satisfied, resolvedArgs]; } - function checkFunctionReturn(target: FunctionType, result: Type | Value, diagnosticTarget: Node) { - const [_, diagnostics] = checkEntityAssignableToConstraint( + function checkFunctionReturn( + target: FunctionType, + result: Type | Value, + diagnosticTarget: Node, + ): Type | Value | null { + const [checked, diagnostics] = checkEntityAssignableToConstraint( result, target.returnType, diagnosticTarget, ); reportCheckerDiagnostics(diagnostics); + + return checked; } function checkEntityAssignableToConstraint( @@ -7050,18 +7062,45 @@ function applyDecoratorToType(program: Program, decApp: DecoratorApplication, ta } } -function createDecoratorContext(program: Program, decApp: DecoratorApplication): DecoratorContext { - function createPassThruContext(program: Program, decApp: DecoratorApplication): DecoratorContext { - return { - program, - decoratorTarget: decApp.node!, - getArgumentTarget: () => decApp.node!, - call: (decorator, target, ...args) => { - return decorator(createPassThruContext(program, decApp), target, ...args); - }, - }; - } +function createPassThruContexts( + program: Program, + target: DiagnosticTarget, +): { + decorator: DecoratorContext; + function: FunctionContext; +} { + const decCtx: DecoratorContext = { + program, + decoratorTarget: target, + getArgumentTarget: () => target, + call: (decorator, target, ...args) => { + return decorator(decCtx, target, ...args); + }, + callFunction(fn, ...args) { + return fn(fnCtx, ...args); + }, + }; + + const fnCtx: FunctionContext = { + program, + functionCallTarget: target, + getArgumentTarget: () => target, + callFunction(fn, ...args) { + return fn(fnCtx, ...args); + }, + callDecorator(decorator, target, ...args) { + return decorator(decCtx, target, ...args); + }, + }; + + return { + decorator: decCtx, + function: fnCtx, + }; +} +function createDecoratorContext(program: Program, decApp: DecoratorApplication): DecoratorContext { + const passthrough = createPassThruContexts(program, decApp.node!); return { program, decoratorTarget: decApp.node!, @@ -7069,7 +7108,27 @@ function createDecoratorContext(program: Program, decApp: DecoratorApplication): return decApp.args[index]?.node; }, call: (decorator, target, ...args) => { - return decorator(createPassThruContext(program, decApp), target, ...args); + return decorator(passthrough.decorator, target, ...args); + }, + callFunction(fn, ...args) { + return fn(passthrough.function, ...args); + }, + }; +} + +function createFunctionContext(program: Program, fnCall: CallExpressionNode): FunctionContext { + const passthrough = createPassThruContexts(program, fnCall); + return { + program, + functionCallTarget: fnCall, + getArgumentTarget: (index: number) => { + return fnCall.arguments[index]; + }, + callDecorator(decorator, target, ...args) { + return decorator(passthrough.decorator, target, ...args); + }, + callFunction(fn, ...args) { + return fn(passthrough.function, ...args); }, }; } diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 8ffa8f3ab87..a182071f74c 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -540,10 +540,11 @@ const diagnostics = { default: "A function declaration must be prefixed with the 'extern' modifier.", }, }, - "function-unsupported": { + "function-return": { severity: "error", messages: { - default: "Function are currently not supported.", + default: "Function implementation returned an invalid result.", + "invalid-value": paramMessage`Function implementation returned invalid JS value '${"value"}'.`, }, }, "missing-implementation": { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index cd735feb931..d75aa1f613a 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -702,7 +702,7 @@ export interface Decorator extends BaseType { namespace: Namespace; target: MixedFunctionParameter; parameters: MixedFunctionParameter[]; - implementation: (...args: unknown[]) => void; + implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; } export interface FunctionType extends BaseType { @@ -712,7 +712,7 @@ export interface FunctionType extends BaseType { namespace?: Namespace; parameters: MixedFunctionParameter[]; returnType: MixedParameterConstraint; - implementation: (...args: unknown[]) => unknown; + implementation: (ctx: FunctionContext, ...args: unknown[]) => unknown; } export interface FunctionParameterBase extends BaseType { diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index a23d57707e9..4fa0288e09a 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -1,6 +1,12 @@ import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Diagnostic, ModelProperty, Namespace, Type } from "../../src/core/types.js"; +import { + Diagnostic, + FunctionContext, + ModelProperty, + Namespace, + Type, +} from "../../src/core/types.js"; import { Program, setTypeSpecNamespace } from "../../src/index.js"; import { BasicTestRunner, @@ -12,6 +18,8 @@ import { } from "../../src/testing/index.js"; import { $ } from "../../src/typekit/index.js"; +// TODO/witemple: thoroughly review this file + /** Helper to assert a function declaration was bound to the js implementation */ function expectFunction(ns: Namespace, name: string, impl: any) { const fn = ns.functionDeclarations.get(name); @@ -31,7 +39,7 @@ describe("compiler: checker: functions", () => { let testJs: Record; let testImpl: any; beforeEach(() => { - testImpl = (_program: Program) => undefined; + testImpl = (ctx: FunctionContext) => undefined; testJs = { testFn: testImpl }; testHost.addJsFile("test.js", testJs); runner = createTestWrapper(testHost, { @@ -62,14 +70,14 @@ describe("compiler: checker: functions", () => { }); it("defined at root via $functions map", async () => { - const impl = (_p: Program) => undefined; + const impl = (_ctx: FunctionContext) => undefined; testJs.$functions = { "": { otherFn: impl } }; await runner.compile(`extern fn otherFn();`); expectFunction(runner.program.getGlobalNamespaceType(), "otherFn", impl); }); it("in namespace via $functions map", async () => { - const impl = (_p: Program) => undefined; + const impl = (_ctx: FunctionContext) => undefined; testJs.$functions = { "Foo.Bar": { nsFn: impl } }; await runner.compile(`namespace Foo.Bar { extern fn nsFn(); }`); const ns = runner.program @@ -118,18 +126,18 @@ describe("compiler: checker: functions", () => { beforeEach(() => { calledArgs = undefined; testHost.addJsFile("test.js", { - testFn(program: Program, a: any, b: any, ...rest: any[]) { - calledArgs = [program, a, b, ...rest]; + testFn(ctx: FunctionContext, a: any, b: any, ...rest: any[]) { + calledArgs = [ctx, a, b, ...rest]; return a; // Return first arg }, - sum(program: Program, ...addends: number[]) { + sum(_ctx: FunctionContext, ...addends: number[]) { return addends.reduce((a, b) => a + b, 0); }, - valFirst(program: Program, v: any) { + valFirst(_ctx: FunctionContext, v: any) { return v; }, - voidFn(program: Program, arg: any) { - calledArgs = [program, arg]; + voidFn(ctx: FunctionContext, arg: any) { + calledArgs = [ctx, arg]; // No return value }, }); @@ -178,7 +186,7 @@ describe("compiler: checker: functions", () => { it("accepts function with explicit void return type", async () => { const diagnostics = await runner.diagnose( - `extern fn voidFn(a: valueof string): valueof void; const X: void = voidFn("test");`, + `extern fn voidFn(a: valueof string): void; alias V = voidFn("test");`, ); expectDiagnostics(diagnostics, []); expectCalledWith("test"); @@ -252,8 +260,8 @@ describe("compiler: checker: functions", () => { describe("referencing result type", () => { it("can use function result in alias", async () => { testHost.addJsFile("test.js", { - makeArray(program: Program, t: Type) { - return $(program).array.create(t); + makeArray(ctx: FunctionContext, t: Type) { + return $(ctx.program).array.create(t); }, }); const runner = createTestWrapper(testHost, { @@ -288,31 +296,31 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedTypes = []; testHost.addJsFile("test.js", { - expectModel(program: Program, model: Type) { + expectModel(_ctx: FunctionContext, model: Type) { receivedTypes.push(model); return model; }, - expectEnum(program: Program, enumType: Type) { + expectEnum(_ctx: FunctionContext, enumType: Type) { receivedTypes.push(enumType); return enumType; }, - expectScalar(program: Program, scalar: Type) { + expectScalar(_ctx: FunctionContext, scalar: Type) { receivedTypes.push(scalar); return scalar; }, - expectUnion(program: Program, union: Type) { + expectUnion(_ctx: FunctionContext, union: Type) { receivedTypes.push(union); return union; }, - expectInterface(program: Program, iface: Type) { + expectInterface(_ctx: FunctionContext, iface: Type) { receivedTypes.push(iface); return iface; }, - expectNamespace(program: Program, ns: Type) { + expectNamespace(_ctx: FunctionContext, ns: Type) { receivedTypes.push(ns); return ns; }, - expectOperation(program: Program, op: Type) { + expectOperation(_ctx: FunctionContext, op: Type) { receivedTypes.push(op); return op; }, @@ -392,7 +400,7 @@ describe("compiler: checker: functions", () => { it("accepts Reflection.Operation parameter", async () => { const diagnostics = await runner.diagnose(` - extern fn expectOperation(oper: Reflection.Operation): Reflection.Operation; + extern fn expectOperation(operation: Reflection.Operation): Reflection.Operation; op testOp(): string; alias X = expectOperation(testOp); `); @@ -524,10 +532,10 @@ describe("compiler: checker: functions", () => { const X = returnInvalidJsValue(); `); // Should not crash, but may produce diagnostics about invalid return value - // The implementation currently has a TODO for this case + // The implementation currently has a TODO/witemple for this case }); - it("unmarshals complex JS objects to values", async () => { + it("unmarshal complex JS objects to values", async () => { const _diagnostics = await runner.diagnose(` extern fn returnComplexObject(): valueof unknown; const X = returnComplexObject(); @@ -544,21 +552,21 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedArgs = []; testHost.addJsFile("test.js", { - acceptTypeOrValue(program: Program, arg: any) { + acceptTypeOrValue(_ctx: FunctionContext, arg: any) { receivedArgs.push(arg); return arg; }, - acceptMultipleTypes(program: Program, arg: any) { + acceptMultipleTypes(_ctx: FunctionContext, arg: any) { receivedArgs.push(arg); return arg; }, - acceptMultipleValues(program: Program, arg: any) { + acceptMultipleValues(_ctx: FunctionContext, arg: any) { receivedArgs.push(arg); return arg; }, - returnTypeOrValue(program: Program, returnType: boolean) { + returnTypeOrValue(ctx: FunctionContext, returnType: boolean) { if (returnType) { - return program.checker.getStdType("string"); + return ctx.program.checker.getStdType("string"); } else { return "hello"; } @@ -653,22 +661,22 @@ describe("compiler: checker: functions", () => { beforeEach(() => { testHost.addJsFile("test.js", { - returnWrongEntityKind(program: Program) { + returnWrongEntityKind(_ctx: FunctionContext) { return "string value"; // Returns value when type expected }, - returnWrongValueType(program: Program) { + returnWrongValueType(_ctx: FunctionContext) { return 42; // Returns number when string expected }, - throwError(program: Program) { + throwError(_ctx: FunctionContext) { throw new Error("JS error"); }, - returnUndefined(program: Program) { + returnUndefined(_ctx: FunctionContext) { return undefined; }, - returnNull(program: Program) { + returnNull(_ctx: FunctionContext) { return null; }, - expectNonOptionalAfterOptional(program: Program, opt: any, req: any) { + expectNonOptionalAfterOptional(_ctx: FunctionContext, _opt: any, req: any) { return req; }, }); @@ -679,21 +687,24 @@ describe("compiler: checker: functions", () => { }); it("errors when function returns wrong type kind", async () => { - const _diagnostics = await runner.diagnose(` + const diagnostics = await runner.diagnose(` extern fn returnWrongEntityKind(): Reflection.Type; alias X = returnWrongEntityKind(); `); // Should get diagnostics about type mismatch in return value - // The current implementation has TODO for better error handling + expectDiagnostics(diagnostics, { + code: "value-in-type", + message: "A value cannot be used as a type.", + }); }); it("errors when function returns wrong value type", async () => { - const _diagnostics = await runner.diagnose(` + const diagnostics = await runner.diagnose(` extern fn returnWrongValueType(): valueof string; const X = returnWrongValueType(); `); - expectDiagnostics(_diagnostics, { + expectDiagnostics(diagnostics, { code: "unassignable", message: "Type '42' is not assignable to type 'string'", }); @@ -720,6 +731,7 @@ describe("compiler: checker: functions", () => { const X = returnUndefined(); `); // Should handle undefined appropriately + expectDiagnosticEmpty(_diagnostics); }); it("handles null return value", async () => { @@ -728,6 +740,7 @@ describe("compiler: checker: functions", () => { const X = returnNull(); `); // Should handle null appropriately + expectDiagnosticEmpty(_diagnostics); }); it("validates required parameter after optional not allowed in regular param position", async () => { @@ -741,7 +754,7 @@ describe("compiler: checker: functions", () => { it("allows rest parameters after optional parameters", async () => { testHost.addJsFile("rest-after-optional.js", { - restAfterOptional(program: Program, opt: any, ...rest: any[]) { + restAfterOptional(_ctx: FunctionContext, opt: any, ...rest: any[]) { return rest.length; }, }); @@ -758,18 +771,9 @@ describe("compiler: checker: functions", () => { expectDiagnosticEmpty(diagnostics); }); - it("errors on empty union constraints", async () => { - // This is likely a parse error, but worth testing behavior - const _diagnostics = await runner.diagnose(` - extern fn emptyUnion(arg: ): unknown; - alias X = emptyUnion(); - `); - // Should get parse error - }); - it("handles deeply nested type constraints", async () => { testHost.addJsFile("nested.js", { - processNestedModel(program: Program, model: Type) { + processNestedModel(_ctx: FunctionContext, model: Type) { return model; }, }); @@ -796,10 +800,10 @@ describe("compiler: checker: functions", () => { it("validates function return type matches declared constraint", async () => { testHost.addJsFile("return-validation.js", { - returnString(program: Program) { + returnString(_ctx: FunctionContext) { return "hello"; }, - returnNumber(program: Program) { + returnNumber(_ctx: FunctionContext) { return 42; }, }); @@ -808,7 +812,7 @@ describe("compiler: checker: functions", () => { autoUsings: ["TypeSpec.Reflection"], }); - const _diagnostics = await returnRunner.diagnose(` + const diagnostics = await returnRunner.diagnose(` extern fn returnString(): valueof string; extern fn returnNumber(): valueof string; // Wrong: returns number but declares string @@ -816,6 +820,10 @@ describe("compiler: checker: functions", () => { const Y = returnNumber(); `); // Should get diagnostic about return type mismatch for returnNumber + expectDiagnostics(diagnostics, { + code: "unassignable", + message: "Type '42' is not assignable to type 'string'", + }); }); }); @@ -868,10 +876,10 @@ describe("compiler: checker: functions", () => { beforeEach(() => { testHost.addJsFile("templates.js", { - processGeneric(program: Program, type: Type) { - return $(program).array.create(type); + processGeneric(ctx: FunctionContext, type: Type) { + return $(ctx.program).array.create(type); }, - processConstrainedGeneric(program: Program, type: Type) { + processConstrainedGeneric(_ctx: FunctionContext, type: Type) { return type; }, }); diff --git a/packages/compiler/test/checker/values/unknown-value.test.ts b/packages/compiler/test/checker/values/unknown-value.test.ts index 7b1f46a594f..784df454592 100644 --- a/packages/compiler/test/checker/values/unknown-value.test.ts +++ b/packages/compiler/test/checker/values/unknown-value.test.ts @@ -72,3 +72,50 @@ describe("invalid uses of unknown value", () => { ]); }); }); + +describe("usage", () => { + let host: TestHost; + let runner: BasicTestRunner; + + beforeEach(async () => { + host = await createTestHost(); + runner = await createTestRunner(host); + }); + + for (const typeDescriptor of [ + "unknown", + "string", + "int32", + "boolean", + "model Foo", + "union Bar", + "enum Baz", + "string[]", + "Record", + ]) { + const type = typeDescriptor.replace(/^(model|union|enum) /, ""); + + it(`can be assigned to variable of type valueof '${typeDescriptor}'`, async () => { + const diags = await runner.diagnose(` + model Foo { + example: string; + } + + union Bar { + foo: Foo; + baz: Baz; + } + + enum Baz { + A, + B, + C, + } + + const x: ${type} = unknown; + `); + + expectDiagnostics(diags, []); + }); + } +}); diff --git a/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx index 00ae0a8b782..dcb630ed411 100644 --- a/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx +++ b/packages/tspd/src/gen-extern-signatures/components/function-signature-type.tsx @@ -25,8 +25,8 @@ export function FunctionSignatureType(props: Readonly) { const func = props.signature.tspFunction; const parameters: ts.ParameterDescriptor[] = [ { - name: "program", - type: typespecCompiler.Program, + name: "context", + type: typespecCompiler.FunctionContext, }, ...func.parameters.map( (param): ts.ParameterDescriptor => ({ diff --git a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts index 05ee8da2416..ce9188c6092 100644 --- a/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts +++ b/packages/tspd/src/gen-extern-signatures/external-packages/compiler.ts @@ -8,6 +8,7 @@ export const typespecCompiler = createPackage({ named: [ "Program", "DecoratorContext", + "FunctionContext", "Type", "Namespace", "Model", diff --git a/website/src/content/docs/docs/extending-typespec/implement-functions.md b/website/src/content/docs/docs/extending-typespec/implement-functions.md new file mode 100644 index 00000000000..ec78eda02ad --- /dev/null +++ b/website/src/content/docs/docs/extending-typespec/implement-functions.md @@ -0,0 +1,157 @@ +--- +id: implement-functions +title: Functions +--- + +TypeSpec functions, like [Decorators](./create-decorators.md), are implemented using JavaScript functions. To provide +a function in your library, you must: + +1. [Declare the function signature in TypeSpec](#declare-the-function-signature). +2. [Implement the function in JavaScript](#implement-the-function-in-javascript). + +## Declare the function signature + +Unlike decorators, declaring a function's signature in TypeSpec is mandatory. A function signature is declared using the +`fn` keyword. Functions are implemented in JavaScript and therefore the signature also requires the `extern` keyword. + +```tsp +extern fn myFn(); +``` + +A function signature can specify a list of parameters and optionally a return type constraint. + +```tsp +/** + * Concatenates to strings, equivalent to `l + r` in JavaScript, where `l` and `r` are strings. + * + * @param l String to be appended first to the result string. + * @param r String to be appended second to the result string. + * @returns the result of concatenating `l` and `r`. + */ +extern fn concat(l: valueof string, r: valueof string): valueof string; +``` + +Type constraints for parameters work exactly the same as constraints for [Decorator](../language-basics/decorators.md). + +### Optional parameters + +You can mark a function parameter as optional using `?`: + +```tsp +/** + * Renames a model, if + */ +extern fn rename(m: Reflection.Model, name?: valueof string): Reflection.Model; +``` + +### Rest parameters + +Functions may also specify "rest" parameters. The rest parameter collects all remaining arguments passed to the function, +and is declared using `...`. The type of a rest parameter _must_ be an array. + +```tsp +/** + * Joins a list of strings, equivalent to `rest.join(sep)` in JavaScript. + * + * @param sep the separator string used to join the list. + * @param rest the list of strings to join + * @returns the list of strings joined by the separator + */ +extern fn join(sep: valueof string, ...rest: valueof string[]): valueof string; +``` + +### Return type constraints + +Functions may optionally specify a return type constraint. The return type constraint is checked when the function is +called, and whatever the function returns must be assignable to the constraint. + +#### Void functions + +The `void` return type is treated specially. A JS implementation for a TypeSpec function that returns `void` may return +_either_ `undefined`, or an instance of the `void` intrinsic type, for compatibility with JavaScript void functions. +Regardless of what the implementation returns, the TypeSpec function call will _always_ evaluate to `void`. + +```tsp +namespace Example; + +extern fn myFn(): void; + +// Calling myFn() is guaranteed to evaluate to the `void` intrinsic type. +``` + +## Implement the function in JavaScript + +Functions must be implemented in a JavaScript library by exporting the functions the library implements using the +`$functions` variable. + +```ts +// lib.ts +import { FunctionContext } from "@typespec/compiler"; + +export const $functions = { + // Namespace + "MyOrg.MyLib": { + concat, + }, +}; + +function concat(context: FunctionContext, l: string, r: string): string { + return l + r; +} +``` + +The function implementation must be imported from TypeSpec to bind to the declaration in the signature: + +```tsp +// lib.tsp +import "./lib.js"; + +namespace MyOrg.MyLib; + +extern fn concat(l: valueof string, r: valueof string): valueof string; +``` + +The first argument passed to a JS function implementation is always the function's _context_, which has type +`FunctionContext`. The context provides information about where the function call was located in TypeSpec source, and +can be used to call other functions or invoke decorators from within the function implementation. + +### Function parameter marshalling + +When function arguments are _Types_, the type is passed to the function as-is. When a function argument is a _value_, +the function implementation receives a JavaScript value with a type that is appropriate for representing that value. + +| TypeSpec value type | Marshalled type in JS | +| ------------------- | --------------------------------- | +| `string` | `string` | +| `boolean` | `boolean` | +| `numeric` | `Numeric` or `number` (see below) | +| `null` | `null` | +| enum member | `EnumValue` | + +When marshalling numeric values, either the `Numeric` wrapper type is used, or a `number` is passed directly, depending on whether the value can be represented as a JavaScript number without precision loss. In particular, the types `numeric`, `integer`, `decimal`, `float`, `int64`, `uint64`, and `decimal128` are marshalled as a `Numeric` type. All other numeric types are marshalled as `number`. + +When marshalling custom scalar subtypes, the marshalling behavior of the known supertype is used. For example, a `scalar customScalar extends numeric` will marshal as a `Numeric`, regardless of any value constraints that might be present. + +### Reporting diagnostics on function calls or arguments + +The function context provides the `functionCallTarget` and `getArgumentTarget` helpers. + +```ts +import type { FunctionContext, Type } from "typespec/compiler"; +import { reportDiagnostic } from "./lib.js"; + +export function renamed(ctx: FunctionContext, model: Model, name: string): Model { + // To report a diagnostic on the function call + reportDiagnostic({ + code: "my-diagnostic-code", + target: ctx.functionCallTarget, + }); + // To report an error on a specific argument (for example the `model`), use the argument target. + // Note: targeting the `model` itself will put the diagnostic on the type's _declaration_, but using + // getArgumentTarget will put it on the _function argument_, which is probably what you want. + reportDiagnostic({ + code: "my-other-code", + target: ctx.getArgumentTarget(0), + }); +} +``` diff --git a/website/src/content/docs/docs/language-basics/functions.md b/website/src/content/docs/docs/language-basics/functions.md index 0c0c0d0d14f..d2e9322ad31 100644 --- a/website/src/content/docs/docs/language-basics/functions.md +++ b/website/src/content/docs/docs/language-basics/functions.md @@ -3,13 +3,23 @@ id: functions title: Functions --- -Functions in TypeSpec allow developers to compute and return types or values based on their inputs. Compared to [decorators](./decorators.md), functions provide an input-output based approach to creating type or value instances, offering more flexibility than decorators for creating new types dynamically. Functions enable complex type manipulation, filtering, and transformation. - -Functions are declared using the `fn` keyword (with the required `extern` modifier, like decorators) and are backed by JavaScript implementations. When a TypeSpec program calls a function, the corresponding JavaScript function is invoked with the provided arguments, and the result is returned as either a Type or a Value depending on the function's declaration. +Functions in TypeSpec allow library developers to compute and return types or +values based on their inputs. Compared to [decorators](./decorators.md), +functions provide an input-output based approach to creating type or value +instances, offering more flexibility than decorators for creating new types +dynamically. Functions enable complex type manipulation, filtering, and +transformation. + +Functions are declared using the `fn` keyword (with the required `extern` +modifier, like decorators) and are backed by JavaScript implementations. When a +TypeSpec program calls a function, the corresponding JavaScript function is +invoked with the provided arguments, and the result is returned as either a Type +or a Value depending on the function's declaration. ## Declaring functions -Functions are declared using the `extern fn` syntax followed by a name, parameter list, optional return type constraint, and semicolon: +Functions are declared using the `extern fn` syntax followed by a name, +parameter list, optional return type constraint, and semicolon: ```typespec extern fn functionName(param1: Type, param2: valueof string): ReturnType; @@ -70,12 +80,19 @@ extern fn getName(): valueof string; extern fn flexible(): unknown | (valueof unknown); ``` +:::note +A function call does not always evaluate to its return type. The function call may evaluate to any _subtype_ +of the return type constraint (any type or value that is _assignable_ to the constraint). For example, a function that +returns `Reflection.Model` may actually evaluate to any model. A function that returns `Foo` where `Foo` is a model may +evaluate to any model that is assignable to `Foo`. +::: + ## Parameter types Function parameters follow the same rules as decorator parameters: - **Type parameters**: Accept TypeScript types (e.g., `param: string`) -- **Value parameters**: Accept runtime values using `valueof` (e.g., `param: valueof string`) +- **Value parameters**: Accept values using `valueof` (e.g., `param: valueof string`) - **Mixed parameters**: Can accept both types and values with union syntax ```typespec @@ -108,7 +125,7 @@ alias PublicModel = applyVisibility(UserModel, PUBLIC_FILTER); ### Value computation ```typespec -// Compute a default value +// Compute a default value using some external logic extern fn computeDefault(fieldType: string): valueof unknown; model Config { From a34ab242c033f34a269a0710c3a798359c3f5a70 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 10:49:40 -0500 Subject: [PATCH 18/30] Fixed completion behavior --- packages/compiler/src/core/checker.ts | 23 ++++++++++++-- .../src/core/helpers/type-name-utils.ts | 2 ++ packages/compiler/src/core/messages.ts | 1 + packages/compiler/src/core/parser.ts | 3 ++ .../compiler/test/checker/functions.test.ts | 31 ++++++++++--------- .../compiler/test/server/completion.test.ts | 1 + .../extending-typespec/create-decorators.md | 2 +- .../docs/docs/language-basics/functions.md | 28 ++++++++--------- 8 files changed, 58 insertions(+), 33 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 263022e735f..591a2637ac9 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -3059,12 +3059,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker case IdentifierKind.Decorator: // Only return decorators and namespaces when completing decorator return !!(sym.flags & (SymbolFlags.Decorator | SymbolFlags.Namespace)); + case IdentifierKind.Function: + // Only return functions and namespaces when completing function calls + return !!(sym.flags & (SymbolFlags.Function | SymbolFlags.Namespace)); case IdentifierKind.Using: // Only return namespaces when completing using return !!(sym.flags & SymbolFlags.Namespace); case IdentifierKind.TypeReference: - // Do not return functions or decorators when completing types - return !(sym.flags & (SymbolFlags.Function | SymbolFlags.Decorator)); + // Do not return decorators when completing types + return !(sym.flags & SymbolFlags.Decorator); case IdentifierKind.TemplateArgument: return !!(sym.flags & SymbolFlags.TemplateParameter); default: @@ -4598,7 +4601,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker diagnosticTarget, ); - reportCheckerDiagnostics(diagnostics); + if (diagnostics.length > 0) { + reportCheckerDiagnostic( + createDiagnostic({ + code: "function-return", + messageId: "unassignable", + format: { + name: getTypeName(target, { printable: true }), + entityKind: result.entityKind.toLowerCase(), + return: getEntityName(result, { printable: true }), + type: getEntityName(target.returnType, { printable: true }), + }, + target: diagnosticTarget, + }), + ); + } return checked; } diff --git a/packages/compiler/src/core/helpers/type-name-utils.ts b/packages/compiler/src/core/helpers/type-name-utils.ts index 01b7173d707..943db461e06 100644 --- a/packages/compiler/src/core/helpers/type-name-utils.ts +++ b/packages/compiler/src/core/helpers/type-name-utils.ts @@ -40,6 +40,8 @@ export function getTypeName(type: Type, options?: TypeNameOptions): string { return getInterfaceName(type, options); case "Operation": return getOperationName(type, options); + case "Function": + return getIdentifierName(type.name, options); case "Enum": return getEnumName(type, options); case "EnumMember": diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index a182071f74c..1c3e0470a59 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -545,6 +545,7 @@ const diagnostics = { messages: { default: "Function implementation returned an invalid result.", "invalid-value": paramMessage`Function implementation returned invalid JS value '${"value"}'.`, + unassignable: paramMessage`Implementation of function '${"name"}' returned ${"entityKind"} '${"return"}', which is not assignable to the declared return type '${"type"}'.`, }, }, "missing-implementation": { diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 70261685f0a..49d61b917c1 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -3306,6 +3306,9 @@ export function getIdentifierContext(id: IdentifierNode): IdentifierContext { case SyntaxKind.DecoratorExpression: kind = IdentifierKind.Decorator; break; + case SyntaxKind.CallExpression: + kind = IdentifierKind.Function; + break; case SyntaxKind.UsingStatement: kind = IdentifierKind.Using; break; diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index 4fa0288e09a..b1edc83cf3a 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -7,7 +7,7 @@ import { Namespace, Type, } from "../../src/core/types.js"; -import { Program, setTypeSpecNamespace } from "../../src/index.js"; +import { setTypeSpecNamespace } from "../../src/index.js"; import { BasicTestRunner, TestHost, @@ -429,30 +429,30 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedValues = []; testHost.addJsFile("test.js", { - expectString(program: Program, str: string) { + expectString(ctx: FunctionContext, str: string) { receivedValues.push(str); return str; }, - expectNumber(program: Program, num: number) { + expectNumber(ctx: FunctionContext, num: number) { receivedValues.push(num); return num; }, - expectBoolean(program: Program, bool: boolean) { + expectBoolean(ctx: FunctionContext, bool: boolean) { receivedValues.push(bool); return bool; }, - expectArray(program: Program, arr: any[]) { + expectArray(ctx: FunctionContext, arr: any[]) { receivedValues.push(arr); return arr; }, - expectObject(program: Program, obj: Record) { + expectObject(ctx: FunctionContext, obj: Record) { receivedValues.push(obj); return obj; }, - returnInvalidJsValue(program: Program) { + returnInvalidJsValue(ctx: FunctionContext) { return Symbol("invalid"); // Invalid JS value that can't be unmarshaled }, - returnComplexObject(program: Program) { + returnComplexObject(ctx: FunctionContext) { return { nested: { value: 42 }, array: [1, "test", true], @@ -693,8 +693,9 @@ describe("compiler: checker: functions", () => { `); // Should get diagnostics about type mismatch in return value expectDiagnostics(diagnostics, { - code: "value-in-type", - message: "A value cannot be used as a type.", + code: "function-return", + message: + "Implementation of function 'returnWrongEntityKind' returned value '\"string value\"', which is not assignable to the declared return type 'Type'.", }); }); @@ -705,8 +706,9 @@ describe("compiler: checker: functions", () => { `); expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '42' is not assignable to type 'string'", + code: "function-return", + message: + "Implementation of function 'returnWrongValueType' returned value '42', which is not assignable to the declared return type 'valueof string'.", }); }); @@ -821,8 +823,9 @@ describe("compiler: checker: functions", () => { `); // Should get diagnostic about return type mismatch for returnNumber expectDiagnostics(diagnostics, { - code: "unassignable", - message: "Type '42' is not assignable to type 'string'", + code: "function-return", + message: + "Implementation of function 'returnNumber' returned value '42', which is not assignable to the declared return type 'valueof string'.", }); }); }); diff --git a/packages/compiler/test/server/completion.test.ts b/packages/compiler/test/server/completion.test.ts index aca984ec5ee..321daee906f 100644 --- a/packages/compiler/test/server/completion.test.ts +++ b/packages/compiler/test/server/completion.test.ts @@ -21,6 +21,7 @@ describe("complete statement keywords", () => { ["op", true], ["extern", true], ["dec", true], + ["fn", true], ["alias", true], ["namespace", true], ["import", true], diff --git a/website/src/content/docs/docs/extending-typespec/create-decorators.md b/website/src/content/docs/docs/extending-typespec/create-decorators.md index 380b7e8e2ff..04ac86ee03c 100644 --- a/website/src/content/docs/docs/extending-typespec/create-decorators.md +++ b/website/src/content/docs/docs/extending-typespec/create-decorators.md @@ -211,7 +211,7 @@ export const $lib = createTypeSpecLibrary({ export const StateKeys = $lib.stateKeys; ``` -### Reporting diagnostic on decorator or arguments +### Reporting diagnostics on decorator or arguments The decorator context provides the `decoratorTarget` and `getArgumentTarget` helpers. diff --git a/website/src/content/docs/docs/language-basics/functions.md b/website/src/content/docs/docs/language-basics/functions.md index d2e9322ad31..1c6fb5e3e12 100644 --- a/website/src/content/docs/docs/language-basics/functions.md +++ b/website/src/content/docs/docs/language-basics/functions.md @@ -3,23 +3,20 @@ id: functions title: Functions --- -Functions in TypeSpec allow library developers to compute and return types or -values based on their inputs. Compared to [decorators](./decorators.md), -functions provide an input-output based approach to creating type or value -instances, offering more flexibility than decorators for creating new types -dynamically. Functions enable complex type manipulation, filtering, and -transformation. - -Functions are declared using the `fn` keyword (with the required `extern` -modifier, like decorators) and are backed by JavaScript implementations. When a -TypeSpec program calls a function, the corresponding JavaScript function is -invoked with the provided arguments, and the result is returned as either a Type -or a Value depending on the function's declaration. +Functions in TypeSpec allow library developers to compute and return types or values based on their inputs. Compared to +[decorators](./decorators.md), functions provide an input-output based approach to creating type or value instances, +offering more flexibility than decorators for creating new types dynamically. Functions enable complex type +manipulation, filtering, and transformation. + +Functions are declared using the `fn` keyword (with the required `extern` modifier, like decorators) and are backed by +JavaScript implementations. When a TypeSpec program calls a function, the corresponding JavaScript function is invoked +with the provided arguments, and the result is returned as either a Type or a Value depending on the function's +declaration. ## Declaring functions -Functions are declared using the `extern fn` syntax followed by a name, -parameter list, optional return type constraint, and semicolon: +Functions are declared using the `extern fn` syntax followed by a name, parameter list, optional return type constraint, +and semicolon: ```typespec extern fn functionName(param1: Type, param2: valueof string): ReturnType; @@ -46,7 +43,8 @@ extern fn processFilter(filter: valueof Filter): valueof Filter; ## Calling functions -Functions are called using standard function call syntax with parentheses. They can be used in type expressions, aliases, and anywhere a type or value is expected: +Functions are called using standard function call syntax with parentheses. They can be used in type expressions, aliases, +and anywhere a type or value is expected: ```typespec // Call a function in an alias From 4adf661f8e986e018ed583c4f217c78842835763 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 11:38:55 -0500 Subject: [PATCH 19/30] Tested semantic walker. --- packages/compiler/src/core/types.ts | 44 +++++++++++++++++-- .../compiler/test/semantic-walker.test.ts | 26 ++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index d75aa1f613a..757a7c3366f 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -705,34 +705,72 @@ export interface Decorator extends BaseType { implementation: (ctx: DecoratorContext, target: Type, ...args: unknown[]) => void; } +/** + * A function (`fn`) declared in the TypeSpec program. + */ export interface FunctionType extends BaseType { kind: "Function"; node?: FunctionDeclarationStatementNode; + /** + * The function's name as declared in the TypeSpec source. + */ name: string; - namespace?: Namespace; + /** + * The namespace in which this function was declared. + */ + namespace: Namespace; + /** + * The parameters of the function. + */ parameters: MixedFunctionParameter[]; + /** + * The return type constraint of the function. + */ returnType: MixedParameterConstraint; + /** + * The JavaScript implementation of the function. + * + * @internal + */ implementation: (ctx: FunctionContext, ...args: unknown[]) => unknown; } export interface FunctionParameterBase extends BaseType { kind: "FunctionParameter"; node?: FunctionParameterNode; + /** + * The name of this function parameter, as declared in the TypeSpec source. + */ name: string; + /** + * Whether this parameter is optional. + */ optional: boolean; + /** + * Whether this parameter is a rest parameter (i.e., `...args`). + */ rest: boolean; } -/** Represent a function parameter that could accept types or values in the TypeSpec program. */ +/** + * A function parameter with a mixed parameter constraint that could accept a value. + */ export interface MixedFunctionParameter extends FunctionParameterBase { mixed: true; type: MixedParameterConstraint; } -/** Represent a function parameter that represent the parameter signature(i.e the type would be the type of the value passed) */ + +/** + * A function parameter with a simple type constraint. + */ export interface SignatureFunctionParameter extends FunctionParameterBase { mixed: false; type: Type; } + +/** + * A function parameter. + */ export type FunctionParameter = MixedFunctionParameter | SignatureFunctionParameter; export interface Sym { diff --git a/packages/compiler/test/semantic-walker.test.ts b/packages/compiler/test/semantic-walker.test.ts index 81e4e464567..07428e136d1 100644 --- a/packages/compiler/test/semantic-walker.test.ts +++ b/packages/compiler/test/semantic-walker.test.ts @@ -7,6 +7,7 @@ import { navigateType, navigateTypesInNamespace, } from "../src/core/semantic-walker.js"; +import { FunctionType } from "../src/core/types.js"; import { Enum, Interface, @@ -50,6 +51,7 @@ describe("compiler: semantic walker", () => { namespaces: [] as Namespace[], exitNamespaces: [] as Namespace[], operations: [] as Operation[], + functions: [] as FunctionType[], exitOperations: [] as Operation[], tuples: [] as Tuple[], exitTuples: [] as Tuple[], @@ -72,6 +74,10 @@ describe("compiler: semantic walker", () => { result.operations.push(x); return customListener?.operation?.(x); }, + function: (x) => { + result.functions.push(x); + return customListener?.function?.(x); + }, exitOperation: (x) => { result.exitOperations.push(x); return customListener?.exitOperation?.(x); @@ -141,7 +147,14 @@ describe("compiler: semantic walker", () => { customListener?: SemanticNodeListener, options?: NavigationOptions, ) { - host.addTypeSpecFile("main.tsp", typespec); + host.addJsFile("main.js", { + $functions: { + Extern: { + foo() {}, + }, + }, + }); + host.addTypeSpecFile("main.tsp", `import "./main.js";\n\n${typespec}`); await host.compile("main.tsp", { nostdlib: true }); @@ -692,5 +705,16 @@ describe("compiler: semantic walker", () => { expect(results.models).toHaveLength(2); }); + + it("include functions", async () => { + const results = await runNavigator(` + namespace Extern; + + extern fn foo(): string; + `); + + expect(results.functions).toHaveLength(1); + expect(results.functions[0].name).toBe("foo"); + }); }); }); From eed1eac1ad4e3454bb646b2926170ccc4ed6545d Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 11:58:33 -0500 Subject: [PATCH 20/30] Unwind Reflection.Type because it's the same thing as 'unknown' --- packages/compiler/lib/std/reflection.tsp | 1 - .../compiler/src/core/type-relation-checker.ts | 4 ++-- .../compiler/test/checker/functions.test.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/compiler/lib/std/reflection.tsp b/packages/compiler/lib/std/reflection.tsp index 12bb602c034..f1142b2e738 100644 --- a/packages/compiler/lib/std/reflection.tsp +++ b/packages/compiler/lib/std/reflection.tsp @@ -11,4 +11,3 @@ model Scalar {} model Union {} model UnionVariant {} model StringTemplate {} -model Type {} diff --git a/packages/compiler/src/core/type-relation-checker.ts b/packages/compiler/src/core/type-relation-checker.ts index ae8bb93da5d..385228c0c7a 100644 --- a/packages/compiler/src/core/type-relation-checker.ts +++ b/packages/compiler/src/core/type-relation-checker.ts @@ -100,7 +100,7 @@ const ReflectionNameToKind = { UnionVariant: "UnionVariant", } as const satisfies Record; -type ReflectionTypeName = keyof typeof ReflectionNameToKind | "Type"; +type ReflectionTypeName = keyof typeof ReflectionNameToKind; export function createTypeRelationChecker(program: Program, checker: Checker): TypeRelation { return { @@ -511,7 +511,7 @@ export function createTypeRelationChecker(program: Program, checker: Checker): T if (isVoidType(target)) return isVoidType(source); if (isUnknownType(target)) return true; if (isReflectionType(target)) { - return target.name === "Type" || source.kind === ReflectionNameToKind[target.name]; + return source.kind === ReflectionNameToKind[target.name]; } if (target.kind === "Scalar") { diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index b1edc83cf3a..cca7cf98791 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -269,7 +269,7 @@ describe("compiler: checker: functions", () => { autoUsings: ["TypeSpec.Reflection"], }); const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` - extern fn makeArray(T: Reflection.Type); + extern fn makeArray(T: unknown); alias X = makeArray(string); @@ -580,7 +580,7 @@ describe("compiler: checker: functions", () => { it("accepts type parameter", async () => { const diagnostics = await runner.diagnose(` - extern fn acceptTypeOrValue(arg: Reflection.Type): Reflection.Type; + extern fn acceptTypeOrValue(arg: unknown): unknown; alias TypeResult = acceptTypeOrValue(string); `); @@ -639,7 +639,7 @@ describe("compiler: checker: functions", () => { it("can return type from function", async () => { const diagnostics = await runner.diagnose(` - extern fn returnTypeOrValue(returnType: valueof boolean): Reflection.Type; + extern fn returnTypeOrValue(returnType: valueof boolean): unknown; alias TypeResult = returnTypeOrValue(true); `); @@ -688,14 +688,14 @@ describe("compiler: checker: functions", () => { it("errors when function returns wrong type kind", async () => { const diagnostics = await runner.diagnose(` - extern fn returnWrongEntityKind(): Reflection.Type; + extern fn returnWrongEntityKind(): unknown; alias X = returnWrongEntityKind(); `); // Should get diagnostics about type mismatch in return value expectDiagnostics(diagnostics, { code: "function-return", message: - "Implementation of function 'returnWrongEntityKind' returned value '\"string value\"', which is not assignable to the declared return type 'Type'.", + "Implementation of function 'returnWrongEntityKind' returned value '\"string value\"', which is not assignable to the declared return type 'unknown'.", }); }); @@ -716,7 +716,7 @@ describe("compiler: checker: functions", () => { // Wrap in try-catch to handle JS errors gracefully try { const _diagnostics = await runner.diagnose(` - extern fn throwError(): Reflection.Type; + extern fn throwError(): unknown; alias X = throwError(); `); // If we get here, the function didn't throw (unexpected) @@ -855,7 +855,7 @@ describe("compiler: checker: functions", () => { it("returns default type for missing type-returning function", async () => { const diagnostics = await runner.diagnose(` - extern fn missingTypeFn(): Reflection.Type; + extern fn missingTypeFn(): unknown; alias X = missingTypeFn(); `); expectDiagnostics(diagnostics, { @@ -865,7 +865,7 @@ describe("compiler: checker: functions", () => { it("returns appropriate default for union return type", async () => { const diagnostics = await runner.diagnose(` - extern fn missingUnionFn(): Reflection.Type | valueof string; + extern fn missingUnionFn(): unknown | valueof string; const X = missingUnionFn(); `); expectDiagnostics(diagnostics, { @@ -894,7 +894,7 @@ describe("compiler: checker: functions", () => { it("works with template aliases", async () => { const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` - extern fn processGeneric(T: Reflection.Type): Reflection.Type; + extern fn processGeneric(T: unknown): unknown; alias ArrayOf = processGeneric(T); From cb905720685b96fa2406fb1a007c81ba401d8406 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 12:14:18 -0500 Subject: [PATCH 21/30] Add callDecorator to decctx --- packages/compiler/src/core/checker.ts | 10 +- packages/compiler/src/core/types.ts | 24 +++- pnpm-lock.yaml | 151 +++++++++++--------------- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 271bdc3ed2a..562939620d8 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -7167,6 +7167,9 @@ function createPassThruContexts( decoratorTarget: target, getArgumentTarget: () => target, call: (decorator, target, ...args) => { + return decCtx.callDecorator(decorator, target, ...args); + }, + callDecorator(decorator, target, ...args) { return decorator(decCtx, target, ...args); }, callFunction(fn, ...args) { @@ -7194,19 +7197,24 @@ function createPassThruContexts( function createDecoratorContext(program: Program, decApp: DecoratorApplication): DecoratorContext { const passthrough = createPassThruContexts(program, decApp.node!); - return { + const decCtx: DecoratorContext = { program, decoratorTarget: decApp.node!, getArgumentTarget: (index: number) => { return decApp.args[index]?.node; }, call: (decorator, target, ...args) => { + return decCtx.callDecorator(decorator, target, ...args); + }, + callDecorator: (decorator, target, ...args) => { return decorator(passthrough.decorator, target, ...args); }, callFunction(fn, ...args) { return fn(passthrough.function, ...args); }, }; + + return decCtx; } function createFunctionContext(program: Program, fnCall: CallExpressionNode): FunctionContext { diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 3bb7f230626..f54450ffb23 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -733,7 +733,13 @@ export interface FunctionType extends BaseType { /** * The JavaScript implementation of the function. * - * @internal + * WARNING: Calling the implementation function directly is dangerous. It assumes that you have marshaled the arguments + * to JS values correctly and that you will handle the return value appropriately. Constructing the correct context + * is your responsibility (use the `call + * + * @param ctx - The FunctionContext providing information about the call site. + * @param args - The arguments passed to the function. + * @returns The return value of the function, which is arbitrary. */ implementation: (ctx: FunctionContext, ...args: unknown[]) => unknown; } @@ -2548,6 +2554,9 @@ export interface DecoratorContext { /** * Helper to call a decorator implementation from within another decorator implementation. + * + * This function is identical to `callDecorator`. + * * @param decorator The decorator function to call. * @param target The target to which the decorator is applied. * @param args Arguments to pass to the decorator. @@ -2558,6 +2567,19 @@ export interface DecoratorContext { ...args: A ): R; + /** + * Helper to call a decorator implementation from within another decorator implementation. + * + * @param decorator The decorator function to call. + * @param target The target to which the decorator is applied. + * @param args Arguments to pass to the decorator. + */ + callDecorator( + decorator: (context: DecoratorContext, target: T, ...args: A) => R, + target: T, + ...args: A + ): R; + /** * Helper to call a function implementation from within a decorator implementation. * @param func The function implementation to call. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e63b555f0ba..70af81dcfc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 - rollup: 4.49.0 - importers: .: @@ -44,10 +40,10 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/eslint-plugin': specifier: ^1.1.38 - version: 1.3.25(eslint@9.38.0)(typescript@5.9.3)(vitest@4.0.4) + version: 1.3.25(eslint@9.38.0)(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) c8: specifier: ^10.1.3 version: 10.1.3 @@ -124,7 +120,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -185,7 +181,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -234,7 +230,7 @@ importers: version: 7.7.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -280,7 +276,7 @@ importers: version: 17.0.34 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -374,7 +370,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -484,7 +480,7 @@ importers: version: 8.46.2 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -520,7 +516,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -590,7 +586,7 @@ importers: version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -638,7 +634,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -884,7 +880,7 @@ importers: version: link:../versioning '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -957,7 +953,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1100,7 +1096,7 @@ importers: version: 17.0.34 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1146,7 +1142,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1179,7 +1175,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1207,7 +1203,7 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1264,7 +1260,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1343,7 +1339,7 @@ importers: version: link:../xml '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1377,7 +1373,7 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1616,7 +1612,7 @@ importers: version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1683,7 +1679,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1744,7 +1740,7 @@ importers: version: 5.1.0(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1789,7 +1785,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1862,7 +1858,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -1920,7 +1916,7 @@ importers: version: 0.4.14 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2130,7 +2126,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2170,7 +2166,7 @@ importers: version: 24.9.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2218,7 +2214,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2319,7 +2315,7 @@ importers: version: link:../prettier-plugin-typespec '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2376,7 +2372,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2451,7 +2447,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2484,7 +2480,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.4(vitest@4.0.4) + version: 4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/ui': specifier: ^4.0.4 version: 4.0.4(vitest@4.0.4) @@ -2647,7 +2643,7 @@ importers: version: 6.0.1 vite-plugin-node-polyfills: specifier: ^0.24.0 - version: 0.24.0(rollup@4.49.0)(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.24.0(rollup@4.49.0)(vite@6.4.1(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)) packages: @@ -5437,7 +5433,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -5448,7 +5444,7 @@ packages: resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -5457,7 +5453,7 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -7697,6 +7693,10 @@ packages: engines: {node: '>=20'} hasBin: true + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -11307,7 +11307,7 @@ packages: engines: {node: '>= 14.18.0'} readline-sync@1.4.9: - resolution: {integrity: sha512-mp5h1N39kuKbCRGebLPIKTBOhuDw55GaNg5S+K9TW9uDAS1wIHpGUc2YokdUMZJb8GqS49sWmWEDijaESYh0Hg==} + resolution: {integrity: sha1-PtqOZfI80qF+YTAbHwADOWr17No=} engines: {node: '>= 0.8.0'} realpath-missing@1.1.0: @@ -11531,7 +11531,7 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 4.49.0 + rollup: 2.x || 3.x || 4.x peerDependenciesMeta: rolldown: optional: true @@ -18687,7 +18687,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.4(vitest@4.0.4)': + '@vitest/coverage-v8@4.0.4(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.4 @@ -18704,7 +18704,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.3.25(eslint@9.38.0)(typescript@5.9.3)(vitest@4.0.4)': + '@vitest/eslint-plugin@1.3.25(eslint@9.38.0)(typescript@5.9.3)(vitest@4.0.4(@types/debug@4.1.12)(@types/node@24.9.1)(@vitest/ui@4.0.4)(happy-dom@20.0.8)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/utils': 8.46.2(eslint@9.38.0)(typescript@5.9.3) @@ -19084,7 +19084,7 @@ snapshots: '@yarnpkg/libui@3.0.2(ink@3.2.0(@types/react@19.2.2)(react@17.0.2))(react@17.0.2)': dependencies: - ink: 3.2.0(@types/react@19.2.2)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.2)(react@17.0.2) react: 17.0.2 tslib: 2.8.1 @@ -19365,7 +19365,7 @@ snapshots: '@yarnpkg/plugin-git': 3.1.3(@yarnpkg/core@4.4.4(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.41.0 - ink: 3.2.0(@types/react@19.2.2)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.2)(react@17.0.2) react: 17.0.2 semver: 7.7.3 tslib: 2.8.1 @@ -19399,7 +19399,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.6 + cross-spawn: 7.0.3 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -20588,6 +20588,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -22622,7 +22628,7 @@ snapshots: ink-text-input@4.0.3(ink@3.2.0(@types/react@19.2.2)(react@17.0.2))(react@17.0.2): dependencies: chalk: 4.1.2 - ink: 3.2.0(@types/react@19.2.2)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.2)(react@17.0.2) react: 17.0.2 type-fest: 0.15.1 @@ -22658,38 +22664,6 @@ snapshots: - bufferutil - utf-8-validate - ink@3.2.0(@types/react@19.2.2)(react@18.3.1): - dependencies: - ansi-escapes: 4.3.2 - auto-bind: 4.0.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - cli-cursor: 3.1.0 - cli-truncate: 2.1.0 - code-excerpt: 3.0.0 - indent-string: 4.0.0 - is-ci: 2.0.0 - lodash: 4.17.21 - patch-console: 1.0.0 - react: 18.3.1 - react-devtools-core: 4.28.5 - react-reconciler: 0.26.2(react@18.3.1) - scheduler: 0.20.2 - signal-exit: 3.0.7 - slice-ansi: 3.0.0 - stack-utils: 2.0.6 - string-width: 4.2.3 - type-fest: 0.12.0 - widest-line: 3.1.0 - wrap-ansi: 6.2.0 - ws: 7.5.10 - yoga-layout-prebuilt: 1.10.0 - optionalDependencies: - '@types/react': 19.2.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - inline-style-parser@0.2.4: {} inquirer@12.10.0(@types/node@24.9.1): @@ -25120,13 +25094,6 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 - react-reconciler@0.26.2(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react: 18.3.1 - scheduler: 0.20.2 - react-refresh@0.17.0: {} react-refresh@0.18.0: {} @@ -26903,6 +26870,14 @@ snapshots: - rollup - supports-color + vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@6.4.1(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@rollup/plugin-inject': 5.0.5(rollup@4.49.0) + node-stdlib-browser: 1.3.1 + vite: 6.4.1(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@7.1.12(@types/node@24.9.1)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.49.0) From 1b0439030a2894e99b5941e471f7af1f6f619f29 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 14:44:48 -0500 Subject: [PATCH 22/30] Implement indeterminate handling --- packages/compiler/src/core/checker.ts | 45 ++++++++------ .../compiler/test/checker/functions.test.ts | 35 +++++++++++ .../test/checker/values/unknown-value.test.ts | 58 ++++++++++++++++--- 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 562939620d8..f0a2c62830f 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4452,18 +4452,20 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ? target.implementation(ctx, ...resolvedArgs) : getDefaultFunctionResult(target.returnType); - const returnIsTypeOrValue = + const returnIsEntity = typeof functionReturn === "object" && functionReturn !== null && "entityKind" in functionReturn && - (functionReturn.entityKind === "Type" || functionReturn.entityKind === "Value"); + (functionReturn.entityKind === "Type" || + functionReturn.entityKind === "Value" || + functionReturn.entityKind === "Indeterminate"); // special case for when the return value is `undefined` and the return type is `void` or `valueof void`. if (functionReturn === undefined && isVoidReturn(target.returnType)) { return voidType; } - const unmarshaled = returnIsTypeOrValue + const unmarshaled = returnIsEntity ? (functionReturn as Type | Value) : unmarshalJsToValue(program, functionReturn, function onInvalid(value) { let valueSummary = String(value); @@ -4480,7 +4482,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ); }); - let result: Type | Value | null = unmarshaled; + let result: Type | Value | IndeterminateEntity | null = unmarshaled; if (satisfied) result = checkFunctionReturn(target, unmarshaled, node); return result; @@ -4583,6 +4585,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker ...restArgs.map((v, idx) => v !== null && isValue(v) ? marshalTypeForJs(v, undefined, function onUnknown() { + satisfied = false; reportCheckerDiagnostic( createDiagnostic({ code: "unknown-value", @@ -4653,7 +4656,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker function checkFunctionReturn( target: FunctionType, - result: Type | Value, + result: Type | Value | IndeterminateEntity, diagnosticTarget: Node, ): Type | Value | null { const [checked, diagnostics] = checkEntityAssignableToConstraint( @@ -4703,6 +4706,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return collector.wrap(assignable ? normed : null); } else { // Constraint is a type + if (entity.entityKind === "Indeterminate") entity = entity.type; if (entity.entityKind !== "Type") { collector.add( @@ -5468,17 +5472,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker !(isType(arg) && isErrorType(arg)) && checkArgumentAssignable(arg, perParamType, argNode) ) { + const [valid, jsValue] = resolveArgumentJsValue( + arg, + extractValueOfConstraints({ + kind: "argument", + constraint: perParamType, + }), + argNode, + ); + + if (!valid) return undefined; + return { value: arg, node: argNode, - jsValue: resolveArgumentJsValue( - arg, - extractValueOfConstraints({ - kind: "argument", - constraint: perParamType, - }), - argNode, - ), + jsValue, }; } else { return undefined; @@ -5555,10 +5563,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker value: Type | Value, valueConstraint: CheckValueConstraint | undefined, diagnosticTarget: Node, - ) { + ): [valid: boolean, jsValue: any] { if (valueConstraint !== undefined) { if (isValue(value)) { - return marshalTypeForJs(value, valueConstraint.type, function onUnknown() { + let valid = true; + const unmarshaled = marshalTypeForJs(value, valueConstraint.type, function onUnknown() { + valid = false; reportCheckerDiagnostic( createDiagnostic({ code: "unknown-value", @@ -5567,11 +5577,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker }), ); }); + return [valid, unmarshaled]; } else { - return value; + return [true, value]; } } - return value; + return [true, value]; } function checkArgumentAssignable( diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index cca7cf98791..0bc182d19bd 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, it } from "vitest"; import { Diagnostic, FunctionContext, + IndeterminateEntity, ModelProperty, Namespace, Type, @@ -192,6 +193,19 @@ describe("compiler: checker: functions", () => { expectCalledWith("test"); }); + it("errors if non-void function returns undefined", async () => { + const diagnostics = await runner.diagnose( + `extern fn voidFn(a: valueof string): unknown; alias V = voidFn("test");`, + ); + + expectCalledWith("test"); + expectDiagnostics(diagnostics, { + code: "function-return", + message: + "Implementation of function 'voidFn' returned value 'null', which is not assignable to the declared return type 'unknown'.", + }); + }); + it("errors if not enough args", async () => { const diagnostics = await runner.diagnose( `extern fn testFn(a: valueof string, b: valueof string): valueof string; const X = testFn("one");`, @@ -459,6 +473,9 @@ describe("compiler: checker: functions", () => { mixed: null, }; }, + returnIndeterminate(ctx: FunctionContext): IndeterminateEntity { + return { entityKind: "Indeterminate", type: $(ctx.program).literal.create(42) }; + }, }); runner = createTestWrapper(testHost, { autoImports: ["./test.js"], @@ -543,6 +560,24 @@ describe("compiler: checker: functions", () => { expectDiagnosticEmpty(_diagnostics); strictEqual(receivedValues.length, 0); // No input values, only return }); + + it("handles indeterminate entities coerced to values", async () => { + const diagnostics = await runner.diagnose(` + extern fn returnIndeterminate(): valueof int32; + extern fn expectNumber(n: valueof int32): valueof int32; + const X = expectNumber(returnIndeterminate()); + `); + expectDiagnosticEmpty(diagnostics); + }); + + it("handles indeterminate entities coerced to types", async () => { + const diagnosed = await runner.diagnose(` + extern fn returnIndeterminate(): int32; + + alias X = returnIndeterminate(); + `); + expectDiagnosticEmpty(diagnosed); + }); }); describe("union type constraints", () => { diff --git a/packages/compiler/test/checker/values/unknown-value.test.ts b/packages/compiler/test/checker/values/unknown-value.test.ts index 784df454592..657a719a9d7 100644 --- a/packages/compiler/test/checker/values/unknown-value.test.ts +++ b/packages/compiler/test/checker/values/unknown-value.test.ts @@ -1,6 +1,6 @@ import { strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { DecoratorContext, Program, Type, Value } from "../../../src/index.js"; +import { DecoratorContext, Program, Type } from "../../../src/index.js"; import { expectDiagnostics } from "../../../src/testing/expect.js"; import { createTestHost, createTestRunner } from "../../../src/testing/test-host.js"; import { BasicTestRunner, TestHost } from "../../../src/testing/types.js"; @@ -8,18 +8,19 @@ import { BasicTestRunner, TestHost } from "../../../src/testing/types.js"; describe("invalid uses of unknown value", () => { let host: TestHost; let runner: BasicTestRunner; - let observedValue: Value | null = null; + let observedValue: { value: unknown } | null = null; beforeEach(async () => { + observedValue = null; host = await createTestHost(); host.addJsFile("lib.js", { - $collect: (_context: DecoratorContext, _target: Type, value: Value) => { - observedValue = value; + $collect: (_context: DecoratorContext, _target: Type, value: unknown) => { + observedValue = { value }; }, $functions: { Items: { - echo: (_: Program, value: Value) => { - observedValue = value; + echo: (_: Program, value: unknown) => { + observedValue = { value }; return value; }, @@ -50,12 +51,55 @@ describe("invalid uses of unknown value", () => { ]); }); + it("cannot be passed to a decorator in rest position", async () => { + const diags = await runner.diagnose(` + import "./lib.js"; + + extern dec collect(target: Reflection.Model, ...values: valueof unknown[]); + + @collect(unknown) + model Test {} + `); + + strictEqual(observedValue, null); + + expectDiagnostics(diags, [ + { + code: "unknown-value", + message: "The 'unknown' value cannot be used as an argument to a function or decorator.", + severity: "error", + }, + ]); + }); + it("cannot be passed to a function", async () => { const diags = await runner.diagnose(` import "./lib.js"; namespace Items { - extern fn echo(value: valueof unknown): valueof unknown; + extern fn echo(value: valueof unknown): unknown; + } + + alias X = Items.echo(unknown); + `); + + strictEqual(observedValue, null); + + expectDiagnostics(diags, [ + { + code: "unknown-value", + message: "The 'unknown' value cannot be used as an argument to a function or decorator.", + severity: "error", + }, + ]); + }); + + it("cannot be passed to a function in rest position", async () => { + const diags = await runner.diagnose(` + import "./lib.js"; + + namespace Items { + extern fn echo(...values: valueof unknown[]): valueof unknown; } const x = Items.echo(unknown); From 8798168ceb485ea6c499ac219bee94d3ab15c126 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 23:46:40 -0500 Subject: [PATCH 23/30] Undo rest after optional, omit function bindings outside $functions --- packages/compiler/src/core/binder.ts | 9 +--- packages/compiler/src/core/parser.ts | 2 +- packages/compiler/test/binder.test.ts | 41 +++++++++++++++---- .../compiler/test/checker/functions.test.ts | 29 +++---------- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/compiler/src/core/binder.ts b/packages/compiler/src/core/binder.ts index df08c407ba3..877f91ef866 100644 --- a/packages/compiler/src/core/binder.ts +++ b/packages/compiler/src/core/binder.ts @@ -134,7 +134,6 @@ export function createBinder(program: Program): Binder { for (const [key, member] of Object.entries(sourceFile.esmExports)) { let name: string; - let kind: "decorator" | "function"; if (key === "$flags") { const context = getLocationContext(program, sourceFile); if (context.type === "library" || context.type === "project") { @@ -171,7 +170,6 @@ export function createBinder(program: Program): Binder { // isn't particularly useful it turns out. if (isFunctionName(key)) { name = getFunctionName(key); - kind = "decorator"; if (name === "onValidate") { const context = getLocationContext(program, sourceFile); const metadata = @@ -184,12 +182,9 @@ export function createBinder(program: Program): Binder { // nothing to do here this is loaded as emitter. continue; } - } else { - name = key; - kind = "function"; + const nsParts = resolveJSMemberNamespaceParts(rootNs, member); + bindFunctionImplementation(nsParts, "decorator", name, member as any, sourceFile); } - const nsParts = resolveJSMemberNamespaceParts(rootNs, member); - bindFunctionImplementation(nsParts, kind, name, member as any, sourceFile); } } } diff --git a/packages/compiler/src/core/parser.ts b/packages/compiler/src/core/parser.ts index 49d61b917c1..7cdd5909a39 100644 --- a/packages/compiler/src/core/parser.ts +++ b/packages/compiler/src/core/parser.ts @@ -2083,7 +2083,7 @@ function createParser(code: string | SourceFile, options: ParseOptions = {}): Pa let foundOptional = false; for (const [index, item] of parameters.items.entries()) { - if (!(item.optional || item.rest) && foundOptional) { + if (!item.optional && foundOptional) { error({ code: "required-parameter-first", target: item }); continue; } diff --git a/packages/compiler/test/binder.test.ts b/packages/compiler/test/binder.test.ts index 488799be5da..3bfa055581d 100644 --- a/packages/compiler/test/binder.test.ts +++ b/packages/compiler/test/binder.test.ts @@ -421,20 +421,12 @@ describe("compiler: binder", () => { flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, - fn2: { - flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, - declarations: [SyntaxKind.JsSourceFile], - }, }, }, "@myDec": { flags: SymbolFlags.Decorator | SymbolFlags.Declaration | SymbolFlags.Implementation, declarations: [SyntaxKind.JsSourceFile], }, - fn: { - flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, - declarations: [SyntaxKind.JsSourceFile], - }, }, }, }); @@ -473,6 +465,39 @@ describe("compiler: binder", () => { }); }); + it("binds $functions in JS file", () => { + const exports = { + $functions: { + "Foo.Bar": { myFn2: () => {} }, + "": { myFn: () => {} }, + }, + }; + + const sourceFile = bindJs(exports); + assertBindings("jsFile", sourceFile.symbol.exports!, { + Foo: { + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, + declarations: [SyntaxKind.JsNamespaceDeclaration], + exports: { + Bar: { + flags: SymbolFlags.Namespace | SymbolFlags.Declaration, + declarations: [SyntaxKind.JsNamespaceDeclaration], + exports: { + myFn2: { + flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, + declarations: [SyntaxKind.JsSourceFile], + }, + }, + }, + }, + }, + myFn: { + flags: SymbolFlags.Function | SymbolFlags.Declaration | SymbolFlags.Implementation, + declarations: [SyntaxKind.JsSourceFile], + }, + }); + }); + function bindTypeSpec(code: string) { const sourceFile = parse(code); expectDiagnosticEmpty(sourceFile.parseDiagnostics); diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index 0bc182d19bd..c2eac41d058 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -165,15 +165,17 @@ describe("compiler: checker: functions", () => { }); it("calls function with arguments", async () => { - await runner.compile( - `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one", "two", "three");`, - ); + await runner.compile(` + extern fn testFn(a: valueof string, b: valueof string, ...rest: valueof string[]): valueof string; + + const X = testFn("one", "two", "three"); + `); expectCalledWith("one", "two", "three"); // program + args, optional b provided }); it("allows omitting optional param", async () => { await runner.compile( - `extern fn testFn(a: valueof string, b?: valueof string, ...rest: valueof string[]): valueof string; const X = testFn("one");`, + `extern fn testFn(a: valueof string, b?: valueof string): valueof string; const X = testFn("one");`, ); expectCalledWith("one", undefined); }); @@ -789,25 +791,6 @@ describe("compiler: checker: functions", () => { // except when rest parameters are involved }); - it("allows rest parameters after optional parameters", async () => { - testHost.addJsFile("rest-after-optional.js", { - restAfterOptional(_ctx: FunctionContext, opt: any, ...rest: any[]) { - return rest.length; - }, - }); - const restRunner = createTestWrapper(testHost, { - autoImports: ["./rest-after-optional.js"], - autoUsings: ["TypeSpec.Reflection"], - }); - - const diagnostics = await restRunner.diagnose(` - extern fn restAfterOptional(opt?: valueof string, ...rest: valueof string[]): valueof int32; - const X = restAfterOptional("optional", "rest1", "rest2"); - const Y = restAfterOptional(); - `); - expectDiagnosticEmpty(diagnostics); - }); - it("handles deeply nested type constraints", async () => { testHost.addJsFile("nested.js", { processNestedModel(_ctx: FunctionContext, model: Type) { From 71392a1471eb59a5c72285607225384e44b455c0 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 20 Nov 2025 23:54:10 -0500 Subject: [PATCH 24/30] Updated tests that used direct function bindings --- .../compiler/test/checker/functions.test.ts | 298 ++++++++++-------- 1 file changed, 159 insertions(+), 139 deletions(-) diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index c2eac41d058..1e52c24c928 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -8,7 +8,6 @@ import { Namespace, Type, } from "../../src/core/types.js"; -import { setTypeSpecNamespace } from "../../src/index.js"; import { BasicTestRunner, TestHost, @@ -41,7 +40,7 @@ describe("compiler: checker: functions", () => { let testImpl: any; beforeEach(() => { testImpl = (ctx: FunctionContext) => undefined; - testJs = { testFn: testImpl }; + testJs = { $functions: { "": { testFn: testImpl } } }; testHost.addJsFile("test.js", testJs); runner = createTestWrapper(testHost, { autoImports: ["./test.js"], @@ -57,19 +56,6 @@ describe("compiler: checker: functions", () => { expectFunction(runner.program.getGlobalNamespaceType(), "testFn", testImpl); }); - it("in a namespace via direct export", async () => { - setTypeSpecNamespace("Foo.Bar", testImpl); - await runner.compile(` - namespace Foo.Bar { extern fn testFn(); } - `); - const ns = runner.program - .getGlobalNamespaceType() - .namespaces.get("Foo") - ?.namespaces.get("Bar"); - ok(ns); - expectFunction(ns, "testFn", testImpl); - }); - it("defined at root via $functions map", async () => { const impl = (_ctx: FunctionContext) => undefined; testJs.$functions = { "": { otherFn: impl } }; @@ -127,19 +113,23 @@ describe("compiler: checker: functions", () => { beforeEach(() => { calledArgs = undefined; testHost.addJsFile("test.js", { - testFn(ctx: FunctionContext, a: any, b: any, ...rest: any[]) { - calledArgs = [ctx, a, b, ...rest]; - return a; // Return first arg - }, - sum(_ctx: FunctionContext, ...addends: number[]) { - return addends.reduce((a, b) => a + b, 0); - }, - valFirst(_ctx: FunctionContext, v: any) { - return v; - }, - voidFn(ctx: FunctionContext, arg: any) { - calledArgs = [ctx, arg]; - // No return value + $functions: { + "": { + testFn(ctx: FunctionContext, a: any, b: any, ...rest: any[]) { + calledArgs = [ctx, a, b, ...rest]; + return a; // Return first arg + }, + sum(_ctx: FunctionContext, ...addends: number[]) { + return addends.reduce((a, b) => a + b, 0); + }, + valFirst(_ctx: FunctionContext, v: any) { + return v; + }, + voidFn(ctx: FunctionContext, arg: any) { + calledArgs = [ctx, arg]; + // No return value + }, + }, }, }); runner = createTestWrapper(testHost, { @@ -276,8 +266,12 @@ describe("compiler: checker: functions", () => { describe("referencing result type", () => { it("can use function result in alias", async () => { testHost.addJsFile("test.js", { - makeArray(ctx: FunctionContext, t: Type) { - return $(ctx.program).array.create(t); + $functions: { + "": { + makeArray(ctx: FunctionContext, t: Type) { + return $(ctx.program).array.create(t); + }, + }, }, }); const runner = createTestWrapper(testHost, { @@ -312,33 +306,37 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedTypes = []; testHost.addJsFile("test.js", { - expectModel(_ctx: FunctionContext, model: Type) { - receivedTypes.push(model); - return model; - }, - expectEnum(_ctx: FunctionContext, enumType: Type) { - receivedTypes.push(enumType); - return enumType; - }, - expectScalar(_ctx: FunctionContext, scalar: Type) { - receivedTypes.push(scalar); - return scalar; - }, - expectUnion(_ctx: FunctionContext, union: Type) { - receivedTypes.push(union); - return union; - }, - expectInterface(_ctx: FunctionContext, iface: Type) { - receivedTypes.push(iface); - return iface; - }, - expectNamespace(_ctx: FunctionContext, ns: Type) { - receivedTypes.push(ns); - return ns; - }, - expectOperation(_ctx: FunctionContext, op: Type) { - receivedTypes.push(op); - return op; + $functions: { + "": { + expectModel(_ctx: FunctionContext, model: Type) { + receivedTypes.push(model); + return model; + }, + expectEnum(_ctx: FunctionContext, enumType: Type) { + receivedTypes.push(enumType); + return enumType; + }, + expectScalar(_ctx: FunctionContext, scalar: Type) { + receivedTypes.push(scalar); + return scalar; + }, + expectUnion(_ctx: FunctionContext, union: Type) { + receivedTypes.push(union); + return union; + }, + expectInterface(_ctx: FunctionContext, iface: Type) { + receivedTypes.push(iface); + return iface; + }, + expectNamespace(_ctx: FunctionContext, ns: Type) { + receivedTypes.push(ns); + return ns; + }, + expectOperation(_ctx: FunctionContext, op: Type) { + receivedTypes.push(op); + return op; + }, + }, }, }); runner = createTestWrapper(testHost, { @@ -445,38 +443,42 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedValues = []; testHost.addJsFile("test.js", { - expectString(ctx: FunctionContext, str: string) { - receivedValues.push(str); - return str; - }, - expectNumber(ctx: FunctionContext, num: number) { - receivedValues.push(num); - return num; - }, - expectBoolean(ctx: FunctionContext, bool: boolean) { - receivedValues.push(bool); - return bool; - }, - expectArray(ctx: FunctionContext, arr: any[]) { - receivedValues.push(arr); - return arr; - }, - expectObject(ctx: FunctionContext, obj: Record) { - receivedValues.push(obj); - return obj; - }, - returnInvalidJsValue(ctx: FunctionContext) { - return Symbol("invalid"); // Invalid JS value that can't be unmarshaled - }, - returnComplexObject(ctx: FunctionContext) { - return { - nested: { value: 42 }, - array: [1, "test", true], - mixed: null, - }; - }, - returnIndeterminate(ctx: FunctionContext): IndeterminateEntity { - return { entityKind: "Indeterminate", type: $(ctx.program).literal.create(42) }; + $functions: { + "": { + expectString(ctx: FunctionContext, str: string) { + receivedValues.push(str); + return str; + }, + expectNumber(ctx: FunctionContext, num: number) { + receivedValues.push(num); + return num; + }, + expectBoolean(ctx: FunctionContext, bool: boolean) { + receivedValues.push(bool); + return bool; + }, + expectArray(ctx: FunctionContext, arr: any[]) { + receivedValues.push(arr); + return arr; + }, + expectObject(ctx: FunctionContext, obj: Record) { + receivedValues.push(obj); + return obj; + }, + returnInvalidJsValue(ctx: FunctionContext) { + return Symbol("invalid"); // Invalid JS value that can't be unmarshaled + }, + returnComplexObject(ctx: FunctionContext) { + return { + nested: { value: 42 }, + array: [1, "test", true], + mixed: null, + }; + }, + returnIndeterminate(ctx: FunctionContext): IndeterminateEntity { + return { entityKind: "Indeterminate", type: $(ctx.program).literal.create(42) }; + }, + }, }, }); runner = createTestWrapper(testHost, { @@ -589,24 +591,28 @@ describe("compiler: checker: functions", () => { beforeEach(() => { receivedArgs = []; testHost.addJsFile("test.js", { - acceptTypeOrValue(_ctx: FunctionContext, arg: any) { - receivedArgs.push(arg); - return arg; - }, - acceptMultipleTypes(_ctx: FunctionContext, arg: any) { - receivedArgs.push(arg); - return arg; - }, - acceptMultipleValues(_ctx: FunctionContext, arg: any) { - receivedArgs.push(arg); - return arg; - }, - returnTypeOrValue(ctx: FunctionContext, returnType: boolean) { - if (returnType) { - return ctx.program.checker.getStdType("string"); - } else { - return "hello"; - } + $functions: { + "": { + acceptTypeOrValue(_ctx: FunctionContext, arg: any) { + receivedArgs.push(arg); + return arg; + }, + acceptMultipleTypes(_ctx: FunctionContext, arg: any) { + receivedArgs.push(arg); + return arg; + }, + acceptMultipleValues(_ctx: FunctionContext, arg: any) { + receivedArgs.push(arg); + return arg; + }, + returnTypeOrValue(ctx: FunctionContext, returnType: boolean) { + if (returnType) { + return ctx.program.checker.getStdType("string"); + } else { + return "hello"; + } + }, + }, }, }); runner = createTestWrapper(testHost, { @@ -698,23 +704,27 @@ describe("compiler: checker: functions", () => { beforeEach(() => { testHost.addJsFile("test.js", { - returnWrongEntityKind(_ctx: FunctionContext) { - return "string value"; // Returns value when type expected - }, - returnWrongValueType(_ctx: FunctionContext) { - return 42; // Returns number when string expected - }, - throwError(_ctx: FunctionContext) { - throw new Error("JS error"); - }, - returnUndefined(_ctx: FunctionContext) { - return undefined; - }, - returnNull(_ctx: FunctionContext) { - return null; - }, - expectNonOptionalAfterOptional(_ctx: FunctionContext, _opt: any, req: any) { - return req; + $functions: { + "": { + returnWrongEntityKind(_ctx: FunctionContext) { + return "string value"; // Returns value when type expected + }, + returnWrongValueType(_ctx: FunctionContext) { + return 42; // Returns number when string expected + }, + throwError(_ctx: FunctionContext) { + throw new Error("JS error"); + }, + returnUndefined(_ctx: FunctionContext) { + return undefined; + }, + returnNull(_ctx: FunctionContext) { + return null; + }, + expectNonOptionalAfterOptional(_ctx: FunctionContext, _opt: any, req: any) { + return req; + }, + }, }, }); runner = createTestWrapper(testHost, { @@ -793,8 +803,12 @@ describe("compiler: checker: functions", () => { it("handles deeply nested type constraints", async () => { testHost.addJsFile("nested.js", { - processNestedModel(_ctx: FunctionContext, model: Type) { - return model; + $functions: { + "": { + processNestedModel(_ctx: FunctionContext, model: Type) { + return model; + }, + }, }, }); const nestedRunner = createTestWrapper(testHost, { @@ -820,11 +834,15 @@ describe("compiler: checker: functions", () => { it("validates function return type matches declared constraint", async () => { testHost.addJsFile("return-validation.js", { - returnString(_ctx: FunctionContext) { - return "hello"; - }, - returnNumber(_ctx: FunctionContext) { - return 42; + $functions: { + "": { + returnString(_ctx: FunctionContext) { + return "hello"; + }, + returnNumber(_ctx: FunctionContext) { + return 42; + }, + }, }, }); const returnRunner = createTestWrapper(testHost, { @@ -852,9 +870,7 @@ describe("compiler: checker: functions", () => { let runner: BasicTestRunner; beforeEach(() => { - testHost.addJsFile("missing-impl.js", { - // Intentionally empty - to test default implementations - }); + testHost.addJsFile("missing-impl.js", {}); runner = createTestWrapper(testHost, { autoImports: ["./missing-impl.js"], autoUsings: ["TypeSpec.Reflection"], @@ -897,11 +913,15 @@ describe("compiler: checker: functions", () => { beforeEach(() => { testHost.addJsFile("templates.js", { - processGeneric(ctx: FunctionContext, type: Type) { - return $(ctx.program).array.create(type); - }, - processConstrainedGeneric(_ctx: FunctionContext, type: Type) { - return type; + $functions: { + "": { + processGeneric(ctx: FunctionContext, type: Type) { + return $(ctx.program).array.create(type); + }, + processConstrainedGeneric(_ctx: FunctionContext, type: Type) { + return type; + }, + }, }, }); runner = createTestWrapper(testHost, { From b0c7072f265642114cb22af05915042c505fb8d3 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 11:05:48 -0500 Subject: [PATCH 25/30] Discard unknown values in default position in openapi3. --- packages/openapi3/src/openapi.ts | 2 +- packages/openapi3/src/schema-emitter.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 1356578c378..12568f1a5b5 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1584,7 +1584,7 @@ function createOAPIEmitter( options, ); - if (param.defaultValue) { + if (param.defaultValue && param.defaultValue.valueKind !== "UnknownValue") { schema.default = getDefaultValue(program, param.defaultValue, param); } // Description is already provided in the parameter itself. diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 22117946257..3225e6f4023 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -433,7 +433,7 @@ export class OpenAPI3SchemaEmitterBase< {} as Schema, schema, ) as any; - if (prop.defaultValue) { + if (prop.defaultValue && prop.defaultValue.valueKind !== "UnknownValue") { additionalProps.default = getDefaultValue(program, prop.defaultValue, prop); } From 1f405a30a36494f788513ad9ac7eda47594591ee Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 12:41:05 -0500 Subject: [PATCH 26/30] Throughly reviewed all tests --- packages/compiler/src/core/checker.ts | 84 ++- .../compiler/test/checker/functions.test.ts | 493 ++++++++++++------ 2 files changed, 394 insertions(+), 183 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index f0a2c62830f..1f1af0d4fb4 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -4690,45 +4690,75 @@ export function createChecker(program: Program, resolver: NameResolver): Checker diagnosticTarget: Node, ): DiagnosticResult { const constraintIsValue = !!constraint.valueType; + const constraintIsType = !!constraint.type; const collector = createDiagnosticCollector(); - if (constraintIsValue) { - const normed = collector.pipe(normalizeValue(entity, constraint, diagnosticTarget)); + switch (true) { + case constraintIsValue && constraintIsType: { + const tried = tryAssignValue(); - // Error should have been reported in normalizeValue - if (!normed) return collector.wrap(null); + if (tried[0] !== null || entity.entityKind === "Value") { + // Succeeded as value or is a value + return tried; + } - const assignable = collector.pipe( - relation.isValueOfType(normed, constraint.valueType, diagnosticTarget), - ); + // Now we are guaranteed a type. + const typeEntity = entity.entityKind === "Indeterminate" ? entity.type : entity; - return collector.wrap(assignable ? normed : null); - } else { - // Constraint is a type - if (entity.entityKind === "Indeterminate") entity = entity.type; + const assignable = collector.pipe( + relation.isTypeAssignableTo(typeEntity, constraint.type, diagnosticTarget), + ); - if (entity.entityKind !== "Type") { - collector.add( - createDiagnostic({ - code: "value-in-type", - format: { name: getTypeName(entity.type) }, - target: diagnosticTarget, - }), + return collector.wrap(assignable ? typeEntity : null); + } + case constraintIsValue: { + const normed = collector.pipe(normalizeValue(entity, constraint, diagnosticTarget)); + + // Error should have been reported in normalizeValue + if (!normed) return collector.wrap(null); + + const assignable = collector.pipe( + relation.isValueOfType(normed, constraint.valueType, diagnosticTarget), ); - return collector.wrap(null); + + return collector.wrap(assignable ? normed : null); } + case constraintIsType: { + if (entity.entityKind === "Indeterminate") entity = entity.type; - compilerAssert( - constraint.type, - "Expected type constraint to be defined when known not to be a value constraint.", - ); + if (entity.entityKind !== "Type") { + collector.add( + createDiagnostic({ + code: "value-in-type", + format: { name: getTypeName(entity.type) }, + target: diagnosticTarget, + }), + ); + return collector.wrap(null); + } - const assignable = collector.pipe( - relation.isTypeAssignableTo(entity, constraint.type, diagnosticTarget), - ); + const assignable = collector.pipe( + relation.isTypeAssignableTo(entity, constraint.type, diagnosticTarget), + ); - return collector.wrap(assignable ? entity : null); + return collector.wrap(assignable ? entity : null); + } + default: { + compilerAssert(false, "Expected at least one of type or value constraint to be defined."); + } + } + + function tryAssignValue(): DiagnosticResult { + const collector = createDiagnosticCollector(); + + const normed = collector.pipe(normalizeValue(entity, constraint, diagnosticTarget)); + + const assignable = normed + ? collector.pipe(relation.isValueOfType(normed, constraint.valueType!, diagnosticTarget)) + : false; + + return collector.wrap(assignable ? normed : null); } } diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index 1e52c24c928..8d10b4d8a2e 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -1,9 +1,10 @@ -import { ok, strictEqual } from "assert"; +import { deepStrictEqual, fail, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { Diagnostic, FunctionContext, IndeterminateEntity, + Model, ModelProperty, Namespace, Type, @@ -18,8 +19,6 @@ import { } from "../../src/testing/index.js"; import { $ } from "../../src/typekit/index.js"; -// TODO/witemple: thoroughly review this file - /** Helper to assert a function declaration was bound to the js implementation */ function expectFunction(ns: Namespace, name: string, impl: any) { const fn = ns.functionDeclarations.get(name); @@ -36,12 +35,21 @@ describe("compiler: checker: functions", () => { describe("declaration", () => { let runner: BasicTestRunner; - let testJs: Record; let testImpl: any; + let nsFnImpl: any; beforeEach(() => { - testImpl = (ctx: FunctionContext) => undefined; - testJs = { $functions: { "": { testFn: testImpl } } }; - testHost.addJsFile("test.js", testJs); + testImpl = (_ctx: FunctionContext) => undefined; + nsFnImpl = (_ctx: FunctionContext) => undefined; + testHost.addJsFile("test.js", { + $functions: { + "": { + testFn: testImpl, + }, + "Foo.Bar": { + nsFn: nsFnImpl, + }, + }, + }); runner = createTestWrapper(testHost, { autoImports: ["./test.js"], autoUsings: ["TypeSpec.Reflection"], @@ -56,23 +64,14 @@ describe("compiler: checker: functions", () => { expectFunction(runner.program.getGlobalNamespaceType(), "testFn", testImpl); }); - it("defined at root via $functions map", async () => { - const impl = (_ctx: FunctionContext) => undefined; - testJs.$functions = { "": { otherFn: impl } }; - await runner.compile(`extern fn otherFn();`); - expectFunction(runner.program.getGlobalNamespaceType(), "otherFn", impl); - }); - it("in namespace via $functions map", async () => { - const impl = (_ctx: FunctionContext) => undefined; - testJs.$functions = { "Foo.Bar": { nsFn: impl } }; await runner.compile(`namespace Foo.Bar { extern fn nsFn(); }`); const ns = runner.program .getGlobalNamespaceType() .namespaces.get("Foo") ?.namespaces.get("Bar"); ok(ns); - expectFunction(ns, "nsFn", impl); + expectFunction(ns, "nsFn", nsFnImpl); }); }); @@ -148,6 +147,7 @@ describe("compiler: checker: functions", () => { it("errors if function not declared", async () => { const diagnostics = await runner.diagnose(`const X = missing();`); + expectDiagnostics(diagnostics, { code: "invalid-ref", message: "Unknown identifier missing", @@ -160,6 +160,7 @@ describe("compiler: checker: functions", () => { const X = testFn("one", "two", "three"); `); + expectCalledWith("one", "two", "three"); // program + args, optional b provided }); @@ -167,71 +168,119 @@ describe("compiler: checker: functions", () => { await runner.compile( `extern fn testFn(a: valueof string, b?: valueof string): valueof string; const X = testFn("one");`, ); + expectCalledWith("one", undefined); }); it("allows zero args for rest-only", async () => { - const diagnostics = await runner.diagnose( - `extern fn sum(...addends: valueof int32[]): valueof int32; const S = sum();`, - ); + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn sum(...addends: valueof int32[]): valueof int32; + const S = sum(); + + model Observer { + @test + p: int32 = S; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnostics(diagnostics, []); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue.valueKind, "NumericValue"); + strictEqual(p.defaultValue.value.asNumber(), 0); }); it("accepts function with explicit void return type", async () => { - const diagnostics = await runner.diagnose( - `extern fn voidFn(a: valueof string): void; alias V = voidFn("test");`, - ); + const diagnostics = await runner.diagnose(` + extern fn voidFn(a: valueof string): void; + alias V = voidFn("test"); + `); + expectDiagnostics(diagnostics, []); expectCalledWith("test"); }); it("errors if non-void function returns undefined", async () => { - const diagnostics = await runner.diagnose( - `extern fn voidFn(a: valueof string): unknown; alias V = voidFn("test");`, - ); + const diagnostics = await runner.diagnose(` + extern fn voidFn(a: valueof string): unknown; + alias V = voidFn("test"); + `); - expectCalledWith("test"); expectDiagnostics(diagnostics, { code: "function-return", message: "Implementation of function 'voidFn' returned value 'null', which is not assignable to the declared return type 'unknown'.", }); + expectCalledWith("test"); }); it("errors if not enough args", async () => { - const diagnostics = await runner.diagnose( - `extern fn testFn(a: valueof string, b: valueof string): valueof string; const X = testFn("one");`, - ); + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn testFn(a: valueof string, b: valueof string): valueof string; + const X = testFn("one"); + + model Observer { + @test p: string = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnostics(diagnostics, { code: "invalid-argument-count", message: "Expected at least 2 arguments, but got 1.", }); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue.valueKind, "UnknownValue"); }); it("errors if too many args", async () => { - const diagnostics = await runner.diagnose( - `extern fn testFn(a: valueof string): valueof string; const X = testFn("one", "two");`, - ); + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn testFn(a: valueof string): valueof string; + const X = testFn("one", "two"); + + model Observer { + @test p: string = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnostics(diagnostics, { code: "invalid-argument-count", message: "Expected 1 arguments, but got 2.", }); + + expectCalledWith("one", undefined); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue.valueKind, "StringValue"); + strictEqual(p.defaultValue.value, "one"); }); it("errors if too few with rest", async () => { - const diagnostics = await runner.diagnose( - `extern fn testFn(a: string, ...rest: string[]); alias X = testFn();`, - ); + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn testFn(a: string, ...rest: string[]); + + alias X = testFn(); + + model Observer { + @test p: X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnostics(diagnostics, { code: "invalid-argument-count", message: "Expected at least 1 arguments, but got 0.", }); + + strictEqual(p.type.kind, "Intrinsic"); + strictEqual(p.type.name, "unknown"); }); it("errors if argument type mismatch (value)", async () => { - const diagnostics = await runner.diagnose( - `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(123);`, - ); + const diagnostics = await runner.diagnose(` + extern fn valFirst(a: valueof string): valueof string; + const X = valFirst(123); + `); + expectDiagnostics(diagnostics, { code: "unassignable", message: "Type '123' is not assignable to type 'string'", @@ -239,9 +288,11 @@ describe("compiler: checker: functions", () => { }); it("errors if passing type where value expected", async () => { - const diagnostics = await runner.diagnose( - `extern fn valFirst(a: valueof string): valueof string; const X = valFirst(string);`, - ); + const diagnostics = await runner.diagnose(` + extern fn valFirst(a: valueof string): valueof string; + const X = valFirst(string); + `); + expectDiagnostics(diagnostics, { code: "expect-value", message: "string refers to a type, but is being used as a value here.", @@ -249,22 +300,38 @@ describe("compiler: checker: functions", () => { }); it("accepts string literal for type param", async () => { - const diagnostics = await runner.diagnose( - `extern fn testFn(a: string); alias X = testFn("abc");`, - ); + const diagnostics = await runner.diagnose(` + extern fn testFn(a: string); + alias X = testFn("abc"); + `); + expectDiagnosticEmpty(diagnostics); + + strictEqual(calledArgs?.[1].entityKind, "Type"); + strictEqual(calledArgs?.[1].kind, "String"); + strictEqual(calledArgs?.[1].value, "abc"); }); it("accepts arguments matching rest", async () => { - const diagnostics = await runner.diagnose( - `extern fn testFn(a: string, ...rest: string[]); alias X = testFn("a", "b", "c");`, - ); + const diagnostics = await runner.diagnose(` + extern fn testFn(a: string, ...rest: string[]); + alias X = testFn("a", "b", "c"); + `); + expectDiagnosticEmpty(diagnostics); + + const expectedLiterals = ["a", "b", "c"]; + + for (let i = 1; i < calledArgs!.length; i++) { + strictEqual(calledArgs?.[i].entityKind, "Type"); + strictEqual(calledArgs?.[i].kind, "String"); + strictEqual(calledArgs?.[i].value, expectedLiterals[i - 1]); + } }); }); - describe("referencing result type", () => { - it("can use function result in alias", async () => { + describe("typekit construction", () => { + it("can construct array with typekit in impl", async () => { testHost.addJsFile("test.js", { $functions: { "": { @@ -278,21 +345,22 @@ describe("compiler: checker: functions", () => { autoImports: ["./test.js"], autoUsings: ["TypeSpec.Reflection"], }); - const [{ prop }, diagnostics] = (await runner.compileAndDiagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn makeArray(T: unknown); alias X = makeArray(string); model M { - @test prop: X; + @test p: X; } - `)) as [{ prop: ModelProperty }, Diagnostic[]]; + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); - ok(prop.type); - ok($(runner.program).array.is(prop.type)); + ok(p.type); + ok($(runner.program).array.is(p.type)); - const arrayIndexerType = prop.type.indexer.value; + const arrayIndexerType = p.type.indexer.value; ok(arrayIndexerType); ok($(runner.program).scalar.isString(arrayIndexerType)); @@ -351,9 +419,11 @@ describe("compiler: checker: functions", () => { model TestModel { x: string; } alias X = expectModel(TestModel); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Model"); + strictEqual(receivedTypes[0].name, "TestModel"); }); it("accepts Reflection.Enum parameter", async () => { @@ -362,9 +432,11 @@ describe("compiler: checker: functions", () => { enum TestEnum { A, B } alias X = expectEnum(TestEnum); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Enum"); + strictEqual(receivedTypes[0].name, "TestEnum"); }); it("accepts Reflection.Scalar parameter", async () => { @@ -373,9 +445,11 @@ describe("compiler: checker: functions", () => { scalar TestScalar extends string; alias X = expectScalar(TestScalar); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Scalar"); + strictEqual(receivedTypes[0].name, "TestScalar"); }); it("accepts Reflection.Union parameter", async () => { @@ -383,9 +457,11 @@ describe("compiler: checker: functions", () => { extern fn expectUnion(u: Reflection.Union): Reflection.Union; alias X = expectUnion(string | int32); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Union"); + strictEqual(receivedTypes[0].name, undefined); }); it("accepts Reflection.Interface parameter", async () => { @@ -396,9 +472,11 @@ describe("compiler: checker: functions", () => { } alias X = expectInterface(TestInterface); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Interface"); + strictEqual(receivedTypes[0].name, "TestInterface"); }); it("accepts Reflection.Namespace parameter", async () => { @@ -407,9 +485,11 @@ describe("compiler: checker: functions", () => { namespace TestNs {} alias X = expectNamespace(TestNs); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Namespace"); + strictEqual(receivedTypes[0].name, "TestNs"); }); it("accepts Reflection.Operation parameter", async () => { @@ -418,9 +498,11 @@ describe("compiler: checker: functions", () => { op testOp(): string; alias X = expectOperation(testOp); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedTypes.length, 1); strictEqual(receivedTypes[0].kind, "Operation"); + strictEqual(receivedTypes[0].name, "testOp"); }); it("errors when wrong type kind is passed", async () => { @@ -429,6 +511,7 @@ describe("compiler: checker: functions", () => { enum TestEnum { A, B } alias X = expectModel(TestEnum); `); + expectDiagnostics(diagnostics, { code: "unassignable", message: "Type 'TestEnum' is not assignable to type 'Model'", @@ -466,13 +549,13 @@ describe("compiler: checker: functions", () => { return obj; }, returnInvalidJsValue(ctx: FunctionContext) { - return Symbol("invalid"); // Invalid JS value that can't be unmarshaled + return Symbol("invalid"); }, returnComplexObject(ctx: FunctionContext) { return { nested: { value: 42 }, array: [1, "test", true], - mixed: null, + null: null, }; }, returnIndeterminate(ctx: FunctionContext): IndeterminateEntity { @@ -492,6 +575,7 @@ describe("compiler: checker: functions", () => { extern fn expectString(s: valueof string): valueof string; const X = expectString("hello world"); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedValues.length, 1); strictEqual(receivedValues[0], "hello world"); @@ -502,6 +586,7 @@ describe("compiler: checker: functions", () => { extern fn expectNumber(n: valueof int32): valueof int32; const X = expectNumber(42); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedValues.length, 1); strictEqual(receivedValues[0], 42); @@ -512,6 +597,7 @@ describe("compiler: checker: functions", () => { extern fn expectBoolean(b: valueof boolean): valueof boolean; const X = expectBoolean(true); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedValues.length, 1); strictEqual(receivedValues[0], true); @@ -522,6 +608,7 @@ describe("compiler: checker: functions", () => { extern fn expectArray(arr: valueof string[]): valueof string[]; const X = expectArray(#["a", "b", "c"]); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedValues.length, 1); ok(Array.isArray(receivedValues[0])); @@ -540,6 +627,7 @@ describe("compiler: checker: functions", () => { extern fn expectObject(obj: valueof {name: string, age: int32}): valueof {name: string, age: int32}; const X = expectObject(#{name: "test", age: 25}); `); + expectDiagnosticEmpty(diagnostics); strictEqual(receivedValues.length, 1); strictEqual(typeof receivedValues[0], "object"); @@ -548,39 +636,108 @@ describe("compiler: checker: functions", () => { }); it("handles invalid JS return values gracefully", async () => { - const _diagnostics = await runner.diagnose(` + const diagnostics = await runner.diagnose(` extern fn returnInvalidJsValue(): valueof string; const X = returnInvalidJsValue(); `); - // Should not crash, but may produce diagnostics about invalid return value - // The implementation currently has a TODO/witemple for this case + + expectDiagnostics(diagnostics, { + code: "function-return", + message: "Function implementation returned invalid JS value 'Symbol(invalid)'.", + }); }); it("unmarshal complex JS objects to values", async () => { - const _diagnostics = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnComplexObject(): valueof unknown; const X = returnComplexObject(); - `); - expectDiagnosticEmpty(_diagnostics); - strictEqual(receivedValues.length, 0); // No input values, only return + + model Observer { + @test p: unknown = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedValues.length, 0); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue?.valueKind, "ObjectValue"); + + const obj = p.defaultValue!.properties; + strictEqual(obj.size, 3); + + const nested = obj.get("nested")?.value; + ok(nested); + strictEqual(nested.entityKind, "Value"); + strictEqual(nested.valueKind, "ObjectValue"); + + const nestedProps = nested.properties; + strictEqual(nestedProps.size, 1); + const nestedValue = nestedProps.get("value")?.value; + ok(nestedValue); + strictEqual(nestedValue.entityKind, "Value"); + strictEqual(nestedValue.valueKind, "NumericValue"); + strictEqual(nestedValue.value.asNumber(), 42); + + const array = obj.get("array")?.value; + ok(array); + strictEqual(array.entityKind, "Value"); + strictEqual(array.valueKind, "ArrayValue"); + + const arrayItems = array.values; + strictEqual(arrayItems.length, 3); + + strictEqual(arrayItems[0].entityKind, "Value"); + strictEqual(arrayItems[0].valueKind, "NumericValue"); + strictEqual(arrayItems[0].value.asNumber(), 1); + + strictEqual(arrayItems[1].entityKind, "Value"); + strictEqual(arrayItems[1].valueKind, "StringValue"); + strictEqual(arrayItems[1].value, "test"); + + strictEqual(arrayItems[2].entityKind, "Value"); + strictEqual(arrayItems[2].valueKind, "BooleanValue"); + strictEqual(arrayItems[2].value, true); + + const nullP = obj.get("null")?.value; + ok(nullP); + strictEqual(nullP.entityKind, "Value"); + strictEqual(nullP.valueKind, "NullValue"); }); it("handles indeterminate entities coerced to values", async () => { - const diagnostics = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnIndeterminate(): valueof int32; extern fn expectNumber(n: valueof int32): valueof int32; const X = expectNumber(returnIndeterminate()); - `); + + model Observer { + @test p: int32 = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue?.valueKind, "NumericValue"); + strictEqual(p.defaultValue?.value.asNumber(), 42); }); it("handles indeterminate entities coerced to types", async () => { - const diagnosed = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnIndeterminate(): int32; alias X = returnIndeterminate(); - `); - expectDiagnosticEmpty(diagnosed); + + model Observer { + @test p: X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + + expectDiagnosticEmpty(diagnostics); + + strictEqual(p.type.kind, "Number"); + strictEqual(p.type.value, 42); }); }); @@ -606,6 +763,7 @@ describe("compiler: checker: functions", () => { return arg; }, returnTypeOrValue(ctx: FunctionContext, returnType: boolean) { + receivedArgs.push(returnType); if (returnType) { return ctx.program.checker.getStdType("string"); } else { @@ -623,22 +781,28 @@ describe("compiler: checker: functions", () => { it("accepts type parameter", async () => { const diagnostics = await runner.diagnose(` - extern fn acceptTypeOrValue(arg: unknown): unknown; + extern fn acceptTypeOrValue(arg: unknown | valueof unknown): unknown; alias TypeResult = acceptTypeOrValue(string); `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 1); }); - it("accepts value parameter", async () => { + it("prefers value when applicable", async () => { const diagnostics = await runner.diagnose(` - extern fn acceptTypeOrValue(arg: valueof string): valueof string; + extern fn acceptTypeOrValue(arg: string | valueof string): valueof string; const ValueResult = acceptTypeOrValue("hello"); `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 1); + // Prefer value overload + strictEqual(receivedArgs[0], "hello"); }); it("accepts multiple specific types", async () => { @@ -651,8 +815,14 @@ describe("compiler: checker: functions", () => { alias ModelResult = acceptMultipleTypes(TestModel); alias EnumResult = acceptMultipleTypes(TestEnum); `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 2); + strictEqual(receivedArgs[0].kind, "Model"); + strictEqual(receivedArgs[0].name, "TestModel"); + strictEqual(receivedArgs[1].kind, "Enum"); + strictEqual(receivedArgs[1].name, "TestEnum"); }); it("accepts multiple value types", async () => { @@ -662,7 +832,9 @@ describe("compiler: checker: functions", () => { const StringResult = acceptMultipleValues("test"); const NumberResult = acceptMultipleValues(42); `); + expectDiagnosticEmpty(diagnostics); + strictEqual(receivedArgs.length, 2); strictEqual(receivedArgs[0], "test"); strictEqual(receivedArgs[1], 42); @@ -675,27 +847,50 @@ describe("compiler: checker: functions", () => { scalar TestScalar extends string; alias Result = acceptMultipleTypes(TestScalar); `); + expectDiagnostics(diagnostics, { code: "unassignable", + message: "Type 'TestScalar' is not assignable to type 'Model | Enum'", }); }); it("can return type from function", async () => { - const diagnostics = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnTypeOrValue(returnType: valueof boolean): unknown; alias TypeResult = returnTypeOrValue(true); - `); + + model Observer { + @test p: TypeResult; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + deepStrictEqual(receivedArgs, [true]); + + strictEqual(p.type.kind, "Scalar"); + strictEqual(p.type.name, "string"); }); it("can return value from function", async () => { - const diagnostics = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnTypeOrValue(returnType: valueof boolean): valueof string; const ValueResult = returnTypeOrValue(false); - `); + + model Observer { + @test p: string = ValueResult; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); + + deepStrictEqual(receivedArgs, [false]); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue?.valueKind, "StringValue"); + strictEqual(p.defaultValue?.value, "hello"); }); }); @@ -733,12 +928,12 @@ describe("compiler: checker: functions", () => { }); }); - it("errors when function returns wrong type kind", async () => { + it("errors when function returns wrong entity kind", async () => { const diagnostics = await runner.diagnose(` extern fn returnWrongEntityKind(): unknown; alias X = returnWrongEntityKind(); `); - // Should get diagnostics about type mismatch in return value + expectDiagnostics(diagnostics, { code: "function-return", message: @@ -759,109 +954,61 @@ describe("compiler: checker: functions", () => { }); }); - it("handles JS function that throws", async () => { - // Wrap in try-catch to handle JS errors gracefully + it("thrown JS error bubbles up as ICE", async () => { try { const _diagnostics = await runner.diagnose(` extern fn throwError(): unknown; alias X = throwError(); `); - // If we get here, the function didn't throw (unexpected) + + fail("Expected error to be thrown"); } catch (error) { - // Expected - JS function threw an error ok(error instanceof Error); strictEqual(error.message, "JS error"); } }); - it("handles undefined return value", async () => { - const _diagnostics = await runner.diagnose(` + it("returns null for undefined return in value position", async () => { + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnUndefined(): valueof unknown; const X = returnUndefined(); - `); - // Should handle undefined appropriately - expectDiagnosticEmpty(_diagnostics); + + model Observer { + @test p: unknown = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + + expectDiagnosticEmpty(diagnostics); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue?.valueKind, "NullValue"); }); it("handles null return value", async () => { - const _diagnostics = await runner.diagnose(` + const [{ p }, diagnostics] = (await runner.compileAndDiagnose(` extern fn returnNull(): valueof unknown; const X = returnNull(); - `); - // Should handle null appropriately - expectDiagnosticEmpty(_diagnostics); + + model Observer { + @test p: unknown = X; + } + `)) as [{ p: ModelProperty }, Diagnostic[]]; + + expectDiagnosticEmpty(diagnostics); + + strictEqual(p.defaultValue?.entityKind, "Value"); + strictEqual(p.defaultValue?.valueKind, "NullValue"); }); it("validates required parameter after optional not allowed in regular param position", async () => { - const _diagnostics = await runner.diagnose(` + const diagnostics = await runner.diagnose(` extern fn expectNonOptionalAfterOptional(opt?: valueof string, req: valueof string): valueof string; const X = expectNonOptionalAfterOptional("test"); `); - // This should be a syntax/declaration error - required params can't follow optional ones - // except when rest parameters are involved - }); - - it("handles deeply nested type constraints", async () => { - testHost.addJsFile("nested.js", { - $functions: { - "": { - processNestedModel(_ctx: FunctionContext, model: Type) { - return model; - }, - }, - }, - }); - const nestedRunner = createTestWrapper(testHost, { - autoImports: ["./nested.js"], - autoUsings: ["TypeSpec.Reflection"], - }); - - const _diagnostics = await nestedRunner.diagnose(` - extern fn processNestedModel(m: Reflection.Model): Reflection.Model; - - model Level1 { - level2: { - level3: { - deep: string; - }[]; - }; - } - - alias X = processNestedModel(Level1); - `); - expectDiagnosticEmpty(_diagnostics); - }); - - it("validates function return type matches declared constraint", async () => { - testHost.addJsFile("return-validation.js", { - $functions: { - "": { - returnString(_ctx: FunctionContext) { - return "hello"; - }, - returnNumber(_ctx: FunctionContext) { - return 42; - }, - }, - }, - }); - const returnRunner = createTestWrapper(testHost, { - autoImports: ["./return-validation.js"], - autoUsings: ["TypeSpec.Reflection"], - }); - const diagnostics = await returnRunner.diagnose(` - extern fn returnString(): valueof string; - extern fn returnNumber(): valueof string; // Wrong: returns number but declares string - - const X = returnString(); - const Y = returnNumber(); - `); - // Should get diagnostic about return type mismatch for returnNumber expectDiagnostics(diagnostics, { - code: "function-return", - message: - "Implementation of function 'returnNumber' returned value '42', which is not assignable to the declared return type 'valueof string'.", + code: "required-parameter-first", + message: "A required parameter cannot follow an optional parameter.", }); }); }); @@ -940,6 +1087,7 @@ describe("compiler: checker: functions", () => { @test prop: ArrayOf; } `)) as [{ prop: ModelProperty }, Diagnostic[]]; + expectDiagnosticEmpty(diagnostics); ok(prop.type); @@ -955,6 +1103,7 @@ describe("compiler: checker: functions", () => { model TestModel {} alias Result = ProcessModel; `); + expectDiagnosticEmpty(diagnostics); }); @@ -967,9 +1116,41 @@ describe("compiler: checker: functions", () => { enum TestEnum { A } alias Result = ProcessModel; `); + expectDiagnostics(diagnostics, { code: "invalid-argument", }); }); + + it("template instantiations of function calls yield identical instances", async () => { + const [{ A, B }, diagnostics] = (await runner.compileAndDiagnose(` + extern fn processGeneric(T: unknown): unknown; + + alias ArrayOf = processGeneric(T); + + @test + model A { + propA: ArrayOf; + } + + @test + model B { + propB: ArrayOf; + } + `)) as [{ A: Model; B: Model }, Diagnostic[]]; + + expectDiagnosticEmpty(diagnostics); + + const aProp = A.properties.get("propA"); + const bProp = B.properties.get("propB"); + + ok(aProp); + ok(bProp); + + ok($(runner.program).array.is(aProp.type)); + ok($(runner.program).array.is(bProp.type)); + + strictEqual(aProp.type, bProp.type); + }); }); }); From b64b0671a40025b7e6670b71696c735309ea93f2 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 12:55:17 -0500 Subject: [PATCH 27/30] chronus --- ...mple-msft-extern-fn-2025-10-21-12-47-57.md | 9 +++++++++ ...mple-msft-extern-fn-2025-10-21-12-48-14.md | 7 +++++++ ...mple-msft-extern-fn-2025-10-21-12-48-30.md | 20 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 .chronus/changes/witemple-msft-extern-fn-2025-10-21-12-47-57.md create mode 100644 .chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-14.md create mode 100644 .chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-30.md diff --git a/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-47-57.md b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-47-57.md new file mode 100644 index 00000000000..ffa1c37382a --- /dev/null +++ b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-47-57.md @@ -0,0 +1,9 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added support for Functions, a new type graph entity and language feature. Functions enable library authors to provide input-output style transforms that operate on types and values. See [the Functions Documentation](https://typespec.io/docs/language-basics/functions/) for more information about the use and implementation of functions. + +Added an `unknown` value that can be used to denote when a property or parameter _has_ a default value, but its value cannot be expressed in TypeSpec (for example, because it depends on the server instance where it is generated, or because the service author simply does not with to specify how the default value is generated). This adds a new kind of `Value` to the language that represents a value that is not known. diff --git a/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-14.md b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-14.md new file mode 100644 index 00000000000..ad95fdc5886 --- /dev/null +++ b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-14.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/openapi3" +--- + +Updated emitter logic to ignore `unknown` values in parameter and schema property defaults. diff --git a/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-30.md b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-30.md new file mode 100644 index 00000000000..5f99ed1e74b --- /dev/null +++ b/.chronus/changes/witemple-msft-extern-fn-2025-10-21-12-48-30.md @@ -0,0 +1,20 @@ +--- +changeKind: internal +packages: + - "@typespec/events" + - "@typespec/html-program-viewer" + - "@typespec/http-client" + - "@typespec/http" + - "@typespec/json-schema" + - "@typespec/openapi" + - "@typespec/protobuf" + - "@typespec/rest" + - "@typespec/spector" + - "@typespec/sse" + - "@typespec/streams" + - "@typespec/tspd" + - "@typespec/versioning" + - "@typespec/xml" +--- + +Updated `tspd` to generate extern function signatures. Regenerated all extern signatures. From b140017cd02a12a222fbbaf771fd759e6164bdde Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 13:03:57 -0500 Subject: [PATCH 28/30] Small tweaks to functions docs --- .../extending-typespec/implement-functions.md | 7 ++++++- .../docs/docs/language-basics/functions.md | 20 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/website/src/content/docs/docs/extending-typespec/implement-functions.md b/website/src/content/docs/docs/extending-typespec/implement-functions.md index ec78eda02ad..c23e3563e68 100644 --- a/website/src/content/docs/docs/extending-typespec/implement-functions.md +++ b/website/src/content/docs/docs/extending-typespec/implement-functions.md @@ -39,7 +39,12 @@ You can mark a function parameter as optional using `?`: ```tsp /** - * Renames a model, if + * Renames a model, if `name` is provided and different from the input model's name. + * + * @param m the input Model to rename + * @param name if set, the name of the output model + * @returns `m` if `name` is not set or `m`'s name is equal to `name`, otherwise a new Model instance with the given + * name that is otherwise identical to `m`. */ extern fn rename(m: Reflection.Model, name?: valueof string): Reflection.Model; ``` diff --git a/website/src/content/docs/docs/language-basics/functions.md b/website/src/content/docs/docs/language-basics/functions.md index 1c6fb5e3e12..0802b0ca222 100644 --- a/website/src/content/docs/docs/language-basics/functions.md +++ b/website/src/content/docs/docs/language-basics/functions.md @@ -106,22 +106,31 @@ extern fn process( ### Type transformation +Functions may be used to transform types in arbitrary ways _without_ modifying an existing type instance. In the +following example, we declare a function `applyVisibility` that could be used to transform an input Model into an +output Model based on a `VisibilityFilter` object. We use a template alias to instantiate the new instance, because +templates _cache_ their instances and always return the same type for the same template arguments. + ```typespec // Transform a model based on a filter extern fn applyVisibility(input: Model, visibility: valueof VisibilityFilter): Model; -const PUBLIC_FILTER: VisibilityFilter = #{ any: #[Public] }; +const READ_FILTER: VisibilityFilter = #{ any: #[Public] }; // Using a template to call a function can be beneficial because templates cache // their instances. A function _never_ caches its results, so each time `applyVisibility` // is called, it will run the underlying JavaScript function. By using a template to call // the function, it ensures that the function is only called once per unique instance // of the template. -alias PublicModel = applyVisibility(UserModel, PUBLIC_FILTER); +alias Read = applyVisibility(M, READ_FILTER); ``` ### Value computation +Functions can also be used to extract complex logic. The following example shows how a function might be used to compute +a default value for a given type of field. The external function can have arbitrarily complex JavaScript logic, so it +can utilize any method of computing the result value that it deems appropriate. + ```typespec // Compute a default value using some external logic extern fn computeDefault(fieldType: string): valueof unknown; @@ -138,3 +147,10 @@ model Config { - Functions _may_ have side-effects when called; they are not guaranteed to be "pure" functions. Be careful when writing functions to avoid manipulating the type graph or storing undesirable state (though there is no rule that will prevent you from doing so). +- Functions are evaluated in the compiler. If you write or utilize computationally intense functions, it will impact + compilation times and may affect language server performance. + +## Implementing functions in your library + +See [Extending TypeSpec - Functions](../extending-typespec/implement-functions.md) for more information about how to add +a function to your TypeSpec library. From 5cd7821eadb378fd415e8297dc1db083b643367e Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 13:37:29 -0500 Subject: [PATCH 29/30] Forbid functions from serving as regular types. --- packages/compiler/src/core/checker.ts | 43 ++++++++++++++----- .../compiler/test/checker/functions.test.ts | 16 +++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 1f1af0d4fb4..8a6b6e3cf77 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -1093,19 +1093,21 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param node Node. * @param mapper Type mapper for template instantiation context. * @param instantiateTemplate If templated type should be instantiated if they haven't yet. + * @param allowFunctions If functions are allowed as types. * @returns Resolved type. */ function checkTypeReference( node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplate = true, + allowFunctions = false, ): Type { const sym = resolveTypeReferenceSym(node, mapper); if (!sym) { return errorType; } - const type = checkTypeReferenceSymbol(sym, node, mapper, instantiateTemplate); + const type = checkTypeReferenceSymbol(sym, node, mapper, instantiateTemplate, allowFunctions); return type; } @@ -1421,6 +1423,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker * @param node Node * @param mapper Type mapper for template instantiation context. * @param instantiateTemplates If a templated type should be instantiated if not yet @default true + * @param allowFunctions If functions are allowed as types. @default false * @returns resolved type. */ function checkTypeReferenceSymbol( @@ -1428,8 +1431,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplates = true, + allowFunctions = false, ): Type { - const result = checkTypeOrValueReferenceSymbol(sym, node, mapper, instantiateTemplates); + const result = checkTypeOrValueReferenceSymbol( + sym, + node, + mapper, + instantiateTemplates, + allowFunctions, + ); if (result === null || isValue(result)) { reportCheckerDiagnostic(createDiagnostic({ code: "value-in-type", target: node })); return errorType; @@ -1445,8 +1455,15 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplates = true, + allowFunctions = false, ): Type | Value | IndeterminateEntity | null { - const entity = checkTypeOrValueReferenceSymbolWorker(sym, node, mapper, instantiateTemplates); + const entity = checkTypeOrValueReferenceSymbolWorker( + sym, + node, + mapper, + instantiateTemplates, + allowFunctions, + ); if (entity !== null && isType(entity) && entity.kind === "TemplateParameter") { templateParameterUsageMap.set(entity.node!, true); @@ -1459,6 +1476,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: TypeReferenceNode | MemberExpressionNode | IdentifierNode, mapper: TypeMapper | undefined, instantiateTemplates = true, + allowFunctions = false, ): Type | Value | IndeterminateEntity | null { if (sym.flags & SymbolFlags.Const) { return getValueForNode(sym.declarations[0], mapper); @@ -1472,13 +1490,13 @@ export function createChecker(program: Program, resolver: NameResolver): Checker return errorType; } - // if (sym.flags & SymbolFlags.Function) { - // reportCheckerDiagnostic( - // createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: sym }), - // ); + if (!allowFunctions && sym.flags & SymbolFlags.Function) { + reportCheckerDiagnostic( + createDiagnostic({ code: "invalid-type-ref", messageId: "function", target: node }), + ); - // return errorType; - // } + return errorType; + } const argumentNodes = node.kind === SyntaxKind.TypeReference ? node.arguments : []; const symbolLinks = getSymbolLinks(sym); @@ -4248,7 +4266,12 @@ export function createChecker(program: Program, resolver: NameResolver): Checker node: CallExpressionNode, mapper: TypeMapper | undefined, ): ScalarConstructor | Scalar | FunctionType | null { - const target = checkTypeReference(node.target, mapper); + const target = checkTypeReference( + node.target, + mapper, + /* instantiateTemplate */ true, + /* allowFunctions */ true, + ); if ( target.kind === "Scalar" || diff --git a/packages/compiler/test/checker/functions.test.ts b/packages/compiler/test/checker/functions.test.ts index 8d10b4d8a2e..c08c1a940af 100644 --- a/packages/compiler/test/checker/functions.test.ts +++ b/packages/compiler/test/checker/functions.test.ts @@ -901,6 +901,7 @@ describe("compiler: checker: functions", () => { testHost.addJsFile("test.js", { $functions: { "": { + testFn() {}, returnWrongEntityKind(_ctx: FunctionContext) { return "string value"; // Returns value when type expected }, @@ -1011,6 +1012,21 @@ describe("compiler: checker: functions", () => { message: "A required parameter cannot follow an optional parameter.", }); }); + + it("cannot be used as a regular type", async () => { + const diagnostics = await runner.diagnose(` + extern fn testFn(): unknown; + + model M { + prop: testFn; + } + `); + + expectDiagnostics(diagnostics, { + code: "invalid-type-ref", + message: "Can't use a function as a type", + }); + }); }); describe("default function results", () => { From f62f80d6a5c909ccf63b28a655cba8f7df961c6d Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 21 Nov 2025 15:07:55 -0500 Subject: [PATCH 30/30] Revert changes to pnpm lock --- pnpm-lock.yaml | 153 ++++++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5068c7dfc11..89e516b483a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 + rollup: 4.49.0 + importers: .: @@ -40,10 +44,10 @@ importers: version: 24.10.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/eslint-plugin': specifier: ^1.1.38 - version: 1.4.3(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.4.3(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.12) c8: specifier: ^10.1.3 version: 10.1.3 @@ -120,7 +124,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -181,7 +185,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -230,7 +234,7 @@ importers: version: 7.7.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -276,7 +280,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -370,7 +374,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -480,7 +484,7 @@ importers: version: 8.47.0 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -516,7 +520,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -586,7 +590,7 @@ importers: version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -634,7 +638,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -880,7 +884,7 @@ importers: version: link:../versioning '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -953,7 +957,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1096,7 +1100,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1142,7 +1146,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1175,7 +1179,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1203,7 +1207,7 @@ importers: version: 24.10.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1260,7 +1264,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1339,7 +1343,7 @@ importers: version: link:../xml '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1373,7 +1377,7 @@ importers: version: 24.10.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1612,7 +1616,7 @@ importers: version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1679,7 +1683,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1740,7 +1744,7 @@ importers: version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1785,7 +1789,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1858,7 +1862,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -1916,7 +1920,7 @@ importers: version: 0.4.14 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2126,7 +2130,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2166,7 +2170,7 @@ importers: version: 24.10.1 '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2214,7 +2218,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2315,7 +2319,7 @@ importers: version: link:../prettier-plugin-typespec '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2372,7 +2376,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2447,7 +2451,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2480,7 +2484,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.4 - version: 4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.12(vitest@4.0.12) '@vitest/ui': specifier: ^4.0.4 version: 4.0.12(vitest@4.0.12) @@ -2643,7 +2647,7 @@ importers: version: 6.1.2 vite-plugin-node-polyfills: specifier: ^0.24.0 - version: 0.24.0(rollup@4.49.0)(vite@6.4.1(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 0.24.0(rollup@4.49.0)(vite@7.2.4(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)) packages: @@ -5601,7 +5605,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -5612,7 +5616,7 @@ packages: resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: rollup: optional: true @@ -5621,7 +5625,7 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: rollup: optional: true @@ -5944,7 +5948,7 @@ packages: resolution: {integrity: sha512-OtLUWHIm3SDGtclQn6Mdd/YsWizLBgdEBRAdekGtwI/TvICfT7gpWYIycP53v2t9ufu2MIXjsxtV2maZKs8sZg==} peerDependencies: esbuild: '*' - rollup: '*' + rollup: 4.49.0 storybook: ^10.0.8 vite: '*' webpack: '*' @@ -7886,10 +7890,6 @@ packages: engines: {node: '>=20'} hasBin: true - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -11528,7 +11528,7 @@ packages: engines: {node: '>= 14.18.0'} readline-sync@1.4.9: - resolution: {integrity: sha1-PtqOZfI80qF+YTAbHwADOWr17No=} + resolution: {integrity: sha512-mp5h1N39kuKbCRGebLPIKTBOhuDw55GaNg5S+K9TW9uDAS1wIHpGUc2YokdUMZJb8GqS49sWmWEDijaESYh0Hg==} engines: {node: '>= 0.8.0'} realpath-missing@1.1.0: @@ -11752,7 +11752,7 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 2.x || 3.x || 4.x + rollup: 4.49.0 peerDependenciesMeta: rolldown: optional: true @@ -19024,7 +19024,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.12(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/coverage-v8@4.0.12(vitest@4.0.12)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.12 @@ -19041,7 +19041,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.4.3(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/eslint-plugin@1.4.3(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.12)': dependencies: '@typescript-eslint/scope-manager': 8.47.0 '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) @@ -19421,7 +19421,7 @@ snapshots: '@yarnpkg/libui@3.0.2(ink@3.2.0(@types/react@19.2.6)(react@17.0.2))(react@17.0.2)': dependencies: - ink: 3.2.0(@types/react@19.2.6)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.6)(react@18.3.1) react: 17.0.2 tslib: 2.8.1 @@ -19702,7 +19702,7 @@ snapshots: '@yarnpkg/plugin-git': 3.1.3(@yarnpkg/core@4.5.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.42.0 - ink: 3.2.0(@types/react@19.2.6)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.6)(react@18.3.1) react: 17.0.2 semver: 7.7.3 tslib: 2.8.1 @@ -19736,7 +19736,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -20934,12 +20934,6 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -23021,7 +23015,7 @@ snapshots: ink-text-input@4.0.3(ink@3.2.0(@types/react@19.2.6)(react@17.0.2))(react@17.0.2): dependencies: chalk: 4.1.2 - ink: 3.2.0(@types/react@19.2.6)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.6)(react@18.3.1) react: 17.0.2 type-fest: 0.15.1 @@ -23057,6 +23051,38 @@ snapshots: - bufferutil - utf-8-validate + ink@3.2.0(@types/react@19.2.6)(react@18.3.1): + dependencies: + ansi-escapes: 4.3.2 + auto-bind: 4.0.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + cli-cursor: 3.1.0 + cli-truncate: 2.1.0 + code-excerpt: 3.0.0 + indent-string: 4.0.0 + is-ci: 2.0.0 + lodash: 4.17.21 + patch-console: 1.0.0 + react: 18.3.1 + react-devtools-core: 4.28.5 + react-reconciler: 0.26.2(react@18.3.1) + scheduler: 0.20.2 + signal-exit: 3.0.7 + slice-ansi: 3.0.0 + stack-utils: 2.0.6 + string-width: 4.2.3 + type-fest: 0.12.0 + widest-line: 3.1.0 + wrap-ansi: 6.2.0 + ws: 7.5.10 + yoga-layout-prebuilt: 1.10.0 + optionalDependencies: + '@types/react': 19.2.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + inline-style-parser@0.2.7: {} inquirer@13.0.1(@types/node@24.10.1): @@ -25504,6 +25530,13 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 + react-reconciler@0.26.2(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 18.3.1 + scheduler: 0.20.2 + react-refresh@0.17.0: {} react-refresh@0.18.0: {} @@ -27297,14 +27330,6 @@ snapshots: - rollup - supports-color - vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@6.4.1(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)): - dependencies: - '@rollup/plugin-inject': 5.0.5(rollup@4.49.0) - node-stdlib-browser: 1.3.1 - vite: 6.4.1(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1) - transitivePeerDependencies: - - rollup - vite-plugin-node-polyfills@0.24.0(rollup@4.49.0)(vite@7.2.4(@types/node@24.10.1)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.49.0)