idlize
512 строк · 17.1 Кб
1/*
2* Copyright (c) 2022-2024 Huawei Device Co., Ltd.
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7* http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15
16import * as ts from 'typescript';
17import * as fs from "fs"
18import { UniqueId } from "@koalaui/common"
19import { Rewrite } from './transformation-context';
20
21
22export enum FunctionKind {
23REGULAR,
24MEMO,
25MEMO_INTRINSIC,
26}
27
28export type FunctionTable = Map<ts.SignatureDeclarationBase, FunctionKind>
29export type CallTable = Map<ts.CallExpression, FunctionKind>
30export type EntryTable = Set<ts.CallExpression>
31export type VariableTable = Map<ts.VariableLikeDeclaration, FunctionKind>
32
33export function isNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration {
34return ("name" in node )
35}
36
37export function asString(node: ts.Node|undefined): string {
38if (node === undefined) return "undefined node"
39if (ts.isIdentifier(node)) return ts.idText(node)
40if (isNamedDeclaration(node)) {
41if (node.name === undefined) {
42return `${ts.SyntaxKind[node.kind]}(undefined name)`
43} else {
44
45return `${ts.SyntaxKind[node.kind]}(${asString(node.name)})`
46}
47} else {
48return `${ts.SyntaxKind[node.kind]}`
49}
50}
51
52export function isFunctionOrMethod(node: ts.Node): node is ts.FunctionLikeDeclaration {
53return ts.isFunctionDeclaration(node) ||
54ts.isMethodDeclaration(node) ||
55ts.isFunctionExpression(node) ||
56ts.isArrowFunction(node)
57}
58
59export enum RuntimeNames {
60COMPUTE = "compute",
61CONTEXT = "__memo_context",
62ID = "__memo_id",
63SCOPE = "__memo_scope",
64INTERNAL_PARAMETER_STATE = "param",
65INTERNAL_VALUE = "cached",
66INTERNAL_VALUE_NEW = "recache",
67INTERNAL_SCOPE = "scope",
68INTERNAL_VALUE_OK = "unchanged",
69CONTENT = "content",
70VALUE = "value",
71__CONTEXT = "__context",
72__ID = "__id",
73__KEY = "__key",
74__STATE = "__state",
75CONTEXT_TYPE = "__memo_context_type",
76ID_TYPE = "__memo_id_type",
77TRANSFORMED_TYPE = "__memo_transformed",
78SYNTHETIC_RETURN_MARK = "__synthetic_return_value",
79CONTEXT_TYPE_DEFAULT_IMPORT = "@koalaui/runtime",
80ANNOTATION = "@memo",
81ANNOTATION_INTRINSIC = "@memo:intrinsic",
82ANNOTATION_ENTRY = "@memo:entry",
83ANNOTATION_SKIP = "@skip:memo", // skip binding to parameter changes
84ANNOTATION_STABLE = "@memo:stable", // assume this should not be tracked
85}
86
87export function Undefined(): ts.Identifier {
88return ts.factory.createIdentifier("undefined")
89}
90
91export function runtimeIdentifier(name: RuntimeNames): ts.Identifier {
92return ts.factory.createIdentifier(name)
93}
94
95export function componentTypeName(functionName: ts.PropertyName|undefined): ts.Identifier {
96if (!functionName || !ts.isIdentifier(functionName) && !ts.isPrivateIdentifier(functionName)) {
97throw new Error("Expected an identifier: " + asString(functionName))
98}
99return ts.factory.createIdentifier(ts.idText(functionName)+"Node")
100}
101
102export function isSpecialContentParameter(param: ts.ParameterDeclaration): boolean {
103if (!param.type) return false
104if (!param.name) return false
105return (
106ts.isFunctionTypeNode(param.type) &&
107ts.isIdentifier(param.name) &&
108ts.idText(param.name) == RuntimeNames.CONTENT
109)
110}
111
112export function isSpecialContextParameter(parameter: ts.ParameterDeclaration): boolean {
113return !!parameter.name && ts.isIdentifier(parameter.name) && ts.idText(parameter.name) == RuntimeNames.CONTEXT
114}
115
116export function isSpecialIdParameter(parameter: ts.ParameterDeclaration): boolean {
117return !!parameter.name && ts.isIdentifier(parameter.name) && ts.idText(parameter.name) == RuntimeNames.ID
118}
119
120export function isSkippedParameter(sourceFile: ts.SourceFile, parameter: ts.ParameterDeclaration): boolean {
121return getComment(sourceFile, parameter).includes(RuntimeNames.ANNOTATION_SKIP)
122}
123
124export function isStableClass(sourceFile: ts.SourceFile, clazz: ts.ClassDeclaration): boolean {
125return getComment(sourceFile, clazz).includes(RuntimeNames.ANNOTATION_STABLE)
126}
127
128export function hasMemoEntry(sourceFile: ts.SourceFile, node: ts.Node): boolean {
129const comment = getComment(sourceFile, node)
130return comment.includes(RuntimeNames.ANNOTATION_ENTRY)
131}
132
133export function isMemoEntry(sourceFile: ts.SourceFile, func: ts.FunctionDeclaration): boolean {
134const name = func.name
135if (!name) return false
136if (!ts.isIdentifier(name)) return false
137
138return hasMemoEntry(sourceFile, func)
139}
140
141export function isTrackableParameter(sourceFile: ts.SourceFile, parameter: ts.ParameterDeclaration): boolean {
142return !isSpecialContentParameter(parameter)
143&& !isSpecialContextParameter(parameter)
144&& !isSpecialIdParameter(parameter)
145&& !isSkippedParameter(sourceFile, parameter)
146}
147
148export function parameterStateName(original: string): string {
149return `__memo_parameter_${original}`
150}
151
152export function getSymbolByNode(typechecker: ts.TypeChecker, node: ts.Node) : ts.Symbol|undefined {
153return typechecker.getSymbolAtLocation(node)
154}
155
156export function getDeclarationsByNode(typechecker: ts.TypeChecker, node: ts.Node) : ts.Declaration[] {
157const symbol = getSymbolByNode(typechecker, node)
158const declarations = symbol?.getDeclarations() ?? []
159return declarations
160}
161
162export function findSourceFile(node: ts.Node): ts.SourceFile|undefined {
163let element = node
164while(element) {
165if (ts.isSourceFile(element)) return element
166element = element.parent
167}
168return undefined
169}
170
171export function findFunctionDeclaration(node: ts.Node): ts.FunctionDeclaration|undefined {
172let element = node
173while(element) {
174if (ts.isFunctionDeclaration(element)) return element
175element = element.parent
176}
177return undefined
178}
179
180export function isMethodOfStableClass(sourceFile: ts.SourceFile, method: ts.MethodDeclaration): boolean {
181const original = ts.getOriginalNode(method)
182const parent = original.parent
183if (!parent) return false
184if (!ts.isClassDeclaration(parent)) return false
185return isStableClass(sourceFile, parent)
186}
187
188export function arrayAt<T>(array: T[] | undefined, index: number): T|undefined {
189return array ? array[index >= 0 ? index : array.length + index] : undefined
190}
191
192export function getComment(sourceFile: ts.SourceFile, node: ts.Node): string {
193const commentRanges = ts.getLeadingCommentRanges(
194sourceFile.getFullText(),
195node.getFullStart()
196)
197
198const commentRange = arrayAt(commentRanges, -1)
199if (!commentRange) return ""
200
201const comment = sourceFile.getFullText()
202.slice(commentRange.pos, commentRange.end)
203return comment
204}
205
206export function isVoidOrNotSpecified(type: ts.TypeNode | undefined): boolean {
207return type === undefined
208|| type.kind === ts.SyntaxKind.VoidKeyword
209|| ts.isTypeReferenceNode(type)
210&& ts.isIdentifier(type.typeName)
211&& ts.idText(type.typeName) == "void"
212}
213
214export class PositionalIdTracker {
215// Global for the whole program.
216static callCount: number = 0
217
218// Set `stable` to true if you want to have more predictable values.
219// For example for tests.
220// Don't use it in production!
221constructor(public sourceFile: ts.SourceFile, public stableForTests: boolean = false) {
222if (stableForTests) PositionalIdTracker.callCount = 0
223}
224
225sha1Id(callName: string, fileName: string): string {
226const uniqId = new UniqueId()
227uniqId.addString("memo call uniqid")
228uniqId.addString(fileName)
229uniqId.addString(callName)
230uniqId.addI32(PositionalIdTracker.callCount++)
231return uniqId.compute().substring(0,10)
232}
233
234stringId(callName: string, fileName: string): string {
235return `${PositionalIdTracker.callCount++}_${callName}_id_DIRNAME/${fileName}`
236}
237
238id(callName: string): ts.Expression {
239
240const fileName = this.stableForTests ?
241baseName(this.sourceFile.fileName) :
242this.sourceFile.fileName
243
244const positionId = (this.stableForTests) ?
245this.stringId(callName, fileName) :
246this.sha1Id(callName, fileName)
247
248
249return this.stableForTests ?
250ts.factory.createStringLiteral(positionId) :
251ts.factory.createNumericLiteral("0x"+positionId)
252
253}
254}
255
256export function wrapInCompute(node: ts.ConciseBody, id: ts.Expression): ts.Expression {
257return ts.factory.createCallExpression(
258ts.factory.createPropertyAccessExpression(
259runtimeIdentifier(RuntimeNames.CONTEXT),
260runtimeIdentifier(RuntimeNames.COMPUTE)
261),
262/*typeArguments*/ undefined,
263[
264id,
265ts.factory.createArrowFunction(
266/*modifiers*/ undefined,
267/*typeParameters*/ undefined,
268/*parameters*/ [],
269/*type*/ undefined,
270ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
271node
272)
273]
274)
275}
276
277export function createComputeScope(pseudoStateCount: number, id: ts.Expression, typeArgument: ts.TypeNode | undefined): ts.VariableStatement {
278return constVariable(RuntimeNames.SCOPE,
279ts.factory.createCallExpression(
280ts.factory.createPropertyAccessExpression(
281runtimeIdentifier(RuntimeNames.CONTEXT),
282runtimeIdentifier(RuntimeNames.INTERNAL_SCOPE)
283),
284typeArgument ? [typeArgument] : undefined,
285pseudoStateCount > 0
286// TODO: these are because es2panda is not goot at handling default values in complex signatures
287? [id, ts.factory.createNumericLiteral(pseudoStateCount), Undefined(), Undefined(), Undefined(), Undefined()]
288: [id, Undefined(), Undefined(), Undefined(), Undefined(), Undefined()]
289)
290)
291}
292
293export function constVariable(name: string, initializer: ts.Expression): ts.VariableStatement {
294return ts.factory.createVariableStatement(
295undefined,
296ts.factory.createVariableDeclarationList(
297[ts.factory.createVariableDeclaration(
298ts.factory.createIdentifier(name),
299undefined,
300undefined,
301initializer
302)],
303ts.NodeFlags.Const
304)
305)
306}
307
308export function idPlusKey(positionalIdTracker: PositionalIdTracker): ts.Expression {
309return ts.factory.createBinaryExpression(
310runtimeIdentifier(RuntimeNames.ID),
311ts.factory.createToken(ts.SyntaxKind.PlusToken),
312ts.factory.createAsExpression(
313positionalIdTracker.id(RuntimeNames.__KEY),
314ts.factory.createTypeReferenceNode(RuntimeNames.ID_TYPE)
315)
316)
317}
318
319export function isAnyMemoKind(kind: FunctionKind|undefined): boolean {
320switch(kind) {
321case FunctionKind.MEMO:
322case FunctionKind.MEMO_INTRINSIC:
323return true
324}
325return false
326}
327
328export function isMemoKind(kind: FunctionKind|undefined): boolean {
329switch(kind) {
330case FunctionKind.MEMO:
331return true
332}
333return false
334}
335
336export function createContextType(): ts.TypeNode {
337return ts.factory.createTypeReferenceNode(
338runtimeIdentifier(RuntimeNames.CONTEXT_TYPE),
339undefined
340)
341}
342
343export function createIdType(): ts.TypeNode {
344return ts.factory.createTypeReferenceNode(
345runtimeIdentifier(RuntimeNames.ID_TYPE),
346undefined
347)
348}
349
350export function createHiddenParameters(): ts.ParameterDeclaration[] {
351const context = ts.factory.createParameterDeclaration(
352/*modifiers*/ undefined,
353/*dotDotDotToken*/ undefined,
354RuntimeNames.CONTEXT,
355/* questionToken */ undefined,
356createContextType(),
357/* initializer */ undefined
358
359)
360const id = ts.factory.createParameterDeclaration(
361/*modifiers*/ undefined,
362/*dotDotDotToken*/ undefined,
363RuntimeNames.ID,
364/* questionToken */ undefined,
365createIdType(),
366/* initializer */ undefined
367)
368
369return [context, id]
370}
371
372export function createContextTypeImport(importSource: string): ts.Statement {
373return ts.factory.createImportDeclaration(
374undefined,
375ts.factory.createImportClause(
376false,
377undefined,
378ts.factory.createNamedImports(
379[
380ts.factory.createImportSpecifier(
381false,
382undefined,
383ts.factory.createIdentifier(RuntimeNames.CONTEXT_TYPE)
384),
385ts.factory.createImportSpecifier(
386false,
387undefined,
388ts.factory.createIdentifier(RuntimeNames.ID_TYPE)
389)
390]
391)
392),
393ts.factory.createStringLiteral(importSource),
394undefined
395)
396}
397
398export function setNeedTypeImports(rewrite: Rewrite) {
399rewrite.importTypesFrom = rewrite.pluginOptions.contextImport ?? RuntimeNames.CONTEXT_TYPE_DEFAULT_IMPORT
400}
401
402export function hiddenParameters(rewriter: Rewrite): ts.ParameterDeclaration[] {
403setNeedTypeImports(rewriter)
404return createHiddenParameters()
405}
406
407export function skipParenthesizedType(type: ts.TypeNode|undefined): ts.TypeNode|undefined {
408let current: ts.TypeNode|undefined = type
409while (current && ts.isParenthesizedTypeNode(current)) {
410current = current.type
411}
412return current
413}
414
415export function localStateStatement(
416stateName: string,
417referencedEntity: ts.Expression,
418parameterIndex: number
419): ts.Statement {
420return ts.factory.createVariableStatement(
421undefined,
422ts.factory.createVariableDeclarationList(
423[
424ts.factory.createVariableDeclaration(
425parameterStateName(stateName),
426undefined,
427undefined,
428ts.factory.createCallExpression(
429ts.factory.createPropertyAccessExpression(
430runtimeIdentifier(RuntimeNames.SCOPE),
431runtimeIdentifier(RuntimeNames.INTERNAL_PARAMETER_STATE)
432),
433undefined,
434// TODO: these are because es2panda is not goot at handling default values in complex signatures
435[ts.factory.createNumericLiteral(parameterIndex), referencedEntity, Undefined(), Undefined(), Undefined()]
436)
437)
438],
439ts.NodeFlags.Const
440)
441)
442}
443
444export function hasStaticModifier(node: ts.MethodDeclaration): boolean {
445return node.modifiers?.find(it => it.kind == ts.SyntaxKind.StaticKeyword) != undefined
446}
447
448export function error(message: any): any {
449console.log(message)
450console.trace()
451return undefined
452}
453
454export interface TransformerOptions {
455// Emit transformed functions to the console.
456trace?: boolean,
457// Store the transformed functions to this directory.
458keepTransformed?: string,
459// Use human readable callsite IDs without directory paths.
460stableForTest?: boolean,
461// Import context and id types from alternative source
462contextImport?: string,
463// Dump sources with resolved memo annotations to unmemoized directory
464only_unmemoize?: boolean,
465}
466
467function baseName(path: string): string {
468return path.replace(/^.*\/(.*)$/, "$1")
469}
470
471export class Tracer {
472constructor (public options: TransformerOptions, public printer: ts.Printer) {
473}
474
475trace(msg: any) {
476if (!this.options.trace) return
477console.log(msg)
478}
479
480writeTextToFile(text: string, file: string) {
481fs.writeFileSync(file, text, 'utf8')
482this.trace("DUMP TO: " + file)
483}
484
485createDirs(dirs: string) {
486fs.mkdirSync(dirs, { recursive: true });
487}
488
489dumpFileName(sourceFile: ts.SourceFile, transformed: ts.FunctionLikeDeclarationBase): string|undefined {
490if (!this.options.keepTransformed) return undefined
491
492const outDir = (this.options.keepTransformed[0] == "/") ?
493this.options.keepTransformed :
494`${__dirname}/${this.options.keepTransformed}`
495
496this.createDirs(outDir)
497
498const sourceBaseName = baseName(sourceFile.fileName)
499if (!transformed.name) return
500if (!ts.isIdentifier(transformed.name)) return
501const fileName = `${ts.idText(transformed.name)}_${sourceBaseName}`
502return `${outDir}/${fileName}_dump`
503}
504
505keepTransformedFunction(transformed: ts.FunctionLikeDeclarationBase, sourceFile: ts.SourceFile) {
506const fileName = this.dumpFileName(sourceFile, transformed)
507if (!fileName) return
508
509const content = this.printer.printNode(ts.EmitHint.Unspecified, transformed, sourceFile)
510this.writeTextToFile(content+"\n", fileName)
511}
512}
513