From be053347e09cfef450081ac3d70799c8c8e4b844 Mon Sep 17 00:00:00 2001 From: naumovdmitrii Date: Wed, 4 Dec 2024 18:51:55 +0300 Subject: [PATCH] class, interface, func import, export, as --- .../compiler-plugin/src/analysis-visitor.ts | 20 +- .../compiler-plugin/src/import-export.ts | 3 +- .../compiler-plugin/src/koala-transformer.ts | 8 +- .../parameter-types/ParameterTypesExporter.ts | 68 +++++++ .../parameter-types/ParameterTypesImporter.ts | 182 ++++++++++++++++++ .../src/parameter-types/parameter-types.ts | 74 +++++++ incremental/compiler-plugin/src/util.ts | 29 +++ incremental/compiler-plugin/test/.gitignore | 3 +- .../examples/parameter-types/declarations.ts | 18 ++ .../test/examples/parameter-types/usages.ts | 8 + .../examples/parameter-types/declarations.ts | 55 ++++++ .../golden/examples/parameter-types/usages.ts | 17 ++ 12 files changed, 463 insertions(+), 22 deletions(-) create mode 100644 incremental/compiler-plugin/src/parameter-types/ParameterTypesExporter.ts create mode 100644 incremental/compiler-plugin/src/parameter-types/ParameterTypesImporter.ts create mode 100644 incremental/compiler-plugin/src/parameter-types/parameter-types.ts create mode 100644 incremental/compiler-plugin/test/examples/parameter-types/declarations.ts create mode 100644 incremental/compiler-plugin/test/examples/parameter-types/usages.ts create mode 100644 incremental/compiler-plugin/test/golden/examples/parameter-types/declarations.ts create mode 100644 incremental/compiler-plugin/test/golden/examples/parameter-types/usages.ts diff --git a/incremental/compiler-plugin/src/analysis-visitor.ts b/incremental/compiler-plugin/src/analysis-visitor.ts index bc2b72324..b47ba3b39 100644 --- a/incremental/compiler-plugin/src/analysis-visitor.ts +++ b/incremental/compiler-plugin/src/analysis-visitor.ts @@ -20,32 +20,16 @@ import { FunctionKind, Tracer, findSourceFile, - error, asString, - RuntimeNames, isFunctionOrMethod, getComment, - arrayAt, getDeclarationsByNode, findFunctionDeclaration, isMemoEntry, - skipParenthesizedExpression, + skipParenthesizedExpression, parseComment, } from "./util" import { ImportExport } from './import-export'; - -function parseComment(comment: string): FunctionKind { - let kind = FunctionKind.REGULAR - if (comment.includes(RuntimeNames.ANNOTATION_INTRINSIC)) { - kind = FunctionKind.MEMO_INTRINSIC - } else if (comment.includes(RuntimeNames.ANNOTATION_ENTRY)) { - // Do nothing - } else if (comment.includes(RuntimeNames.ANNOTATION)) { - kind = FunctionKind.MEMO - } - return kind -} - export class AnalysisVisitor extends AbstractVisitor { constructor( public tracer: Tracer, @@ -57,7 +41,7 @@ export class AnalysisVisitor extends AbstractVisitor { super(ctx) } - private importExport = new ImportExport(this.typechecker, this.sourceFile) + private importExport = new ImportExport(this.typechecker) trace(msg: any) { this.tracer.trace(msg) diff --git a/incremental/compiler-plugin/src/import-export.ts b/incremental/compiler-plugin/src/import-export.ts index 6ead61791..7a5cabc36 100644 --- a/incremental/compiler-plugin/src/import-export.ts +++ b/incremental/compiler-plugin/src/import-export.ts @@ -23,8 +23,7 @@ import { export class ImportExport { constructor( - public typechecker: ts.TypeChecker, - public sourceFile: ts.SourceFile, + public typechecker: ts.TypeChecker ) {} isRealDeclaration(declaration: ts.Declaration): boolean { diff --git a/incremental/compiler-plugin/src/koala-transformer.ts b/incremental/compiler-plugin/src/koala-transformer.ts index 38a12939f..9f89aa519 100644 --- a/incremental/compiler-plugin/src/koala-transformer.ts +++ b/incremental/compiler-plugin/src/koala-transformer.ts @@ -25,6 +25,8 @@ import { Tracer, TransformerOptions } from "./util" import { DumpVisitor } from './dump-visitor'; import { ThisTransformer } from './this-transformer'; import { ReturnTransformer } from './return-transformer'; +import { ParameterTypesExporter } from "./parameter-types/ParameterTypesExporter"; +import { ParameterTypesImporter } from "./parameter-types/ParameterTypesImporter"; function getUnmemoizedPath(sourcePath: string, root: string, extension: string | undefined, unmemoizedDir?: string) { let relativePath = path.relative(root, sourcePath) @@ -44,6 +46,8 @@ export function memoTransform( rewrite: Rewrite ): ts.SourceFile { const analysisVisitor = new AnalysisVisitor(tracer, typeChecker, sourceFile, rewrite, ctx) + const parameterTypesExporter = new ParameterTypesExporter(tracer, typeChecker, sourceFile, rewrite, ctx); + const parameterTypesImporter = new ParameterTypesImporter(tracer, typeChecker, sourceFile, rewrite, ctx); const diagnosticsVisitor = new DiagnosticsVisitor(tracer, typeChecker, sourceFile, rewrite, extras, ctx) const functionTransformer = new FunctionTransformer(tracer, typeChecker, sourceFile, rewrite, ctx) const parameterTransformer = new ParameterTransformer(tracer, typeChecker, sourceFile, rewrite.positionalIdTracker, rewrite.functionTable, ctx) @@ -56,7 +60,9 @@ export function memoTransform( // The AST tree should be intact above this line. - const transformedFunctions = functionTransformer.visitor(sourceFile) + const parameterTypesExported = parameterTypesExporter.visitor(sourceFile) + const parameterTypesImported = parameterTypesImporter.visitor(parameterTypesExported) + const transformedFunctions = functionTransformer.visitor(parameterTypesImported) const transformedParameters = parameterTransformer.visitor(transformedFunctions) const transformedThis = thisTransformer.visitor(transformedParameters) const transformedReturn = returnTransformer.visitor(transformedThis) diff --git a/incremental/compiler-plugin/src/parameter-types/ParameterTypesExporter.ts b/incremental/compiler-plugin/src/parameter-types/ParameterTypesExporter.ts new file mode 100644 index 000000000..29bca8679 --- /dev/null +++ b/incremental/compiler-plugin/src/parameter-types/ParameterTypesExporter.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbstractVisitor } from "../AbstractVisitor" +import { Tracer } from "../util" +import * as ts from "typescript" +import { Rewrite } from "../transformation-context" +import { extractParameters, isExtractable, ParameterType } from "./parameter-types" + +/** + * TODO: support type parameters + */ +export class ParameterTypesExporter extends AbstractVisitor { + constructor( + public tracer: Tracer, + public typechecker: ts.TypeChecker, + public sourceFile: ts.SourceFile, + public rewrite: Rewrite, + ctx: ts.TransformationContext + ) { + super(ctx) + } + + private topLevelDeclarations: ParameterType[] = [] + + visitor(beforeChildren: ts.Node): ts.Node { + const node = this.visitEachChild(beforeChildren) + if (ts.isSourceFile(node)) { + return this.addTopLevelDeclarations(node) + } + if (isExtractable(node)) { + this.topLevelDeclarations.push(...extractParameters(node)) + } + return node + } + + private addTopLevelDeclarations(node: ts.SourceFile) { + return ts.factory.updateSourceFile( + node, + [ + ...node.statements, + ...this.topLevelDeclarations.map(it => ts.factory.createTypeAliasDeclaration( + [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(it.name), + undefined, + it.type + )) + ], + node.isDeclarationFile, + node.referencedFiles, + node.typeReferenceDirectives, + node.hasNoDefaultLib, + node.libReferenceDirectives + ) + } +} diff --git a/incremental/compiler-plugin/src/parameter-types/ParameterTypesImporter.ts b/incremental/compiler-plugin/src/parameter-types/ParameterTypesImporter.ts new file mode 100644 index 000000000..705e80380 --- /dev/null +++ b/incremental/compiler-plugin/src/parameter-types/ParameterTypesImporter.ts @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript" +import { AbstractVisitor } from "../AbstractVisitor" +import { isAnyMemoKind, isDefined, Tracer, zip } from "../util" +import { Rewrite } from "../transformation-context" +import { ImportExport } from "../import-export" +import { isExtractable, extractParameters, Extractable } from "./parameter-types" + +type Associated = ts.ClassDeclaration | ts.InterfaceDeclaration | ts.FunctionDeclaration + +function isAssociated(node: ts.Node): node is Associated { + return ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isFunctionDeclaration(node) +} + +export class ParameterTypesImporter extends AbstractVisitor { + private importExport = new ImportExport(this.typechecker) + private used: Map = new Map() + + constructor( + public tracer: Tracer, + public typechecker: ts.TypeChecker, + public sourceFile: ts.SourceFile, + public rewrite: Rewrite, + ctx: ts.TransformationContext + ) { + super(ctx) + } + + visitor(beforeChildren: ts.Node): ts.Node { + const node = this.visitEachChild(beforeChildren) + if (ts.isSourceFile(node)) { + return this.updateImports(node) + } + if (ts.isCallExpression(node)) { + if (isAnyMemoKind(this.rewrite.callTable.get(ts.getOriginalNode(node, ts.isCallExpression)))) { + const declaration = this.findExtractableDeclaration(node) + return this.transformCall(node, declaration) + } + } + return node + } + + private transformCall(node: ts.CallExpression, declaration: Extractable | undefined): ts.CallExpression { + if (declaration === undefined) return node + + const newArguments = zip(node.arguments, extractParameters(declaration)) + .map(([arg, { name }]) => + ts.factory.createAsExpression( + ts.factory.createParenthesizedExpression(arg), + ts.factory.createTypeReferenceNode(this.withMarkUsed(name, declaration)) + ) + ) + return ts.factory.updateCallExpression( + node, + node.expression, + node.typeArguments, + newArguments + ) + } + + private addImports(node: ts.ImportDeclaration) { + /* + TODO: only named imports supported for now + */ + if (node.importClause === undefined) return node + const namedImports = node.importClause.namedBindings + if (namedImports === undefined || !ts.isNamedImports(namedImports)) return node + + const add = namedImports.elements + .map(it => this.importExport.findRealDeclaration(it.name)) + .filter(isDefined) + .filter(isAssociated) + .filter(it => this.used.has(it)) + .flatMap(it => this.used.get(it)!) + .map(it => ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(it) + )) + + console.log('ADD IMPORTS', add.map(it => ts.idText(it.name))) + // if (add.length !== 0) throw 'OK' + + return ts.factory.updateImportDeclaration( + node, + node.modifiers, + ts.factory.updateImportClause( + node.importClause, + node.importClause.isTypeOnly, + node.importClause.name, + ts.factory.updateNamedImports( + namedImports, + [ + ...namedImports.elements, + ...add + ] + ) + ), + node.moduleSpecifier, + node.assertClause + ) + } + + private findExtractableDeclaration(node: ts.CallExpression): Extractable | undefined { + let real: ts.Node | undefined = undefined + if (ts.isIdentifier(node.expression)) { + real = this.importExport.findRealDeclaration(node.expression) + } + if (ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.name)) { + real = this.importExport.findRealMethodDeclaration(node.expression.name) + } + + if (real === undefined) return undefined + if (isExtractable(real)) return real + return undefined + } + + private withMarkUsed(name: string, declaration: Extractable): string { + const associated = this.associatedByExtractable(declaration) + if (associated === undefined) return name + + const stored = this.used.get(associated) + if (stored === undefined) { + this.used.set(associated, [name]) + return name + } + stored.push(name) + return name + } + + private updateImports(node: ts.SourceFile) { + const updated = node.statements + .map(it => { + if (ts.isImportDeclaration(it)) { + return this.addImports(it) + } + return it + }) + + return ts.factory.updateSourceFile( + node, + updated, + node.isDeclarationFile, + node.referencedFiles, + node.typeReferenceDirectives, + node.hasNoDefaultLib, + node.libReferenceDirectives + ) + } + + private associatedByExtractable(declaration: Extractable): Associated | undefined { + if (ts.isFunctionDeclaration(declaration)) { + return declaration + } + if (ts.isMethodDeclaration(declaration)) { + if (ts.isClassDeclaration(declaration.parent)) { + return declaration.parent + } + } + if (ts.isMethodDeclaration(declaration)) { + if (ts.isInterfaceDeclaration(declaration.parent)) { + return declaration.parent + } + } + console.log(`Warning: could not associate @memo method with it's parent class or interface`) + return undefined + } +} diff --git a/incremental/compiler-plugin/src/parameter-types/parameter-types.ts b/incremental/compiler-plugin/src/parameter-types/parameter-types.ts new file mode 100644 index 000000000..7eef9bfb4 --- /dev/null +++ b/incremental/compiler-plugin/src/parameter-types/parameter-types.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022-2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as ts from "typescript" +import { assertUnreachable, exportParameterTypeName, getComment, isAnyMemoKind, isDefined, parseComment } from "../util" + +export type ParameterType = { + name: string, + type: ts.TypeNode +} + +export type Extractable = ts.FunctionDeclaration | ts.MethodDeclaration | ts.MethodSignature + +export function isExtractable(node: ts.Node): node is Extractable { + node = ts.getOriginalNode(node) + if (ts.isMethodSignature(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) { + return isAnyMemoKind(parseComment(getComment(node.getSourceFile(), node))) + } + return false +} + +export function extractParameters(node: Extractable): ParameterType[] { + if (node.name === undefined) return [] + return node.parameters + .map((it, index) => { + const type = it.type + if (type === undefined) return undefined + const name = mangle(node, index) + if (name === undefined) return undefined + return { name, type } + }) + .filter(isDefined) +} + +function mangle(node: Extractable, index: number): string | undefined { + function mangleImpl(node: Extractable, index: number): string | undefined { + const name = node.name + if (name === undefined) return undefined + if (!ts.isIdentifier(name)) return undefined + const funcName = ts.idText(name) + if (ts.isMethodDeclaration(node)) { + if (!ts.isClassDeclaration(node.parent)) return undefined + if (node.parent.name === undefined) return undefined + const className = ts.idText(node.parent.name) + return exportParameterTypeName(funcName, index, className) + } + if (ts.isMethodSignature(node)) { + if (!ts.isInterfaceDeclaration(node.parent)) return undefined + if (node.parent.name === undefined) return undefined + const interfaceName = ts.idText(node.parent.name) + return exportParameterTypeName(funcName, index, interfaceName) + } + if (ts.isFunctionDeclaration(node)) { + return exportParameterTypeName(funcName, index) + } + assertUnreachable(node) + } + + const result = mangleImpl(node, index) + if (result === undefined) console.log(`Warning ...`) + return result +} diff --git a/incremental/compiler-plugin/src/util.ts b/incremental/compiler-plugin/src/util.ts index 74f797a2b..d238bf194 100644 --- a/incremental/compiler-plugin/src/util.ts +++ b/incremental/compiler-plugin/src/util.ts @@ -25,6 +25,18 @@ export enum FunctionKind { MEMO_INTRINSIC, } +export function parseComment(comment: string): FunctionKind { + let kind = FunctionKind.REGULAR + if (comment.includes(RuntimeNames.ANNOTATION_INTRINSIC)) { + kind = FunctionKind.MEMO_INTRINSIC + } else if (comment.includes(RuntimeNames.ANNOTATION_ENTRY)) { + // Do nothing + } else if (comment.includes(RuntimeNames.ANNOTATION)) { + kind = FunctionKind.MEMO + } + return kind +} + export type FunctionTable = Map export type CallTable = Map export type EntryTable = Set @@ -529,3 +541,20 @@ export class Tracer { this.writeTextToFile(content+"\n", fileName) } } + +export function assertUnreachable(arg: never): never { + throw new Error(`Won't compile`) +} + +export function exportParameterTypeName(functionName: string, index: number, className?: string): string { + const classPart = className ? `${className}_` : `` + return `__${classPart}${functionName}${index}` +} + +export function isDefined(value: T | null | undefined): value is T { + return value !== null && value !== undefined +} + +export function zip(left: readonly A[], right: readonly B[]): [A, B][] { + return left.map((_, i) => [left[i], right[i]]) +} \ No newline at end of file diff --git a/incremental/compiler-plugin/test/.gitignore b/incremental/compiler-plugin/test/.gitignore index 7f9dca519..8b5e94aec 100644 --- a/incremental/compiler-plugin/test/.gitignore +++ b/incremental/compiler-plugin/test/.gitignore @@ -1 +1,2 @@ -dump/ \ No newline at end of file +dump/ +/unmemoized/ diff --git a/incremental/compiler-plugin/test/examples/parameter-types/declarations.ts b/incremental/compiler-plugin/test/examples/parameter-types/declarations.ts new file mode 100644 index 000000000..807be6044 --- /dev/null +++ b/incremental/compiler-plugin/test/examples/parameter-types/declarations.ts @@ -0,0 +1,18 @@ +export class A { + /** @memo */ + foo(first: number, second: string) {} + + bar(func: () => number) {} +} + +class B { +} + +/** @memo */ +export function foo(first: A, second: Partial) {} + +/** @memo */ +export function goo(/**/ + /** @memo */ + content?: () => void +) {} diff --git a/incremental/compiler-plugin/test/examples/parameter-types/usages.ts b/incremental/compiler-plugin/test/examples/parameter-types/usages.ts new file mode 100644 index 000000000..03584c276 --- /dev/null +++ b/incremental/compiler-plugin/test/examples/parameter-types/usages.ts @@ -0,0 +1,8 @@ +import { A, foo } from "./declarations" + +/** @memo */ +function main() { + new A().foo(4, "4") + + foo(new A(), {}) +} \ No newline at end of file diff --git a/incremental/compiler-plugin/test/golden/examples/parameter-types/declarations.ts b/incremental/compiler-plugin/test/golden/examples/parameter-types/declarations.ts new file mode 100644 index 000000000..dbd944188 --- /dev/null +++ b/incremental/compiler-plugin/test/golden/examples/parameter-types/declarations.ts @@ -0,0 +1,55 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +export class A { + /** @memo */ + foo(__memo_context: __memo_context_type, __memo_id: __memo_id_type, first: number, second: string) { + const __memo_scope = __memo_context.scope(__memo_id + ("0___key_id_DIRNAME/declarations.ts" as __memo_id_type), 3); + const __memo_parameter_first = __memo_scope.param(1, first); + const __memo_parameter_second = __memo_scope.param(2, second); + const __memo_parameter_this = __memo_scope.param(0, this); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + { + __memo_scope.recache(); + return; + } + } + bar(func: () => number) { } +} +class B { +} +/** @memo */ +export function foo(__memo_context: __memo_context_type, __memo_id: __memo_id_type, first: A, second: Partial) { + const __memo_scope = __memo_context.scope(__memo_id + ("1___key_id_DIRNAME/declarations.ts" as __memo_id_type), 2); + const __memo_parameter_first = __memo_scope.param(0, first); + const __memo_parameter_second = __memo_scope.param(1, second); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + { + __memo_scope.recache(); + return; + } +} +/** @memo */ +export function goo(__memo_context: __memo_context_type, __memo_id: __memo_id_type, /**/ +/** @memo */ +content?: (__memo_context: __memo_context_type, __memo_id: __memo_id_type) => void) { + const __memo_scope = __memo_context.scope(__memo_id + ("2___key_id_DIRNAME/declarations.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + { + __memo_scope.recache(); + return; + } +} +export type __A_foo0 = number; +export type __A_foo1 = string; +export type __foo0 = A; +export type __foo1 = Partial; +export type __goo0 = () => void; + diff --git a/incremental/compiler-plugin/test/golden/examples/parameter-types/usages.ts b/incremental/compiler-plugin/test/golden/examples/parameter-types/usages.ts new file mode 100644 index 000000000..c84aaabaa --- /dev/null +++ b/incremental/compiler-plugin/test/golden/examples/parameter-types/usages.ts @@ -0,0 +1,17 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +import { A, foo, __A_foo0, __A_foo1, __foo0, __foo1 } from "./declarations"; +/** @memo */ +function main(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("3___key_id_DIRNAME/usages.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + new A().foo(__memo_context, __memo_id + ("0___key_id_DIRNAME/usages.ts" as __memo_id_type), (4) as __A_foo0, ("4") as __A_foo1); + foo(__memo_context, __memo_id + ("2___key_id_DIRNAME/usages.ts" as __memo_id_type), (new A()) as __foo0, (__memo_context.compute(__memo_id + ("1___key_id_DIRNAME/usages.ts" as __memo_id_type), () => ({}))) as __foo1); + { + __memo_scope.recache(); + return; + } +} + -- Gitee