diff --git a/incremental/compiler-plugin/src/analysis-visitor.ts b/incremental/compiler-plugin/src/analysis-visitor.ts deleted file mode 100644 index bc2b723243143674e2438539c029c00a542646ef..0000000000000000000000000000000000000000 --- a/incremental/compiler-plugin/src/analysis-visitor.ts +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 { Rewrite } from './transformation-context'; -import { - FunctionKind, - Tracer, - findSourceFile, - error, - asString, - RuntimeNames, - isFunctionOrMethod, - getComment, - arrayAt, - getDeclarationsByNode, - findFunctionDeclaration, - isMemoEntry, - skipParenthesizedExpression, -} 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, - public typechecker: ts.TypeChecker, - public sourceFile: ts.SourceFile, - public rewrite: Rewrite, - ctx: ts.TransformationContext - ) { - super(ctx) - } - - private importExport = new ImportExport(this.typechecker, this.sourceFile) - - trace(msg: any) { - this.tracer.trace(msg) - } - - traceAnnotation(nodeName: string, kind: FunctionKind, node: ts.Node) { - if (kind) { - this.trace(`${nodeName} HAS annotation: ${asString(node)} to ${FunctionKind[kind]}`) - } else { - this.trace(`${nodeName} doesn't have annotations: ${asString(node)}`) - } - } - - immediateFunctionKind(source: ts.SourceFile, node: ts.Node, name: string): FunctionKind { - const comment = getComment(source, node) - let kind = parseComment(comment) - this.traceAnnotation(name, kind, node) - return kind - } - - callIsEligible(identifier: ts.Identifier|undefined): FunctionKind { - - if (!identifier) return FunctionKind.REGULAR - - const decl = this.importExport.findRealDeclaration(identifier) - if (!decl) return FunctionKind.REGULAR - - const source = findSourceFile(decl) - if (!source) return FunctionKind.REGULAR - - return this.variableDeclarationAnnotation(source, decl) - } - - methodCallIsEligible(name: ts.MemberName): FunctionKind { - - if (!ts.isIdentifier(name)) return FunctionKind.REGULAR - - const decl = this.findRealMethodDeclaration(name) - if (!decl) return FunctionKind.REGULAR - - const source = findSourceFile(decl) - if (!source) return FunctionKind.REGULAR - - return this.immediateFunctionKind(source, decl, "METHOD CALL") - } - - isInMemoEntry(node: ts.CallExpression): boolean { - const enclosingFunction = findFunctionDeclaration(node) - if (enclosingFunction === undefined) return false - return isMemoEntry(this.sourceFile, enclosingFunction) - } - - parameterHasAnnotation(parameter: ts.ParameterDeclaration): FunctionKind { - const source = findSourceFile(parameter) - if (!source) return FunctionKind.REGULAR - - return this.immediateFunctionKind(source, parameter, "Parameter") - } - - parameterDeclarationHasAnnotation(functionIdentifier: ts.Identifier|undefined, parameterIndex: number): FunctionKind { - if (!functionIdentifier) return FunctionKind.REGULAR - - const functionDeclaration = this.importExport.findRealDeclaration(functionIdentifier) - if (!functionDeclaration) return FunctionKind.REGULAR - if (!isFunctionOrMethod(functionDeclaration)) return FunctionKind.REGULAR - - const parameterDeclaration = functionDeclaration.parameters[parameterIndex] - return this.parameterHasAnnotation(parameterDeclaration) - } - - variableDeclarationAnnotation(sourceFile: ts.SourceFile, variable: ts.Node): FunctionKind { - let immediateFunctionKind = this.immediateFunctionKind(sourceFile, variable, "Variable") - if (immediateFunctionKind != FunctionKind.REGULAR) { - return immediateFunctionKind - } - - let parent = variable.parent - if (!ts.isVariableDeclarationList(parent)) { - return FunctionKind.REGULAR - } - - return this.immediateFunctionKind(sourceFile, parent, "VariableDeclarationList") - } - - variableDeclarationListAnnotation(sourceFile: ts.SourceFile, variableList: ts.VariableDeclarationList): FunctionKind { - return this.immediateFunctionKind(this.sourceFile, variableList, "VariableDeclarationList") - } - - propertyHasAnnotation(sourceFile: ts.SourceFile, property: ts.PropertyDeclaration): FunctionKind { - return this.immediateFunctionKind(sourceFile, property, "Property") - } - - propertySignatureHasAnnotation(sourceFile: ts.SourceFile, property: ts.PropertySignature): FunctionKind { - return this.immediateFunctionKind(sourceFile, property, "Property signature") - } - - functionTypeHasAnnotation(sourceFile: ts.SourceFile, functionType: ts.FunctionTypeNode): FunctionKind { - return this.immediateFunctionKind(sourceFile, functionType, "Function type") - } - - methodSignatureHasAnnotation(sourceFile: ts.SourceFile, signature: ts.MethodSignature): FunctionKind { - return this.immediateFunctionKind(sourceFile, signature, "Method signature") - } - - getterHasAnnotation(sourceFile: ts.SourceFile, getter: ts.GetAccessorDeclaration): FunctionKind { - return this.immediateFunctionKind(sourceFile, getter, "Getter") - } - - setterHasAnnotation(sourceFile: ts.SourceFile, setter: ts.SetAccessorDeclaration): FunctionKind { - return this.immediateFunctionKind(sourceFile, setter, "Setter") - } - - isDirectMemoVariableInitializer(sourceFile: ts.SourceFile, node: ts.Node): boolean { - if (!ts.isVariableDeclaration(node.parent)) return false - const variable = node.parent - let kind = this.variableDeclarationAnnotation(sourceFile, variable) - return kind != FunctionKind.REGULAR - } - - isDirectArgumentForMemoParameter(sourceFile: ts.SourceFile, node: ts.Node): boolean { - const parent = node.parent - if (!ts.isCallExpression(parent)) return false - - let callable: ts.Identifier - if (ts.isIdentifier(parent.expression)) { - callable = parent.expression - } else if (ts.isPropertyAccessExpression(parent.expression)) { - if (ts.isPrivateIdentifier(parent.expression.name)) return false - callable = parent.expression.name - } else { - return false - } - - const index = parent.arguments.findIndex(it => it === node) - if (index < 0) return false - - return this.parameterDeclarationHasAnnotation(callable, index) != FunctionKind.REGULAR - - } - - getAssigneeTransformation(sourceFile: ts.SourceFile, node: ts.Node): FunctionKind { - if (this.isDirectMemoVariableInitializer(sourceFile, node)) return FunctionKind.MEMO - if (this.isDirectArgumentForMemoParameter(sourceFile, node)) return FunctionKind.MEMO - else return FunctionKind.REGULAR - } - - declarationTransformKind(node: ts.FunctionLikeDeclarationBase|undefined): FunctionKind { - if (node === undefined) return FunctionKind.REGULAR - - const name = node.name - const nameString = (name && ts.isIdentifier(name)) ? - ts.idText(name) : - "Couldn't take declaration name" - - let immediateFunctionKind = this.immediateFunctionKind(this.sourceFile, node, "Declaration") - if (immediateFunctionKind != FunctionKind.REGULAR) { - return immediateFunctionKind - } - - let transform = this.getAssigneeTransformation(this.sourceFile, node) - - if (transform != FunctionKind.REGULAR) { - this.trace(`DECLARATION is ELIGIBLE to ${FunctionKind[transform]}: ${nameString}`) - } - - return transform - } - - findRealMethodDeclaration(member: ts.MemberName): ts.Node|undefined { - if (!ts.isIdentifier(member)) return undefined - const declarations = getDeclarationsByNode(this.typechecker, member) - return declarations[0] - } - - visitor(node: ts.Node): ts.Node { - if (ts.getOriginalNode(node) !== node) throw new Error("Analysis phase is expected to work on original nodes") - - if (ts.isCallExpression(node)) { - let node_expr: ts.Expression = node.expression - if (ts.isParenthesizedExpression(node.expression)) { - node_expr = skipParenthesizedExpression(node.expression) - } - if (ts.isIdentifier(node_expr)) { - const kind = this.callIsEligible(node_expr) - if (kind) { - this.rewrite.callTable.set(node, kind) - } - } else if (ts.isPropertyAccessExpression(node_expr)) { - const member = node_expr.name - const kind = this.methodCallIsEligible(member) - if (kind) { - this.rewrite.callTable.set(node, kind) - } - } - if (this.isInMemoEntry(node)) { - this.rewrite.entryTable.add(node) - } - } else if (ts.isFunctionDeclaration(node)) { - switch (this.declarationTransformKind(node)) { - case FunctionKind.MEMO: - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - break; - case FunctionKind.MEMO_INTRINSIC: - this.rewrite.functionTable.set(node, FunctionKind.MEMO_INTRINSIC) - break; - } - } else if (ts.isMethodDeclaration(node)) { - switch (this.declarationTransformKind(node)) { - case FunctionKind.MEMO: - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - break; - case FunctionKind.MEMO_INTRINSIC: - this.rewrite.functionTable.set(node, FunctionKind.MEMO_INTRINSIC) - break; - } - } else if (ts.isArrowFunction(node)) { - if (this.declarationTransformKind(node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isFunctionExpression(node)) { - if (this.declarationTransformKind(node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isParameter(node)) { - if (this.parameterHasAnnotation(node) == FunctionKind.MEMO) { - this.rewrite.variableTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isVariableDeclaration(node)) { - if (this.variableDeclarationAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.variableTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isVariableDeclarationList(node)) { - if (this.variableDeclarationListAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - node.declarations.forEach(declaration => { - this.rewrite.variableTable.set(declaration, FunctionKind.MEMO) - }) - } - } else if (ts.isPropertyDeclaration(node)) { - if (this.propertyHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.variableTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isPropertySignature(node)) { - if (this.propertySignatureHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.variableTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isFunctionTypeNode(node)) { - if (this.functionTypeHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isMethodSignature(node)) { - if (this.methodSignatureHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isGetAccessorDeclaration(node)) { - if (this.getterHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } else if (ts.isSetAccessorDeclaration(node)) { - if (this.setterHasAnnotation(this.sourceFile, node) == FunctionKind.MEMO) { - this.rewrite.functionTable.set(node, FunctionKind.MEMO) - } - } - return this.visitEachChild(node) - } -} diff --git a/incremental/compiler-plugin/src/idTracker.ts b/incremental/compiler-plugin/src/idTracker.ts new file mode 100644 index 0000000000000000000000000000000000000000..56cf9a19597ab1ee879e55726b751c875dca6c2a --- /dev/null +++ b/incremental/compiler-plugin/src/idTracker.ts @@ -0,0 +1,59 @@ +/* + * 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 * as util from './util' + +export class PositionalIdTracker { + // Global for the whole program. + static callCount: number = 0 + + // Set `stable` to true if you want to have more predictable values. + // For example for tests. + // Don't use it in production! + constructor(public sourceFile: ts.SourceFile, public stableForTests: boolean = false) { + if (stableForTests) PositionalIdTracker.callCount = 0 + } + + sha1Id(callName: string, fileName: string): string { + const uniqId = new util.UniqueId() + uniqId.addString("memo call uniqid") + uniqId.addString(fileName) + uniqId.addString(callName) + uniqId.addI32(PositionalIdTracker.callCount++) + return uniqId.compute().substring(0,10) + } + + stringId(callName: string, fileName: string): string { + return `${PositionalIdTracker.callCount++}_${callName}_id_DIRNAME/${fileName}` + } + + id(callName: string): ts.Expression { + + const fileName = this.stableForTests ? + util.baseName(this.sourceFile.fileName) : + this.sourceFile.fileName + + const positionId = (this.stableForTests) ? + this.stringId(callName, fileName) : + this.sha1Id(callName, fileName) + + + return this.stableForTests ? + ts.factory.createStringLiteral(positionId) : + ts.factory.createNumericLiteral("0x"+positionId) + + } +} diff --git a/incremental/compiler-plugin/src/index.ts b/incremental/compiler-plugin/src/index.ts index 89561a86ae7b7aaaaa33eb4019981b209a570bf5..b6b0f8cee78b8e0480f009434f9c5ce67a49d52a 100644 --- a/incremental/compiler-plugin/src/index.ts +++ b/incremental/compiler-plugin/src/index.ts @@ -13,6 +13,6 @@ * limitations under the License. */ -export { Tracer } from "./util" +export { Tracer } from "./tracer" export { memoTransform, default } from "./koala-transformer" -export { Rewrite } from "./transformation-context" \ No newline at end of file +export { Rewrite } from "./transformation-context" diff --git a/incremental/compiler-plugin/src/koala-transformer.ts b/incremental/compiler-plugin/src/koala-transformer.ts index 38a12939f4bdc6bd9dc0bf3c4a8657981388d2dd..3ebb3b3b3d97f1bfa581ed85a49a510b3af10fd8 100644 --- a/incremental/compiler-plugin/src/koala-transformer.ts +++ b/incremental/compiler-plugin/src/koala-transformer.ts @@ -14,17 +14,13 @@ */ import * as ts from 'typescript'; -import * as path from "path" -import { AnalysisVisitor } from './analysis-visitor'; -import { DiagnosticsVisitor } from './diagnostics-visitor'; -import { FunctionTransformer } from "./function-transformer" -import { ParameterTransformer } from "./parameter-transformer" -import { Rewrite } from './transformation-context'; -import { VariableTypeTransformer } from './variable-type-transformer'; -import { Tracer, TransformerOptions } from "./util" -import { DumpVisitor } from './dump-visitor'; -import { ThisTransformer } from './this-transformer'; -import { ReturnTransformer } from './return-transformer'; +import * as path from 'path' +import * as transformers from './transformers' +import { + Tracer, + TransformerOptions +} from './types' +import { Rewrite } from './transformation-context' function getUnmemoizedPath(sourcePath: string, root: string, extension: string | undefined, unmemoizedDir?: string) { let relativePath = path.relative(root, sourcePath) @@ -43,27 +39,26 @@ export function memoTransform( tracer: Tracer, rewrite: Rewrite ): ts.SourceFile { - const analysisVisitor = new AnalysisVisitor(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) - const thisTransformer = new ThisTransformer(tracer, typeChecker, sourceFile, rewrite.positionalIdTracker, rewrite.functionTable, ctx) - const variableTransformer = new VariableTypeTransformer(tracer, typeChecker, sourceFile, rewrite, ctx) - const returnTransformer = new ReturnTransformer(typeChecker, sourceFile, rewrite.functionTable, ctx) + const args: [Tracer, ts.TypeChecker, ts.SourceFile, Rewrite, ts.TransformationContext] = [tracer, typeChecker, sourceFile, rewrite, ctx] - analysisVisitor.visitor(sourceFile) - diagnosticsVisitor.visitor(sourceFile) + const stages: transformers.AbstractVisitor[] = [ + new transformers.AnalysisVisitor(...args), + new transformers.DiagnosticsVisitor(...args, extras), + // The AST tree should be intact above this line. + new transformers.FunctionTransformer(...args), + new transformers.ParameterTransformer(...args), + new transformers.ThisTransformer(...args), + new transformers.ReturnTransformer(...args), + new transformers.VariableTypeTransformer(...args), + ] - // The AST tree should be intact above this line. - - const transformedFunctions = functionTransformer.visitor(sourceFile) - const transformedParameters = parameterTransformer.visitor(transformedFunctions) - const transformedThis = thisTransformer.visitor(transformedParameters) - const transformedReturn = returnTransformer.visitor(transformedThis) - return variableTransformer.visitor(transformedReturn) as ts.SourceFile + return stages.reduce( + (sourceFile, transformer) => transformer.visitor(sourceFile) as ts.SourceFile, + sourceFile + ) } -export default function koala_transformer(program: ts.Program, pluginOptions: TransformerOptions, extras: ts.TransformerExtras) { +export default function koalaTransformer(program: ts.Program, pluginOptions: TransformerOptions, extras: ts.TransformerExtras) { const printer = ts.createPrinter() const tracer = new Tracer(pluginOptions, printer) return (ctx: ts.TransformationContext) => { @@ -71,7 +66,7 @@ export default function koala_transformer(program: ts.Program, pluginOptions: Tr return (sourceFile: ts.SourceFile) => { console.log("Koala rewrite: " + path.normalize(sourceFile.fileName)) const rewrite = new Rewrite(sourceFile, pluginOptions) - const dumpVisitor = new DumpVisitor(tracer, sourceFile, rewrite.functionTable, ctx) + const dumpVisitor = new transformers.DumpVisitor(tracer, sourceFile, rewrite.functionTable, ctx) const result = memoTransform(sourceFile, typechecker, ctx, extras, tracer, rewrite) dumpVisitor.visitor(result) if (pluginOptions.only_unmemoize) { diff --git a/incremental/compiler-plugin/src/tracer.ts b/incremental/compiler-plugin/src/tracer.ts new file mode 100644 index 0000000000000000000000000000000000000000..8879b573a93817ba74fad9f68a01b51da504b5e2 --- /dev/null +++ b/incremental/compiler-plugin/src/tracer.ts @@ -0,0 +1,64 @@ +/* + * 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 * as fs from "fs" +import * as util from "./util" +import { TransformerOptions } from "./types" + +export class Tracer { + constructor (public options: TransformerOptions, public printer: ts.Printer) { + } + + trace(msg: any): void { + if (this.options.trace === undefined) { + return + } + console.log(msg) + } + + writeTextToFile(text: string, file: string): void { + fs.writeFileSync(file, text, 'utf8') + this.trace("DUMP TO: " + file) + } + + createDirs(dirs: string): void { + fs.mkdirSync(dirs, { recursive: true }); + } + + dumpFileName(sourceFile: ts.SourceFile, transformed: ts.FunctionLikeDeclarationBase): string | undefined { + if (this.options.keepTransformed === undefined || transformed.name === undefined || !ts.isIdentifier(transformed.name)) { + return undefined + } + + const outDir = (this.options.keepTransformed[0] == "/") ? + this.options.keepTransformed : + `${__dirname}/${this.options.keepTransformed}` + this.createDirs(outDir) + + const sourceBaseName = util.baseName(sourceFile.fileName) + const fileName = `${ts.idText(transformed.name)}_${sourceBaseName}` + return `${outDir}/${fileName}_dump` + } + + keepTransformedFunction(transformed: ts.FunctionLikeDeclarationBase, sourceFile: ts.SourceFile): void { + const fileName = this.dumpFileName(sourceFile, transformed) + if (fileName === undefined) { + return + } + const content = this.printer.printNode(ts.EmitHint.Unspecified, transformed, sourceFile) + this.writeTextToFile(content+"\n", fileName) + } +} diff --git a/incremental/compiler-plugin/src/transformation-context.ts b/incremental/compiler-plugin/src/transformation-context.ts index 73c7ae48bf5f976e4d515e7c791a10645aeebe60..0b02a57be3156e2191504c695c59bbe301819542 100644 --- a/incremental/compiler-plugin/src/transformation-context.ts +++ b/incremental/compiler-plugin/src/transformation-context.ts @@ -14,7 +14,7 @@ */ import * as ts from "typescript" -import { CallTable, EntryTable, FunctionKind, FunctionTable, PositionalIdTracker, TransformerOptions, VariableTable } from "./util" +import { CallTable, EntryTable, FunctionKind, FunctionTable, PositionalIdTracker, TransformerOptions, VariableTable } from "./types" export class Rewrite { public functionTable: FunctionTable @@ -29,8 +29,7 @@ export class Rewrite { this.variableTable = new Map() this.entryTable = new Set() this.positionalIdTracker = new PositionalIdTracker(sourceFile, pluginOptions.stableForTest ?? false) - } - public importTypesFrom: string|undefined = undefined -} \ No newline at end of file + public importTypesFrom: string | undefined = undefined +} diff --git a/incremental/compiler-plugin/src/transformers/analysis-visitor.ts b/incremental/compiler-plugin/src/transformers/analysis-visitor.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c70ebd78bbae694d1c97cf9ecde9860330f1e7d --- /dev/null +++ b/incremental/compiler-plugin/src/transformers/analysis-visitor.ts @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2022-2023 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 * as util from "../util" +import { AbstractVisitor } from "./base/AbstractVisitor" +import { ImportExport } from '../import-export'; +import { Rewrite } from "../transformation-context" +import { + FunctionKind, + RuntimeNames, + Tracer, +} from "../types" + +type memoAnnotatable = + | ts.CallExpression + + | ts.VariableDeclaration + | ts.ParameterDeclaration + + | ts.FunctionDeclaration + | ts.MethodDeclaration + | ts.ArrowFunction + | ts.FunctionExpression + | ts.PropertyDeclaration + | ts.PropertySignature + | ts.FunctionTypeNode + | ts.MethodSignature + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + +function isMemoAnnotatable(node: ts.Node): node is memoAnnotatable { + return false + || ts.isCallExpression(node) + + || ts.isVariableDeclaration(node) + || ts.isParameter(node) + + || ts.isFunctionDeclaration(node) + || ts.isMethodDeclaration(node) + || ts.isArrowFunction(node) + || ts.isFunctionExpression(node) + || ts.isPropertyDeclaration(node) + || ts.isPropertySignature(node) + || ts.isFunctionTypeNode(node) + || ts.isMethodSignature(node) + || ts.isGetAccessor(node) + || ts.isSetAccessor(node) +} + +function isMemoableCall(node: ts.Node): node is ts.CallExpression { + return false + || ts.isCallExpression(node) +} + +function isMemoableVariable(node: ts.Node): node is ts.VariableLikeDeclaration { + return false + || ts.isVariableDeclaration(node) + || ts.isParameter(node) +} + +function isMemoableFunction(node: ts.Node): node is ts.SignatureDeclarationBase { + return false + || ts.isFunctionDeclaration(node) + || ts.isMethodDeclaration(node) + || ts.isArrowFunction(node) + || ts.isFunctionExpression(node) + || ts.isPropertyDeclaration(node) + || ts.isPropertySignature(node) + || ts.isFunctionTypeNode(node) + || ts.isMethodSignature(node) + || ts.isGetAccessor(node) + || ts.isSetAccessor(node) +} + +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, + public typechecker: ts.TypeChecker, + public sourceFile: ts.SourceFile, + public rewrite: Rewrite, + ctx: ts.TransformationContext + ) { + super(ctx) + } + + private importExport = new ImportExport(this.typechecker, this.sourceFile) + + trace(msg: any) { + this.tracer.trace(msg) + } + + updateRewrite(node: ts.Node, kind: FunctionKind): void { + if (kind === FunctionKind.REGULAR) { + return + } + if (isMemoableCall(node)) { + this.rewrite.callTable.set(node, kind) + return + } + if (isMemoableVariable(node)) { + this.rewrite.variableTable.set(node, kind) + return + } + if (isMemoableFunction(node)) { + this.rewrite.functionTable.set(node, kind) + return + } + throw new Error('unreachable: support new memo type') + } + + getKindOfAnnotatedNode(node: ts.Node | undefined, nodeTypeForTrace?: string): FunctionKind { + if (node === undefined) { + return FunctionKind.REGULAR + } + const source = util.findSourceFile(node) + if (source === undefined) { + return FunctionKind.REGULAR + } + const kind = parseComment(util.getComment(source, node)) + if (kind !== FunctionKind.REGULAR) { + this.trace(`${nodeTypeForTrace ?? ''} has annotation: ${util.nodeToString(node)} to ${FunctionKind[kind]}`) + } + return kind + } + + getKindOfNode(node: ts.Node | undefined): FunctionKind { + if (node === undefined) { + return FunctionKind.REGULAR + } + + if (ts.isFunctionDeclaration(node) || + ts.isMethodDeclaration(node) || + ts.isArrowFunction(node) || + ts.isFunctionExpression(node)) { + return this.getKindOfFunctionLikeDeclaration(node) + } + if (ts.isIdentifier(node)) { + return this.getKindOfIdentifier(node) + } + if (ts.isPropertyAccessExpression(node)) { + return this.getKindOfPropertyAccessExpression(node) + } + if (ts.isCallExpression(node)) { + return this.getKindOfCallExpression(node) + } + if (ts.isParameter(node)) { + return this.getKindOfParameter(node) + } + if (ts.isVariableDeclaration(node)) { + return this.getKindOfVariableDeclaration(node) + } + if (ts.isVariableDeclarationList(node)) { + return this.getKindOfVariableDeclarationList(node) + } + if (ts.isPropertyDeclaration(node)) { + return this.getKindOfProperty(node) + } + if (ts.isPropertySignature(node)) { + return this.getKindOfPropertySignature(node) + } + if (ts.isFunctionTypeNode(node)) { + return this.getKindOfFunctionType(node) + } + if (ts.isMethodSignature(node)) { + return this.getKindOfMethodSignature(node) + } + if (ts.isGetAccessor(node)) { + return this.getKindOfGetAccessor(node) + } + if (ts.isSetAccessor(node)) { + return this.getKindOfSetAccessor(node) + } + + return FunctionKind.REGULAR + } + + getParamDecl(funcIdent: ts.Identifier, node: ts.Expression): ts.ParameterDeclaration | undefined { + if (!ts.isCallExpression(node.parent)) { + return undefined + } + const decl = this.importExport.findRealDeclaration(funcIdent) + if (decl === undefined || !isMemoableFunction(decl) || decl.parameters === undefined) { + return undefined + } + if (!( + ts.isFunctionDeclaration(decl) || + ts.isMethodDeclaration(decl) || + ts.isFunctionExpression(decl) || + ts.isArrowFunction(decl) + )) { + return undefined + } + return decl.parameters[node.parent.arguments.findIndex(it => (it === node))] + } + + getKindOfFunctionLikeDeclaration(node: ts.FunctionLikeDeclarationBase): FunctionKind { + const kind = this.getKindOfAnnotatedNode(node, "FunctionLikeDeclarationBase") + if (kind !== FunctionKind.REGULAR) { + return kind + } + // f = _memo_arrow(x => x) + if (ts.isVariableDeclaration(node.parent)) { + return this.getKindOfVariableDeclaration(node.parent) + } + // f4.f3.f2.f(p1, p2, _memo_arrow(x => x)) + if (ts.isCallExpression(node.parent)) { + if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) { + return FunctionKind.REGULAR + } + if (ts.isIdentifier(node.parent.expression)) { + return this.getKindOfNode(this.getParamDecl(node.parent.expression, node)) + } + if (ts.isPropertyAccessExpression(node.parent.expression) && !ts.isPrivateIdentifier(node.parent.expression.name)) { + return this.getKindOfNode(this.getParamDecl(node.parent.expression.name, node)) + } + return FunctionKind.REGULAR + } + return FunctionKind.REGULAR + } + + // TODO: change ts.Identifier to ts.MemberName + getKindOfIdentifier(node: ts.Identifier): FunctionKind { + const decl = this.importExport.findRealDeclaration(node) + if (decl === undefined) { + return FunctionKind.REGULAR + } + return this.getKindOfNode(decl) + } + + getKindOfPropertyAccessExpression(node: ts.PropertyAccessExpression): FunctionKind { + return this.getKindOfAnnotatedNode( + util.getDeclarationsByNode(this.typechecker, node.name)[0], + "PropertyAccessExpression" + ) + } + + getKindOfCallExpression(node: ts.CallExpression): FunctionKind { + // TODO: make function for that (something like "getExpressionUnderBrackets") + let node_expr: ts.Expression = node.expression + if (ts.isParenthesizedExpression(node.expression)) { + node_expr = util.skipParenthesizedExpression(node.expression) + } + if (util.isInMemoEntry(node)) { + this.rewrite.entryTable.add(node) + } + return this.getKindOfNode(node_expr) + } + + getKindOfParameter(node: ts.ParameterDeclaration): FunctionKind { + return this.getKindOfAnnotatedNode(node, "ParameterDeclaration") + } + + getKindOfVariableDeclaration(node: ts.VariableDeclaration): FunctionKind { + const kind = this.getKindOfAnnotatedNode(node, "VariableDeclaration") + if (kind != FunctionKind.REGULAR) { + return kind + } + const parent = node.parent + if ((parent === undefined) || !ts.isVariableDeclarationList(parent)) { + return FunctionKind.REGULAR + } + return this.getKindOfAnnotatedNode(parent, "VariableDeclarationList") + } + + getKindOfVariableDeclarationList(node: ts.VariableDeclarationList): FunctionKind { + return this.getKindOfAnnotatedNode(node, "VariableDeclarationList") + } + + getKindOfProperty(node: ts.PropertyDeclaration): FunctionKind { + return this.getKindOfAnnotatedNode(node, "PropertyDeclaration") + } + + getKindOfPropertySignature(node: ts.PropertySignature): FunctionKind { + return this.getKindOfAnnotatedNode(node, "PropertySignature") + } + + getKindOfFunctionType(node: ts.FunctionTypeNode): FunctionKind { + return this.getKindOfAnnotatedNode(node, "FunctionTypeNode") + } + + getKindOfMethodSignature(node: ts.MethodSignature): FunctionKind { + return this.getKindOfAnnotatedNode(node, "MethodSignature") + } + + getKindOfGetAccessor(node: ts.GetAccessorDeclaration): FunctionKind { + return this.getKindOfAnnotatedNode(node, "Getter") + } + + getKindOfSetAccessor(node: ts.SetAccessorDeclaration): FunctionKind { + return this.getKindOfAnnotatedNode(node, "Setter") + } + + visitor(node: ts.Node): ts.Node { + if (ts.getOriginalNode(node) !== node) { + throw new Error('Analysis phase is expected to work on original nodes') + } + + if (isMemoAnnotatable(node)) { + this.updateRewrite(node, this.getKindOfNode(node)) + } + + return this.visitEachChild(node) + } +} diff --git a/incremental/compiler-plugin/src/AbstractVisitor.ts b/incremental/compiler-plugin/src/transformers/base/AbstractVisitor.ts similarity index 100% rename from incremental/compiler-plugin/src/AbstractVisitor.ts rename to incremental/compiler-plugin/src/transformers/base/AbstractVisitor.ts diff --git a/incremental/compiler-plugin/src/ScopedVisitor.ts b/incremental/compiler-plugin/src/transformers/base/ScopedVisitor.ts similarity index 67% rename from incremental/compiler-plugin/src/ScopedVisitor.ts rename to incremental/compiler-plugin/src/transformers/base/ScopedVisitor.ts index 5df96aff615cf38abb23feac0b483407d07e7799..a0ab8bda767ee2b5073492c5a90de4f2c842f8fc 100644 --- a/incremental/compiler-plugin/src/ScopedVisitor.ts +++ b/incremental/compiler-plugin/src/transformers/base/ScopedVisitor.ts @@ -13,9 +13,9 @@ * limitations under the License. */ -import * as ts from 'typescript'; +import * as ts from "typescript" import { AbstractVisitor } from "./AbstractVisitor" -import { FunctionKind, isAnyMemoKind, isMemoKind } from "./util" +import { FunctionKind } from "../../types" class FunctionScope { constructor( @@ -32,9 +32,11 @@ class FunctionScopes { push(kind: FunctionKind, data: T) { this.stack.push(new FunctionScope(kind, data)) } + pop() { this.stack.pop() } + peek(): FunctionScope|undefined { if (this.stack.length == 0) return undefined return this.stack[this.stack.length-1] @@ -42,36 +44,22 @@ class FunctionScopes { } export abstract class ScopedVisitor extends AbstractVisitor { - readonly functionScopes: FunctionScopes constructor( - public functionTable: Map, ctx: ts.TransformationContext ) { super(ctx) this.functionScopes = new FunctionScopes() } - declarationKind(node: ts.FunctionLikeDeclarationBase): FunctionKind { - const originalNode = ts.getOriginalNode(node) as ts.FunctionLikeDeclarationBase - return this.functionTable.get(originalNode) ?? FunctionKind.REGULAR - } + readonly functionScopes: FunctionScopes currentKind(): FunctionKind { return this.functionScopes.peek()?.kind ?? FunctionKind.REGULAR } - isAnyMemo(node: ts.FunctionLikeDeclarationBase): boolean { - return isAnyMemoKind(this.declarationKind(node)) - } - - isMemoOrComponent(node: ts.FunctionLikeDeclarationBase): boolean { - return isMemoKind(this.declarationKind(node)) - } - - withFunctionScope(kind: FunctionKind, data: T, body: ()=>R): R { - let result: R + withFunctionScope(kind: FunctionKind, data: T, body: () => R): R { this.functionScopes.push(kind, data) - result = body() + const result = body() this.functionScopes.pop() return result } diff --git a/incremental/compiler-plugin/src/diagnostics-visitor.ts b/incremental/compiler-plugin/src/transformers/diagnostics-visitor.ts similarity index 82% rename from incremental/compiler-plugin/src/diagnostics-visitor.ts rename to incremental/compiler-plugin/src/transformers/diagnostics-visitor.ts index 10d1f943c4f357e3be9fcf4a3801753fbf5ddd76..647540c06ed5e9efb5ba7dd63a42394cb9b9add4 100644 --- a/incremental/compiler-plugin/src/diagnostics-visitor.ts +++ b/incremental/compiler-plugin/src/transformers/diagnostics-visitor.ts @@ -14,18 +14,15 @@ */ import * as ts from 'typescript'; -import { MemoArgumentDetector } from './MemoArgumentDetector'; -import { ScopedVisitor } from './ScopedVisitor'; -import { Rewrite } from './transformation-context'; +import * as util from "../util" +import { ScopedVisitor } from './base/ScopedVisitor'; +import { Rewrite } from "../transformation-context" import { FunctionKind, - Tracer, - isFunctionOrMethod, - isAnyMemoKind, RuntimeNames, - getDeclarationsByNode, - isMemoKind -} from "./util" + Tracer, + MemoArgumentDetector, +} from "../types" enum MessageCode { CALLINNG_MEMO_FROM_NON_MEMO = 10001, @@ -43,10 +40,10 @@ export class DiagnosticsVisitor extends ScopedVisitor some_expression - // we need to check if some_expression has type void - if (!ts.isBlock(arrow.body) && // The Block case is handled in checkReturnType() - !this.canFindVoidType(arrow.body) - ) { - this.reportError( - MessageCode.MEMO_ARROW_MUST_HAVE_ITS_TYPE_EXPLICITLY_SPECIFIED, - `${RuntimeNames.ANNOTATION} arrow must have its type specified explicitly`, - arrow, - ) - } + // So we have an arrow function like + // (some args) => some_expression + // we need to check if some_expression has type void + if (!ts.isBlock(arrow.body) && + arrow.type === undefined && + !this.canFindVoidType(arrow.body) + ) { + this.reportError( + MessageCode.MEMO_ARROW_MUST_HAVE_ITS_TYPE_EXPLICITLY_SPECIFIED, + `${RuntimeNames.ANNOTATION} arrow must have its type specified explicitly`, + arrow, + ) } } - indent = 0 visitor(node: ts.Node): ts.Node { - if (isFunctionOrMethod(node)) { - const kind = this.declarationKind(node) - if (ts.isArrowFunction(node) && isMemoKind(kind)) { + if (ts.isFunctionDeclaration(node) || + ts.isMethodDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) + ) { + const kind = util.declarationKindByTable(this.rewrite.functionTable, node) + if (ts.isArrowFunction(node) && util.isMemoKind(kind)) { this.checkArrowReturnType(node) } return this.withFunctionScope(kind, node, () => { @@ -219,7 +217,7 @@ export class DiagnosticsVisitor extends ScopedVisitor, hasThis: boolean ): ts.Statement[] { - const trackable = originalParameters.filter(parameter => isTrackableParameter(this.sourceFile, parameter)) + const trackable = originalParameters.filter(parameter => util.isTrackableParameter(this.sourceFile, parameter)) const statements = trackable.map( (parameter, index) => this.parameterStateStatement(parameter, hasThis ? index + 1 : index ) @@ -151,7 +139,7 @@ function foo1(p1, p2): R { if (hasThis) { statements.push( - localStateStatement( + util.createLocalStateStatements( "this", ts.factory.createThis(), 0) ) } @@ -161,30 +149,30 @@ function foo1(p1, p2): R { createEarlyReturn(originalType: ts.TypeNode | undefined): ts.Statement { return ts.factory.createIfStatement( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_VALUE_OK), + util.createRuntimeIdentifier(RuntimeNames.SCOPE), + util.createRuntimeIdentifier(RuntimeNames.INTERNAL_VALUE_OK), ), - isVoidOrNotSpecified(originalType) + util.isVoidOrNotSpecified(originalType) ? ts.factory.createBlock([ ts.factory.createExpressionStatement( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_VALUE), + util.createRuntimeIdentifier(RuntimeNames.SCOPE), + util.createRuntimeIdentifier(RuntimeNames.INTERNAL_VALUE), )), createSyntheticReturn(undefined) ]) - : isThis(originalType) && isThisStable(this.typechecker, this.sourceFile, originalType) + : util.isThis(originalType) && util.isThisStable(this.typechecker, this.sourceFile, originalType) ? ts.factory.createBlock([ ts.factory.createExpressionStatement(ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_VALUE), + util.createRuntimeIdentifier(RuntimeNames.SCOPE), + util.createRuntimeIdentifier(RuntimeNames.INTERNAL_VALUE), )), createSyntheticReturn(ts.factory.createThis()) ]) : createSyntheticReturn( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_VALUE), + util.createRuntimeIdentifier(RuntimeNames.SCOPE), + util.createRuntimeIdentifier(RuntimeNames.INTERNAL_VALUE), ) ) ) @@ -199,16 +187,16 @@ function foo1(p1, p2): R { newParameters: ts.ParameterDeclaration[], newBody: ts.FunctionBody|undefined } { - const additionalParameters = hiddenParameters(this.rewrite) + const additionalParameters = util.hiddenParameters(this.rewrite) const newParameters = additionalParameters.concat(originalParameters) if (!originalBody) return {newParameters, newBody: undefined} const parameterStates = this.parameterStateStatements(originalParameters, hasThis) - const scope = createComputeScope( + const scope = util.createComputeScope( parameterStates.length, - idPlusKey(this.rewrite.positionalIdTracker), - isThis(originalType) && isThisStable(this.typechecker, this.sourceFile, originalType) + util.idPlusKey(this.rewrite.positionalIdTracker), + util.isThis(originalType) && util.isThisStable(this.typechecker, this.sourceFile, originalType) ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword) : originalType) const earlyReturn = this.createEarlyReturn(originalType) @@ -260,7 +248,7 @@ function foo1(p1, p2): R { originalMethod.parameters, originalMethod.body, this.getReturnType(originalMethod), - (!hasStaticModifier(originalMethod) && !isMethodOfStableClass(this.sourceFile, originalMethod)) + (!util.hasStaticModifier(originalMethod) && !util.isMethodOfStableClass(this.sourceFile, originalMethod)) ) const updatedMethod = ts.factory.updateMethodDeclaration( originalMethod, @@ -406,7 +394,7 @@ function foo1(p1, p2): R { originalMethod.name, originalMethod.questionToken, originalMethod.typeParameters, - hiddenParameters(this.rewrite).concat(originalMethod.parameters), + util.hiddenParameters(this.rewrite).concat(originalMethod.parameters), originalMethod.type, originalMethod.body ) @@ -421,7 +409,7 @@ function foo1(p1, p2): R { originalFunction.asteriskToken, originalFunction.name, originalFunction.typeParameters, - hiddenParameters(this.rewrite).concat(originalFunction.parameters), + util.hiddenParameters(this.rewrite).concat(originalFunction.parameters), originalFunction.type, originalFunction.body ) @@ -431,13 +419,13 @@ function foo1(p1, p2): R { transformCallWithName(node: ts.CallExpression, sourceFile: ts.SourceFile, name: string): ts.CallExpression { const args = node.arguments.slice() - const idExpression = idPlusKey(this.rewrite.positionalIdTracker) + const idExpression = util.idPlusKey(this.rewrite.positionalIdTracker) args.unshift( idExpression ) args.unshift( - runtimeIdentifier(RuntimeNames.CONTEXT), + util.createRuntimeIdentifier(RuntimeNames.CONTEXT), ) const update = ts.factory.updateCallExpression( @@ -472,7 +460,7 @@ function foo1(p1, p2): R { } wrapObjectLiteralInCompute(node: ts.ObjectLiteralExpression | ts.ArrowFunction | ts.AsExpression): ts.Expression { - return wrapInCompute(node, idPlusKey(this.rewrite.positionalIdTracker)) + return util.wrapInCompute(node, util.idPlusKey(this.rewrite.positionalIdTracker)) } transformTransformedType(node: ts.TypeReferenceNode): ts.TypeReferenceNode { @@ -487,7 +475,7 @@ function foo1(p1, p2): R { if (ts.isParenthesizedExpression(node.expression)) { node = ts.factory.updateCallExpression( node, - skipParenthesizedExpression(node.expression), + util.skipParenthesizedExpression(node.expression), undefined, node.arguments ) @@ -516,6 +504,7 @@ function foo1(p1, p2): R { return transformed ?? node } + visitor(node: ts.SourceFile): ts.SourceFile visitor(beforeChildren: ts.Node): ts.Node { // Note that visitEachChild can create a new node if its children have changed. const node = this.visitEachChild(beforeChildren) @@ -555,7 +544,7 @@ function foo1(p1, p2): R { const originalNode = ts.getOriginalNode(node, ts.isArrowFunction) const originalParent = originalNode.parent if (originalParent && ts.isCallExpression(originalParent)) { - if (isMemoKind(this.rewrite.callTable.get(originalParent))) + if (util.isMemoKind(this.rewrite.callTable.get(originalParent))) transformed = this.wrapObjectLiteralInCompute(node) } } @@ -607,18 +596,19 @@ function foo1(p1, p2): R { const originalNode = ts.getOriginalNode(node, ts.isObjectLiteralExpression) const originalParent = originalNode.parent if (originalParent && ts.isCallExpression(originalParent)) { - if (isMemoKind(this.rewrite.callTable.get(originalParent))) + if (util.isMemoKind(this.rewrite.callTable.get(originalParent))) { transformed = this.wrapObjectLiteralInCompute(node) + } } } else if (ts.isAsExpression(node)) { const originalNode = ts.getOriginalNode(node, ts.isAsExpression) const originalParent = originalNode.parent if (originalParent && ts.isCallExpression(originalParent)) { - if (isMemoKind(this.rewrite.callTable.get(originalParent))) + if (util.isMemoKind(this.rewrite.callTable.get(originalParent))) { transformed = this.wrapObjectLiteralInCompute(node) + } } - } - else if (ts.isTypeReferenceNode(node)) { + } else if (ts.isTypeReferenceNode(node)) { if (ts.isIdentifier(node.typeName) && ts.idText(node.typeName) == RuntimeNames.TRANSFORMED_TYPE) { transformed = this.transformTransformedType(node) } @@ -630,7 +620,7 @@ function foo1(p1, p2): R { function createSyntheticReturn(node: ts.Expression | undefined): ts.ReturnStatement { return ts.factory.createReturnStatement( ts.factory.createCallExpression( - runtimeIdentifier(RuntimeNames.SYNTHETIC_RETURN_MARK), + util.createRuntimeIdentifier(RuntimeNames.SYNTHETIC_RETURN_MARK), undefined, node ? [node] : undefined ) diff --git a/incremental/compiler-plugin/src/transformers/index.ts b/incremental/compiler-plugin/src/transformers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b6bfe7186f115efbf4fa51f0bf4927d007352cc --- /dev/null +++ b/incremental/compiler-plugin/src/transformers/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023 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. + */ + +export { AbstractVisitor } from './base/AbstractVisitor'; + +export { AnalysisVisitor } from './analysis-visitor'; +export { DiagnosticsVisitor } from './diagnostics-visitor'; +export { FunctionTransformer } from "./function-transformer" +export { ParameterTransformer } from "./parameter-transformer" +export { VariableTypeTransformer } from './variable-type-transformer'; +export { DumpVisitor } from './dump-visitor'; +export { ThisTransformer } from './this-transformer'; +export { ReturnTransformer } from './return-transformer'; diff --git a/incremental/compiler-plugin/src/MemoArgumentDetector.ts b/incremental/compiler-plugin/src/transformers/memo-argument-detector.ts similarity index 49% rename from incremental/compiler-plugin/src/MemoArgumentDetector.ts rename to incremental/compiler-plugin/src/transformers/memo-argument-detector.ts index 8f35a302116c76295ce1f65122ac4fda4dff4967..72f68506c10f642b2f48ef126864219d208b161a 100644 --- a/incremental/compiler-plugin/src/MemoArgumentDetector.ts +++ b/incremental/compiler-plugin/src/transformers/memo-argument-detector.ts @@ -13,38 +13,47 @@ * limitations under the License. */ -import * as ts from 'typescript' -import { Rewrite } from './transformation-context'; -import { AbstractVisitor } from './AbstractVisitor' -import { FunctionKind, isAnyMemoKind } from './util'; +import * as ts from 'typescript'; +import * as util from "../util" +import { ScopedVisitor } from './base/ScopedVisitor'; +import { AbstractVisitor } from "./base/AbstractVisitor" +import { ImportExport } from '../import-export'; +import { Rewrite } from "../transformation-context" +import { + FunctionTable, + FunctionKind, + RuntimeNames, + PositionalIdTracker, + Tracer, +} from "../types" export class MemoArgumentDetector extends AbstractVisitor { constructor( public typechecker: ts.TypeChecker, public rewrite: Rewrite, - public memoArgument: ts.Node, + // public memoArgument: ts.Node, ctx: ts.TransformationContext, ) { super(ctx) } - hasMemoArgument : boolean = false - - visitor(node: ts.Node): ts.Node { + // public hasMemoArgument : boolean = false + public memoArgument: ts.Node | undefined = undefined - if(ts.isCallExpression(node)){ - const callKind = this.rewrite.callTable.get(node) ?? FunctionKind.REGULAR - if(isAnyMemoKind(callKind)) { - this.hasMemoArgument = true + visitor(node: ts.Node): ts.Node { + if (ts.isCallExpression(node) && util.isAnyMemo(this.rewrite, node)){ + // const callKind = this.rewrite.callTable.get(node) ?? FunctionKind.REGULAR + // if(util.isAnyMemoKind(callKind)) { + // this.hasMemoArgument = true this.memoArgument = node - } + // } } return this.visitEachChild(node) } - usingMemoFunction(node: ts.Expression): boolean { - this.hasMemoArgument = false - this.visitor(node) - return this.hasMemoArgument - } -} \ No newline at end of file + // usingMemoFunction(node: ts.Expression): boolean { + // // this.hasMemoArgument = false + // this.visitor(node) + // // return this.hasMemoArgument + // } +} diff --git a/incremental/compiler-plugin/src/parameter-transformer.ts b/incremental/compiler-plugin/src/transformers/parameter-transformer.ts similarity index 67% rename from incremental/compiler-plugin/src/parameter-transformer.ts rename to incremental/compiler-plugin/src/transformers/parameter-transformer.ts index 6bc8bf28ce167a936a92de81a4cea9dda77c5f26..c4dc47e7bfe1e7e56d41da6612721a68c91635cc 100644 --- a/incremental/compiler-plugin/src/parameter-transformer.ts +++ b/incremental/compiler-plugin/src/transformers/parameter-transformer.ts @@ -13,58 +13,57 @@ * limitations under the License. */ -import * as ts from 'typescript'; -import { ScopedVisitor } from './ScopedVisitor'; +import * as ts from "typescript" +import * as util from "../util" +import { AbstractVisitor } from "./base/AbstractVisitor" +import { ScopedVisitor } from "./base/ScopedVisitor" +import { ImportExport } from '../import-export'; +import { Rewrite } from "../transformation-context" import { + FunctionTable, FunctionKind, - Tracer, - isTrackableParameter, - parameterStateName as localStateName, - PositionalIdTracker, - runtimeIdentifier, RuntimeNames, - isFunctionOrMethod, - getDeclarationsByNode, - isMemoKind, - asString, -} from "./util" + PositionalIdTracker, + Tracer, +} from "../types" export class ParameterTransformer extends ScopedVisitor { constructor( public tracer: Tracer, public typechecker: ts.TypeChecker, public sourceFile: ts.SourceFile, - public positionalIdTracker: PositionalIdTracker, - public functionTable: Map, + public rewrite: Rewrite, + // public positionalIdTracker: PositionalIdTracker, + // public functionTable: Map, ctx: ts.TransformationContext ) { - super(functionTable, ctx) + super(ctx) } transformParameterUse(node: ts.Identifier): ts.PropertyAccessExpression { const originalName = ts.idText(node) - const newName = localStateName(originalName) + const newName = util.parameterStateName(originalName) return ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier(newName), - runtimeIdentifier(RuntimeNames.VALUE) + util.createRuntimeIdentifier(RuntimeNames.VALUE) ) } isTrackedParameter(node: ts.Identifier): boolean { - const declarations = getDeclarationsByNode(this.typechecker, node) + const declarations = util.getDeclarationsByNode(this.typechecker, node) const declaration = declarations[0] if (!declaration) return false if (!ts.isParameter(declaration)) return false - if (!isTrackableParameter(this.sourceFile, declaration)) return false + if (!util.isTrackableParameter(this.sourceFile, declaration)) return false const func = declaration.parent - if (!isFunctionOrMethod(func)) return false + if (!util.isFunctionLikeDeclaration(func)) return false const originalFunc = ts.getOriginalNode(func) as ts.FunctionLikeDeclarationBase - const kind = this.functionTable.get(originalFunc) + const kind = this.rewrite.functionTable.get(originalFunc) - return isMemoKind(kind) + return util.isMemoKind(kind) } visitor(node: ts.Node): ts.Node { @@ -81,8 +80,8 @@ export class ParameterTransformer extends ScopedVisitor { undefined ) } - if (isFunctionOrMethod(node)) { - const kind = this.declarationKind(node) + if (util.isFunctionLikeDeclaration(node)) { + const kind = util.declarationKindByTable(this.rewrite.functionTable, node) return this.withFunctionScope(kind, undefined, ()=>this.visitEachChild(node)) } if (ts.isIdentifier(node) && this.isTrackedParameter(node)) { diff --git a/incremental/compiler-plugin/src/return-transformer.ts b/incremental/compiler-plugin/src/transformers/return-transformer.ts similarity index 71% rename from incremental/compiler-plugin/src/return-transformer.ts rename to incremental/compiler-plugin/src/transformers/return-transformer.ts index 9a3e9c630d7729d09e0903d6dbefa495c1a2c7f9..adf864f376a9d751e61edfc8d5202093e0b22977 100644 --- a/incremental/compiler-plugin/src/return-transformer.ts +++ b/incremental/compiler-plugin/src/transformers/return-transformer.ts @@ -13,26 +13,37 @@ * limitations under the License. */ -import * as ts from 'typescript'; -import { ScopedVisitor } from "./ScopedVisitor" -import { FunctionKind, isFunctionOrMethod, isMemoKind, isThis, isThisStable, isVoidOrNotSpecified, runtimeIdentifier, RuntimeNames } from './util' +import * as ts from "typescript" +import * as util from "../util" +import { AbstractVisitor } from "./base/AbstractVisitor" +import { ScopedVisitor } from "./base/ScopedVisitor" +import { ImportExport } from '../import-export'; +import { Rewrite } from "../transformation-context" +import { + FunctionTable, + FunctionKind, + RuntimeNames, + PositionalIdTracker, + Tracer, +} from "../types" export class ReturnTransformer extends ScopedVisitor { constructor( + public tracer: Tracer, public typechecker: ts.TypeChecker, public sourceFile: ts.SourceFile, - public functionTable: Map, + public rewrite: Rewrite, ctx: ts.TransformationContext ) { - super(functionTable, ctx) + super(ctx) } visitor(node: ts.Node): ts.Node { - if (isFunctionOrMethod(node)) { - const kind = this.declarationKind(node) + if (util.isFunctionLikeDeclaration(node)) { + const kind = util.declarationKindByTable(this.rewrite.functionTable, node) return this.withFunctionScope(kind, node.type, () => this.visitEachChild(node)) } - if (ts.isReturnStatement(node) && isMemoKind(this.currentKind())) { + if (ts.isReturnStatement(node) && util.isMemoKind(this.currentKind())) { // visit children of the return expression before attaching it to the AST const expression = node.expression ? this.visitEachChild(node.expression) : undefined if (expression && ts.isCallExpression(expression)) { @@ -42,13 +53,13 @@ export class ReturnTransformer extends ScopedVisitor { } } const functionReturnType = this.functionScopes.peek()?.data - if (isVoidOrNotSpecified(functionReturnType)) + if (util.isVoidOrNotSpecified(functionReturnType)) return ts.factory.createBlock([ ts.factory.createExpressionStatement(createScopeRecacheExpression(expression)), ts.factory.updateReturnStatement(node, undefined) ]) - if (isThis(functionReturnType) && isThisStable(this.typechecker, this.sourceFile, functionReturnType)) + if (util.isThis(functionReturnType) && util.isThisStable(this.typechecker, this.sourceFile, functionReturnType)) return ts.factory.createBlock([ ts.factory.createExpressionStatement(createScopeRecacheExpression()), ts.factory.updateReturnStatement(node, ts.factory.createThis()) @@ -63,8 +74,8 @@ export class ReturnTransformer extends ScopedVisitor { function createScopeRecacheExpression(expression?: ts.Expression): ts.Expression { return ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_VALUE_NEW) + util.createRuntimeIdentifier(RuntimeNames.SCOPE), + util.createRuntimeIdentifier(RuntimeNames.INTERNAL_VALUE_NEW) ), undefined, expression ? [expression] : undefined diff --git a/incremental/compiler-plugin/src/this-transformer.ts b/incremental/compiler-plugin/src/transformers/this-transformer.ts similarity index 77% rename from incremental/compiler-plugin/src/this-transformer.ts rename to incremental/compiler-plugin/src/transformers/this-transformer.ts index b2d7f92632b1b65940bdd3fe90070f6f14d6b5b6..3fb5a076384e022e877852fbbb39404965c17d3e 100644 --- a/incremental/compiler-plugin/src/this-transformer.ts +++ b/incremental/compiler-plugin/src/transformers/this-transformer.ts @@ -13,39 +13,36 @@ * limitations under the License. */ -import * as ts from 'typescript'; -import { ScopedVisitor } from './ScopedVisitor'; +import * as ts from "typescript" +import * as util from "../util" +import { AbstractVisitor } from "./base/AbstractVisitor" +import { ScopedVisitor } from "./base/ScopedVisitor" +import { ImportExport } from '../import-export'; +import { Rewrite } from "../transformation-context" import { + FunctionTable, FunctionKind, - Tracer, - parameterStateName as localStateName, - PositionalIdTracker, - runtimeIdentifier, RuntimeNames, - isMemoKind, - getSymbolByNode, - getDeclarationsByNode, - isStableClass, - isThisStable, -} from "./util" + PositionalIdTracker, + Tracer, +} from "../types" export class ThisTransformer extends ScopedVisitor { constructor( public tracer: Tracer, public typechecker: ts.TypeChecker, public sourceFile: ts.SourceFile, - public positionalIdTracker: PositionalIdTracker, - public functionTable: Map, + public rewrite: Rewrite, ctx: ts.TransformationContext ) { - super(functionTable, ctx) + super(ctx) } transformThisUse(node: ts.ThisExpression): ts.PropertyAccessExpression { - const newName = localStateName("this") + const newName = util.parameterStateName("this") return ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier(newName), - runtimeIdentifier(RuntimeNames.VALUE) + util.createRuntimeIdentifier(RuntimeNames.VALUE) ) } @@ -75,15 +72,15 @@ export class ThisTransformer extends ScopedVisitor { return this.visitEachChild(node) }) } else if (ts.isMethodDeclaration(node)) { - const kind = this.declarationKind(node) - const isInMemoMethod = this.isInMemoMethod() || isMemoKind(kind) + const kind = util.declarationKindByTable(this.rewrite.functionTable, node) + const isInMemoMethod = this.isInMemoMethod() || util.isMemoKind(kind) const transformedWithChildren = this.withFunctionScope(kind, isInMemoMethod, ()=>{ return this.visitEachChild(node) }) return transformedWithChildren } else if (this.isThisExpression(node)) { - if (!isThisStable(this.typechecker, this.sourceFile, node) && this.isInMemoMethod()) { + if (!util.isThisStable(this.typechecker, this.sourceFile, node) && this.isInMemoMethod()) { const transformed = node.parent ? this.transformThisUse(node) : node diff --git a/incremental/compiler-plugin/src/variable-type-transformer.ts b/incremental/compiler-plugin/src/transformers/variable-type-transformer.ts similarity index 95% rename from incremental/compiler-plugin/src/variable-type-transformer.ts rename to incremental/compiler-plugin/src/transformers/variable-type-transformer.ts index 64e8369544f687215ccc068efe26733b4bd47b69..6713b75ab00a2f69e7ca6734acdb6b3ffc5e1e7f 100644 --- a/incremental/compiler-plugin/src/variable-type-transformer.ts +++ b/incremental/compiler-plugin/src/transformers/variable-type-transformer.ts @@ -14,9 +14,10 @@ */ import * as ts from "typescript" -import { AbstractVisitor } from "./AbstractVisitor"; -import { Rewrite } from "./transformation-context"; -import { createContextTypeImport, FunctionKind, hiddenParameters, skipParenthesizedType, Tracer } from "./util"; +import { AbstractVisitor } from "./base/AbstractVisitor" +import { Rewrite } from "../transformation-context" +import { createContextTypeImport, hiddenParameters, skipParenthesizedType } from "../util"; +import { FunctionKind, Tracer } from "../types"; export class VariableTypeTransformer extends AbstractVisitor { constructor( @@ -159,8 +160,8 @@ export class VariableTypeTransformer extends AbstractVisitor { ) } - visitor(beforeChildren: ts.Node): ts.Node { - const node = this.visitEachChild(beforeChildren) + visitor(node: ts.Node): ts.Node { + node = this.visitEachChild(node) let transformed: ts.Node|undefined = undefined if (ts.isParameter(node)) { const kind = this.rewrite.variableTable.get(ts.getOriginalNode(node) as ts.ParameterDeclaration) @@ -188,4 +189,4 @@ export class VariableTypeTransformer extends AbstractVisitor { return transformed ?? node } -} \ No newline at end of file +} diff --git a/incremental/compiler-plugin/src/types.ts b/incremental/compiler-plugin/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbbc3d9e62ff4b4a8184b7530d9b6053caba421e --- /dev/null +++ b/incremental/compiler-plugin/src/types.ts @@ -0,0 +1,81 @@ +/* + * 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'; + +export interface TransformerOptions { + // Emit transformed functions to the console. + trace?: boolean, + // Store the transformed functions to this directory. + keepTransformed?: string, + // Use human readable call site IDs without directory paths. + stableForTest?: boolean, + // Import context and id types from alternative source + contextImport?: string, + // Dump sources with resolved memo annotations to unmemoized directory + only_unmemoize?: boolean, + // Replace extension for output TS files + extension?: string, + unmemoizeDir?: string, +} + +export enum FunctionKind { + REGULAR, + MEMO, + MEMO_INTRINSIC, +} + +export enum RuntimeNames { + COMPUTE = "compute", + CONTEXT = "__memo_context", + ID = "__memo_id", + SCOPE = "__memo_scope", + INTERNAL_PARAMETER_STATE = "param", + INTERNAL_VALUE = "cached", + INTERNAL_VALUE_NEW = "recache", + INTERNAL_SCOPE = "scope", + INTERNAL_VALUE_OK = "unchanged", + CONTENT = "content", + VALUE = "value", + __CONTEXT = "__context", + __ID = "__id", + __KEY = "__key", + __STATE = "__state", + CONTEXT_TYPE = "__memo_context_type", + ID_TYPE = "__memo_id_type", + TRANSFORMED_TYPE = "__memo_transformed", + SYNTHETIC_RETURN_MARK = "__synthetic_return_value", + CONTEXT_TYPE_DEFAULT_IMPORT = "@koalaui/runtime", + ANNOTATION = "@memo", + ANNOTATION_INTRINSIC = "@memo:intrinsic", + ANNOTATION_ENTRY = "@memo:entry", + ANNOTATION_SKIP = "@skip:memo", // skip binding to parameter changes + ANNOTATION_STABLE = "@memo:stable", // assume this should not be tracked +} + +// TODO: SignatureDeclarationBase -> CallSignatureDeclaration +export type FunctionTable = Map +export type CallTable = Map +export type EntryTable = Set +export type VariableTable = Map + +import { Tracer } from './tracer' +export { Tracer } + +import { PositionalIdTracker } from './idTracker' +export { PositionalIdTracker } + +import { MemoArgumentDetector } from './transformers/memo-argument-detector' +export { MemoArgumentDetector} diff --git a/incremental/compiler-plugin/src/util.ts b/incremental/compiler-plugin/src/util.ts index 7f8fe0fd1322e77fcf4aa69e41e1c0444e648ba8..3e1d9da7be3d3a5215a9799b08b540ed70e30060 100644 --- a/incremental/compiler-plugin/src/util.ts +++ b/incremental/compiler-plugin/src/util.ts @@ -13,92 +13,70 @@ * limitations under the License. */ -import * as ts from 'typescript'; -import * as fs from "fs" -import { UniqueId } from "@koalaui/common" -import { Rewrite } from './transformation-context'; - - -export enum FunctionKind { - REGULAR, - MEMO, - MEMO_INTRINSIC, -} +import * as ts from "typescript" +import { Rewrite } from "./transformation-context" +import { + FunctionTable, + FunctionKind, + RuntimeNames, + PositionalIdTracker, + CallTable, + VariableTable, +} from "./types" -export type FunctionTable = Map -export type CallTable = Map -export type EntryTable = Set -export type VariableTable = Map +import { UniqueId } from "@koalaui/common" +export { UniqueId } export function isNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration { return ("name" in node ) } -export function asString(node: ts.Node|undefined): string { - if (node === undefined) return "undefined node" - if (ts.isIdentifier(node)) return ts.idText(node) +export function nodeToString(node?: ts.Node): string { + if (node === undefined) { + return "undefined node" + } + if (ts.isIdentifier(node)) { + return ts.idText(node) + } if (isNamedDeclaration(node)) { - if (node.name === undefined) { - return `${ts.SyntaxKind[node.kind]}(undefined name)` - } else { - - return `${ts.SyntaxKind[node.kind]}(${asString(node.name)})` - } - } else { - return `${ts.SyntaxKind[node.kind]}` + return `${ts.SyntaxKind[node.kind]}(${nodeToString(node.name)})` } + return `${ts.SyntaxKind[node.kind]}` } -export function isFunctionOrMethod(node: ts.Node): node is ts.FunctionLikeDeclaration { +export function isFunctionLikeDeclaration(node: ts.Node): node is ts.FunctionLikeDeclaration { return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) || + ts.isConstructorDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) } -export enum RuntimeNames { - COMPUTE = "compute", - CONTEXT = "__memo_context", - ID = "__memo_id", - SCOPE = "__memo_scope", - INTERNAL_PARAMETER_STATE = "param", - INTERNAL_VALUE = "cached", - INTERNAL_VALUE_NEW = "recache", - INTERNAL_SCOPE = "scope", - INTERNAL_VALUE_OK = "unchanged", - CONTENT = "content", - VALUE = "value", - __CONTEXT = "__context", - __ID = "__id", - __KEY = "__key", - __STATE = "__state", - CONTEXT_TYPE = "__memo_context_type", - ID_TYPE = "__memo_id_type", - TRANSFORMED_TYPE = "__memo_transformed", - SYNTHETIC_RETURN_MARK = "__synthetic_return_value", - CONTEXT_TYPE_DEFAULT_IMPORT = "@koalaui/runtime", - ANNOTATION = "@memo", - ANNOTATION_INTRINSIC = "@memo:intrinsic", - ANNOTATION_ENTRY = "@memo:entry", - ANNOTATION_SKIP = "@skip:memo", // skip binding to parameter changes - ANNOTATION_STABLE = "@memo:stable", // assume this should not be tracked -} - -export function runtimeIdentifier(name: RuntimeNames): ts.Identifier { +export function declarationKindByTable(functionTable: FunctionTable, node: ts.FunctionLikeDeclarationBase): FunctionKind { + const originalNode = ts.getOriginalNode(node) + if (!isFunctionLikeDeclaration(originalNode)) { + throw new Error('node supposed to be FuncionLikeDeclaration') + } + return functionTable.get(originalNode) ?? FunctionKind.REGULAR +} + +export function createRuntimeIdentifier(name: RuntimeNames): ts.Identifier { return ts.factory.createIdentifier(name) } -export function componentTypeName(functionName: ts.PropertyName|undefined): ts.Identifier { - if (!functionName || !ts.isIdentifier(functionName) && !ts.isPrivateIdentifier(functionName)) { - throw new Error("Expected an identifier: " + asString(functionName)) +export function createComponentTypeName(functionName: ts.PropertyName | undefined): ts.Identifier { + if ((functionName === undefined) || (!ts.isIdentifier(functionName) && !ts.isPrivateIdentifier(functionName))) { + throw new Error("Expected an identifier: " + nodeToString(functionName)) } return ts.factory.createIdentifier(ts.idText(functionName)+"Node") } export function isSpecialContentParameter(param: ts.ParameterDeclaration): boolean { - if (!param.type) return false - if (!param.name) return false return ( + param.type !== undefined && + param.name !== undefined && ts.isFunctionTypeNode(param.type) && ts.isIdentifier(param.name) && ts.idText(param.name) == RuntimeNames.CONTENT @@ -106,11 +84,19 @@ export function isSpecialContentParameter(param: ts.ParameterDeclaration): boole } export function isSpecialContextParameter(parameter: ts.ParameterDeclaration): boolean { - return !!parameter.name && ts.isIdentifier(parameter.name) && ts.idText(parameter.name) == RuntimeNames.CONTEXT + return ( + parameter.name !== undefined && + ts.isIdentifier(parameter.name) && + ts.idText(parameter.name) === RuntimeNames.CONTEXT + ) } export function isSpecialIdParameter(parameter: ts.ParameterDeclaration): boolean { - return !!parameter.name && ts.isIdentifier(parameter.name) && ts.idText(parameter.name) == RuntimeNames.ID + return ( + parameter.name !== undefined && + ts.isIdentifier(parameter.name) && + ts.idText(parameter.name) == RuntimeNames.ID + ) } export function isSkippedParameter(sourceFile: ts.SourceFile, parameter: ts.ParameterDeclaration): boolean { @@ -134,6 +120,18 @@ export function isMemoEntry(sourceFile: ts.SourceFile, func: ts.FunctionDeclarat return hasMemoEntry(sourceFile, func) } +export function isInMemoEntry(node: ts.Node): boolean { + const enclosingFunction = findFunctionDeclaration(node) + if (enclosingFunction === undefined) { + return false + } + const sourceFile = findSourceFile(node) + if (sourceFile === undefined) { + return false + } + return isMemoEntry(sourceFile, enclosingFunction) +} + export function isTrackableParameter(sourceFile: ts.SourceFile, parameter: ts.ParameterDeclaration): boolean { return !isSpecialContentParameter(parameter) && !isSpecialContextParameter(parameter) @@ -220,53 +218,11 @@ export function isThisStable(typechecker: ts.TypeChecker, sourceFile: ts.SourceF return isStableClass(sourceFile, firstDeclaration) } -export class PositionalIdTracker { - // Global for the whole program. - static callCount: number = 0 - - // Set `stable` to true if you want to have more predictable values. - // For example for tests. - // Don't use it in production! - constructor(public sourceFile: ts.SourceFile, public stableForTests: boolean = false) { - if (stableForTests) PositionalIdTracker.callCount = 0 - } - - sha1Id(callName: string, fileName: string): string { - const uniqId = new UniqueId() - uniqId.addString("memo call uniqid") - uniqId.addString(fileName) - uniqId.addString(callName) - uniqId.addI32(PositionalIdTracker.callCount++) - return uniqId.compute().substring(0,10) - } - - stringId(callName: string, fileName: string): string { - return `${PositionalIdTracker.callCount++}_${callName}_id_DIRNAME/${fileName}` - } - - id(callName: string): ts.Expression { - - const fileName = this.stableForTests ? - baseName(this.sourceFile.fileName) : - this.sourceFile.fileName - - const positionId = (this.stableForTests) ? - this.stringId(callName, fileName) : - this.sha1Id(callName, fileName) - - - return this.stableForTests ? - ts.factory.createStringLiteral(positionId) : - ts.factory.createNumericLiteral("0x"+positionId) - - } -} - export function wrapInCompute(node: ts.ConciseBody, id: ts.Expression): ts.Expression { return ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.CONTEXT), - runtimeIdentifier(RuntimeNames.COMPUTE) + createRuntimeIdentifier(RuntimeNames.CONTEXT), + createRuntimeIdentifier(RuntimeNames.COMPUTE) ), /*typeArguments*/ undefined, [ @@ -287,8 +243,8 @@ export function createComputeScope(pseudoStateCount: number, id: ts.Expression, return constVariable(RuntimeNames.SCOPE, ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.CONTEXT), - runtimeIdentifier(RuntimeNames.INTERNAL_SCOPE) + createRuntimeIdentifier(RuntimeNames.CONTEXT), + createRuntimeIdentifier(RuntimeNames.INTERNAL_SCOPE) ), typeArgument ? [typeArgument] : undefined, pseudoStateCount > 0 @@ -315,7 +271,7 @@ export function constVariable(name: string, initializer: ts.Expression): ts.Vari export function idPlusKey(positionalIdTracker: PositionalIdTracker): ts.Expression { return ts.factory.createBinaryExpression( - runtimeIdentifier(RuntimeNames.ID), + createRuntimeIdentifier(RuntimeNames.ID), ts.factory.createToken(ts.SyntaxKind.PlusToken), ts.factory.createAsExpression( positionalIdTracker.id(RuntimeNames.__KEY), @@ -324,56 +280,59 @@ export function idPlusKey(positionalIdTracker: PositionalIdTracker): ts.Expressi ) } -export function isAnyMemoKind(kind: FunctionKind|undefined): boolean { - switch(kind) { - case FunctionKind.MEMO: - case FunctionKind.MEMO_INTRINSIC: - return true +export function isAnyMemo( + rewrite: Rewrite, + node: ts.FunctionLikeDeclarationBase | ts.CallExpression | ts.VariableLikeDeclaration +): boolean { + const originalNode = ts.getOriginalNode(node) + if (isFunctionLikeDeclaration(originalNode)) { + return (rewrite.functionTable.get(originalNode) ?? FunctionKind.REGULAR) !== FunctionKind.REGULAR + } + if (ts.isCallExpression(originalNode)) { + return (rewrite.callTable.get(originalNode) ?? FunctionKind.REGULAR) !== FunctionKind.REGULAR + } + if (ts.isVariableDeclaration(originalNode)) { + return (rewrite.variableTable.get(originalNode) ?? FunctionKind.REGULAR) !== FunctionKind.REGULAR } return false } -export function isMemoKind(kind: FunctionKind|undefined): boolean { - switch(kind) { - case FunctionKind.MEMO: - return true - } - return false +export function isMemoKind(kind: FunctionKind | undefined): boolean { + return kind === FunctionKind.MEMO } export function createContextType(): ts.TypeNode { return ts.factory.createTypeReferenceNode( - runtimeIdentifier(RuntimeNames.CONTEXT_TYPE), + createRuntimeIdentifier(RuntimeNames.CONTEXT_TYPE), undefined ) } export function createIdType(): ts.TypeNode { return ts.factory.createTypeReferenceNode( - runtimeIdentifier(RuntimeNames.ID_TYPE), + createRuntimeIdentifier(RuntimeNames.ID_TYPE), undefined ) } export function createHiddenParameters(): ts.ParameterDeclaration[] { const context = ts.factory.createParameterDeclaration( - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, + undefined, + undefined, RuntimeNames.CONTEXT, - /* questionToken */ undefined, + undefined, createContextType(), - /* initializer */ undefined + undefined ) const id = ts.factory.createParameterDeclaration( - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, + undefined, + undefined, RuntimeNames.ID, - /* questionToken */ undefined, + undefined, createIdType(), - /* initializer */ undefined + undefined ) - return [context, id] } @@ -381,8 +340,8 @@ export function createContextTypeImport(importSource: string): ts.Statement { return ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause( - false, - undefined, + false, + undefined, ts.factory.createNamedImports( [ ts.factory.createImportSpecifier( @@ -400,7 +359,7 @@ export function createContextTypeImport(importSource: string): ts.Statement { ), ts.factory.createStringLiteral(importSource), undefined - ) + ) } export function setNeedTypeImports(rewrite: Rewrite) { @@ -428,7 +387,7 @@ export function skipParenthesizedExpression(expr: ts.ParenthesizedExpression): t return current.expression } -export function localStateStatement( +export function createLocalStateStatements( stateName: string, referencedEntity: ts.Expression, parameterIndex: number @@ -443,8 +402,8 @@ export function localStateStatement( undefined, ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( - runtimeIdentifier(RuntimeNames.SCOPE), - runtimeIdentifier(RuntimeNames.INTERNAL_PARAMETER_STATE) + createRuntimeIdentifier(RuntimeNames.SCOPE), + createRuntimeIdentifier(RuntimeNames.INTERNAL_PARAMETER_STATE) ), undefined, [ts.factory.createNumericLiteral(parameterIndex), referencedEntity] @@ -466,66 +425,6 @@ export function error(message: any): any { return undefined } -export interface TransformerOptions { - // Emit transformed functions to the console. - trace?: boolean, - // Store the transformed functions to this directory. - keepTransformed?: string, - // Use human readable call site IDs without directory paths. - stableForTest?: boolean, - // Import context and id types from alternative source - contextImport?: string, - // Dump sources with resolved memo annotations to unmemoized directory - only_unmemoize?: boolean, - // Target dir for unmemoization - unmemoizeDir?: string, - // Replace extension for output TS files - extension?: string, -} - -function baseName(path: string): string { +export function baseName(path: string): string { return path.replace(/^.*\/(.*)$/, "$1") } - -export class Tracer { - constructor (public options: TransformerOptions, public printer: ts.Printer) { - } - - trace(msg: any) { - if (!this.options.trace) return - console.log(msg) - } - - writeTextToFile(text: string, file: string) { - fs.writeFileSync(file, text, 'utf8') - this.trace("DUMP TO: " + file) - } - - createDirs(dirs: string) { - fs.mkdirSync(dirs, { recursive: true }); - } - - dumpFileName(sourceFile: ts.SourceFile, transformed: ts.FunctionLikeDeclarationBase): string|undefined { - if (!this.options.keepTransformed) return undefined - - const outDir = (this.options.keepTransformed[0] == "/") ? - this.options.keepTransformed : - `${__dirname}/${this.options.keepTransformed}` - - this.createDirs(outDir) - - const sourceBaseName = baseName(sourceFile.fileName) - if (!transformed.name) return - if (!ts.isIdentifier(transformed.name)) return - const fileName = `${ts.idText(transformed.name)}_${sourceBaseName}` - return `${outDir}/${fileName}_dump` - } - - keepTransformedFunction(transformed: ts.FunctionLikeDeclarationBase, sourceFile: ts.SourceFile) { - const fileName = this.dumpFileName(sourceFile, transformed) - if (!fileName) return - - const content = this.printer.printNode(ts.EmitHint.Unspecified, transformed, sourceFile) - this.writeTextToFile(content+"\n", fileName) - } -} diff --git a/incremental/compiler-plugin/test/unmemoized/context.test.ts b/incremental/compiler-plugin/test/unmemoized/context.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..32e94d288590251a07ace32b57ba4c5c101a813c --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/context.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022-2023 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 { log } from "./util.test"; +export class Node { +} +export class MutableState { + constructor(public value: T) { + this.value = value; + } +} +export function __context(): Context { + throw "__context() should not be called"; +} +export function __id(): Key { + throw "__id() should not be called"; +} +export class Scope { + param(index: number, value: T): MutableState { + return new MutableState(value); + } + recache(value: T) { return value; } + get cached() { + throw new Error(""); + } + unchanged = false; +} +export class Context { + scope(id: Key, paramCount: number = 0): Scope { return new Scope(); } + compute(id: Key, lambda: () => T): T { + return lambda(); + } +} +export type Key = string; +export type __memo_context_type = Context; +export type __memo_id_type = Key; + diff --git a/incremental/compiler-plugin/test/unmemoized/examples/localexport.ts b/incremental/compiler-plugin/test/unmemoized/examples/localexport.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0eb61284b180283c4095d9095303a523fcf84ae --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/examples/localexport.ts @@ -0,0 +1,26 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +/* + * Copyright (c) 2022-2023 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 { __id } from "../context.test"; +export { somename as localexported }; +/** @memo */ +export function somename(__memo_context: __memo_context_type, __memo_id: __memo_id_type, a: number): number { + const __memo_scope = __memo_context.scope(__memo_id + ("0___key_id_DIRNAME/localexport.ts" as __memo_id_type), 1); + const __memo_parameter_a = __memo_scope.param(0, a); + if (__memo_scope.unchanged) + return __memo_scope.cached; + return __memo_scope.recache(__memo_parameter_a.value + 1); +} + diff --git a/incremental/compiler-plugin/test/unmemoized/examples/localexported.ts b/incremental/compiler-plugin/test/unmemoized/examples/localexported.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3d0808df1008dac1ca71141e33989f336006b5c --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/examples/localexported.ts @@ -0,0 +1,31 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +/* + * Copyright (c) 2022-2023 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 { __id } from "../context.test"; +import { localexported } from "./localexport"; +/** @memo */ +function entry(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("1___key_id_DIRNAME/localexported.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + localexported(__memo_context, __memo_id + ("0___key_id_DIRNAME/localexported.ts" as __memo_id_type), 17); + { + __memo_scope.recache(); + return; + } +} + diff --git a/incremental/compiler-plugin/test/unmemoized/examples/memo_stable.ts b/incremental/compiler-plugin/test/unmemoized/examples/memo_stable.ts new file mode 100644 index 0000000000000000000000000000000000000000..466421d45108df9bb864e6868d6e029f23d9a5ad --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/examples/memo_stable.ts @@ -0,0 +1,65 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +import { __id } from "../context.test"; +class X { + a = this; + /** @memo */ + methodX(__memo_context: __memo_context_type, __memo_id: __memo_id_type): this { + const __memo_scope = __memo_context.scope(__memo_id + ("0___key_id_DIRNAME/memo_stable.ts" as __memo_id_type), 1); + const __memo_parameter_this = __memo_scope.param(0, this); + if (__memo_scope.unchanged) + return __memo_scope.cached; + const b = __memo_parameter_this.value; + return __memo_scope.recache(__memo_parameter_this.value); + } +} +/** @memo:stable */ +class Y { + c = this; + /** @memo */ + methodY(__memo_context: __memo_context_type, __memo_id: __memo_id_type): this { + const __memo_scope = __memo_context.scope(__memo_id + ("1___key_id_DIRNAME/memo_stable.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return this; + } + const d = this; + { + __memo_scope.recache(); + return this; + } + } +} +/** @memo:stable */ +class Z { + c = this; + /** @memo */ + methodZ(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("3___key_id_DIRNAME/memo_stable.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + const d = this; + class W { + /** @memo */ + methodW(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("2___key_id_DIRNAME/memo_stable.ts" as __memo_id_type), 1); + const __memo_parameter_this = __memo_scope.param(0, this); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + const d = __memo_parameter_this.value; + { + __memo_scope.recache(); + return; + } + } + } + { + __memo_scope.recache(); + return; + } + } +} + diff --git a/incremental/compiler-plugin/test/unmemoized/examples/union_type_parameters.ts b/incremental/compiler-plugin/test/unmemoized/examples/union_type_parameters.ts new file mode 100644 index 0000000000000000000000000000000000000000..42a3c091b72e606acbc684ee7760e915a7dfca6a --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/examples/union_type_parameters.ts @@ -0,0 +1,27 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +import { __id } from "../context.test"; +/** @memo */ +function foo(__memo_context: __memo_context_type, __memo_id: __memo_id_type, +/** @memo */ +param1: undefined | ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => void) | ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, x?: number) => number), +/** @memo */ +param2: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => void) | undefined | ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, x?: number) => number), +/** @memo */ +param3: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => void) | ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, x?: number) => number) | undefined) { + const __memo_scope = __memo_context.scope(__memo_id + ("3___key_id_DIRNAME/union_type_parameters.ts" as __memo_id_type), 3); + const __memo_parameter_param1 = __memo_scope.param(0, param1); + const __memo_parameter_param2 = __memo_scope.param(1, param2); + const __memo_parameter_param3 = __memo_scope.param(2, param3); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + __memo_parameter_param1.value?.(__memo_context, __memo_id + ("0___key_id_DIRNAME/union_type_parameters.ts" as __memo_id_type)); + __memo_parameter_param2.value?.(__memo_context, __memo_id + ("1___key_id_DIRNAME/union_type_parameters.ts" as __memo_id_type)); + __memo_parameter_param3.value?.(__memo_context, __memo_id + ("2___key_id_DIRNAME/union_type_parameters.ts" as __memo_id_type)); + { + __memo_scope.recache(); + return; + } +} + diff --git a/incremental/compiler-plugin/test/unmemoized/util.test.ts b/incremental/compiler-plugin/test/unmemoized/util.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..da07811cfa352684ac8f85b3a78946d857a7eaae --- /dev/null +++ b/incremental/compiler-plugin/test/unmemoized/util.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022-2023 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 path from "path"; +import * as fs from "fs"; +import { assert } from "chai"; +let logText: string = ""; +export function log(msg: string) { + logText = logText.concat(msg, "\n"); +} +export function getLog(): string { + return logText; +} +export function filterDirname(text: string): string { + // tsc AST on windows uses C:/foo/bar representation, but + // __dirname on windows returns C:\foo\bar. + const platformDirname = __dirname.replaceAll(path.sep, "/"); + return text.replaceAll(platformDirname, "DIRNAME"); +} +export function getLogFiltered(): string { + return filterDirname(logText); +} +export function resetLog() { + logText = ""; +} +export function cleanDumpDirectory() { + fs.rmSync(`${__dirname}/dump}`, { recursive: true, force: true }); +} +export function loadDump(name: string, kind: string, module: string): string | undefined { + const dirname = __dirname; + const dumpFile = `${dirname}/${kind}/${name}_${module}.test.ts_dump`; + const dump = fs.readFileSync(dumpFile, 'utf8'); + return (dump && dump.includes('\r')) + ? dump.replace(/[\r]/g, '') + : dump; +} +export function checkDump(name: string, module: string) { + test(`Transformed ${name} dump matches the golden dump`, () => { + const test = loadDump(name, "dump", module); + const testFiltered = test ? filterDirname(test) : undefined; + const golden = loadDump(name, "golden", module); + assert.equal(golden, testFiltered); + }); +} +