From 8c279b6f4ccbec5f7b26da5c48266005c5d299eb Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:25:29 +0400 Subject: [PATCH] feat(ast): memoize checker calls and avoid regex when possible --- src/baseTranspiler.ts | 130 +++++++++++++++++++++------------------- src/csharpTranspiler.ts | 24 ++++---- src/goTranspiler.ts | 31 ++++++---- src/javaTranspiler.ts | 76 ++++++++++++----------- src/transpiler.ts | 37 ++++++++++++ 5 files changed, 177 insertions(+), 121 deletions(-) diff --git a/src/baseTranspiler.ts b/src/baseTranspiler.ts index c2d7de3..df2e0b2 100644 --- a/src/baseTranspiler.ts +++ b/src/baseTranspiler.ts @@ -962,16 +962,20 @@ class BaseTranspiler { if (text in this.StringLiteralReplacements) { return this.StringLiteralReplacements[text]; } - text = text.replaceAll("\b", "\\b"); - text = text.replaceAll("\f", "\\f"); - text = text.replaceAll("\n", "\\n"); - text = text.replaceAll("\r", "\\r"); - text = text.replaceAll("\t", "\\t"); - if (token === "'") { - text = text.replaceAll("\\\"", "\""); // unscape double quotes - text = text.replaceAll("'", "\\'"); // escape single quotes - } else if (token === "\"") { - text = text.replaceAll("\"", "\\\""); // escape double quotes + // most literals contain none of the escapable characters; one regex test + // avoids seven replaceAll passes over every string in the file + if (/[\b\f\n\r\t'"\\]/.test(text)) { + text = text.replaceAll("\b", "\\b"); + text = text.replaceAll("\f", "\\f"); + text = text.replaceAll("\n", "\\n"); + text = text.replaceAll("\r", "\\r"); + text = text.replaceAll("\t", "\\t"); + if (token === "'") { + text = text.replaceAll("\\\"", "\""); // unscape double quotes + text = text.replaceAll("'", "\\'"); // escape single quotes + } else if (token === "\"") { + text = text.replaceAll("\"", "\\\""); // escape double quotes + } } return token + text + token; } @@ -1581,17 +1585,10 @@ class BaseTranspiler { return this.printPrefixUnaryExpression(node, identation); // avoid infinite recursion } - let expression = this.printNode(node, 0); - // wrap falsy/truty expressions if needed - if ( (1+1) || (node.kind !== ts.SyntaxKind.BinaryExpression && node.kind !== ts.SyntaxKind.ParenthesizedExpression)) { - - const typeFlags = global.checker.getTypeAtLocation(node).flags; - if ( (1+1) || typeFlags !== ts.TypeFlags.BooleanLiteral && typeFlags !== ts.TypeFlags.Boolean) { - expression = this.printNode(node, 0); - // this.warn(node, node.getText(), "Falsy/Truthy expressions are not supported by this language, so adding the defined wrapper!"); - expression = `${this.FALSY_WRAPPER_OPEN}${expression}${this.FALSY_WRAPPER_CLOSE}`; - } - } + // wrap falsy/truthy expressions unconditionally: printing the node once and + // wrapping is equivalent to the previous always-true branches, which printed + // the subtree twice and queried the type checker without using the result + const expression = `${this.FALSY_WRAPPER_OPEN}${this.printNode(node, 0)}${this.FALSY_WRAPPER_CLOSE}`; return `${this.getIden(identation)}${expression}`; // stub to override } @@ -1836,89 +1833,96 @@ class BaseTranspiler { printNode(node, identation = 0): string { try { - if(ts.isExpressionStatement(node)) { + // dispatch on node.kind directly: every ts.isX predicate used here is a + // plain kind comparison, and a switch avoids running ~45 predicate calls + // for nodes that match late (or not at all) in the former if-else chain + switch (node.kind) { + case ts.SyntaxKind.ExpressionStatement: return this.printExpressionStatement(node, identation); - } else if(ts.isBlock(node)) { + case ts.SyntaxKind.Block: return this.printBlock(node, identation); - } else if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)){ + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.ArrowFunction: return this.printFunctionDeclaration(node, identation); - } else if (ts.isClassDeclaration(node)) { + case ts.SyntaxKind.ClassDeclaration: return this.printClass(node, identation); - } else if (ts.isVariableStatement(node)) { + case ts.SyntaxKind.VariableStatement: return this.printVariableStatement(node, identation); - } else if (ts.isMethodDeclaration(node)) { + case ts.SyntaxKind.MethodDeclaration: return this.printMethodDeclaration(node, identation); - } else if (ts.isStringLiteral(node)) { + case ts.SyntaxKind.StringLiteral: return this.printStringLiteral(node); - } else if (ts.isNumericLiteral(node)) { + case ts.SyntaxKind.NumericLiteral: return this.printNumericLiteral(node); - } else if (ts.isPropertyAccessExpression(node)) { + case ts.SyntaxKind.PropertyAccessExpression: return this.printPropertyAccessExpression(node, identation); - } else if (ts.isArrayLiteralExpression(node)) { + case ts.SyntaxKind.ArrayLiteralExpression: return this.printArrayLiteralExpression(node, identation); - } else if (ts.isCallExpression(node)) { + case ts.SyntaxKind.CallExpression: return this.printCallExpression(node, identation); - } else if (ts.isWhileStatement(node)) { + case ts.SyntaxKind.WhileStatement: return this.printWhileStatement(node, identation); - } else if (ts.isBinaryExpression(node)) { + case ts.SyntaxKind.BinaryExpression: return this.printBinaryExpression(node, identation); - } else if (ts.isBreakStatement(node)) { + case ts.SyntaxKind.BreakStatement: return this.printBreakStatement(node, identation); - } else if (ts.isForStatement(node)) { + case ts.SyntaxKind.ForStatement: return this.printForStatement(node, identation); - } else if (ts.isPostfixUnaryExpression(node)) { + case ts.SyntaxKind.PostfixUnaryExpression: return this.printPostFixUnaryExpression(node, identation); - } else if (ts.isVariableDeclarationList(node)) { + case ts.SyntaxKind.VariableDeclarationList: return this.printVariableDeclarationList(node, identation); // statements are slightly different if inside a for - } else if (ts.isObjectLiteralExpression(node)) { + case ts.SyntaxKind.ObjectLiteralExpression: return this.printObjectLiteralExpression(node, identation); - } else if (ts.isPropertyAssignment(node)) { + case ts.SyntaxKind.PropertyAssignment: return this.printPropertyAssignment(node, identation); - } else if (ts.isIdentifier(node)) { + case ts.SyntaxKind.Identifier: return this.printIdentifier(node); - } else if (ts.isElementAccessExpression(node)) { + case ts.SyntaxKind.ElementAccessExpression: return this.printElementAccessExpression(node, identation); - } else if (ts.isIfStatement(node)) { + case ts.SyntaxKind.IfStatement: return this.printIfStatement(node, identation); - } else if (ts.isParenthesizedExpression(node)) { + case ts.SyntaxKind.ParenthesizedExpression: return this.printParenthesizedExpression(node, identation); - } else if ((ts as any).isBooleanLiteral(node)) { + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: return this.printBooleanLiteral(node); - } else if (ts.SyntaxKind.ThisKeyword === node.kind) { + case ts.SyntaxKind.ThisKeyword: return this.printThisKeyword(node, identation); - } else if (ts.SyntaxKind.SuperKeyword === node.kind) { + case ts.SyntaxKind.SuperKeyword: return this.SUPER_TOKEN; - }else if (ts.isTryStatement(node)){ + case ts.SyntaxKind.TryStatement: return this.printTryStatement(node, identation); - } else if (ts.isPrefixUnaryExpression(node)) { + case ts.SyntaxKind.PrefixUnaryExpression: return this.printPrefixUnaryExpression(node, identation); - } else if (ts.isThrowStatement(node)) { + case ts.SyntaxKind.ThrowStatement: return this.printThrowStatement(node, identation); - } else if (ts.isNewExpression(node)) { + case ts.SyntaxKind.NewExpression: return this.printNewExpression(node, identation); - } else if (ts.isAwaitExpression(node)) { + case ts.SyntaxKind.AwaitExpression: return this.printAwaitExpression(node, identation); - } else if (ts.isConditionalExpression(node)) { + case ts.SyntaxKind.ConditionalExpression: return this.printConditionalExpression(node, identation); - } else if (ts.isAsExpression(node)) { + case ts.SyntaxKind.AsExpression: return this.printAsExpression(node, identation); - } else if (ts.isReturnStatement(node)) { + case ts.SyntaxKind.ReturnStatement: return this.printReturnStatement(this.wrapImplicitReturnAwait(node), identation); - } else if (ts.isArrayBindingPattern(node)) { + case ts.SyntaxKind.ArrayBindingPattern: return this.printArrayBindingPattern(node, identation); - } else if (ts.isParameter(node)) { + case ts.SyntaxKind.Parameter: return this.printParameter(node); - } else if (ts.isConstructorDeclaration(node)) { + case ts.SyntaxKind.Constructor: return this.printConstructorDeclaration(node, identation); - } else if (ts.isPropertyDeclaration(node)) { + case ts.SyntaxKind.PropertyDeclaration: return this.printPropertyDeclaration(node, identation); - } else if (ts.isSpreadElement(node)) { + case ts.SyntaxKind.SpreadElement: return this.printSpreadElement(node, identation); - } else if (ts.SyntaxKind.NullKeyword === node.kind) { + case ts.SyntaxKind.NullKeyword: return this.printNullKeyword(node, identation); - } else if (ts.isContinueStatement(node)) { + case ts.SyntaxKind.ContinueStatement: return this.printContinueStatement(node, identation); - } else if (ts.isDeleteExpression(node)) { + case ts.SyntaxKind.DeleteExpression: return this.printDeleteExpression(node, identation); } diff --git a/src/csharpTranspiler.ts b/src/csharpTranspiler.ts index bc846a2..ec8b24e 100644 --- a/src/csharpTranspiler.ts +++ b/src/csharpTranspiler.ts @@ -462,19 +462,21 @@ export class CSharpTranspiler extends BaseTranspiler { return `inOp(${this.printNode(right, 0)}, ${this.printNode(left, 0)})`; } - const leftText = this.printNode(left, 0); - const rightText = this.printNode(right, 0); - - if (op === ts.SyntaxKind.PlusEqualsToken) { - return `${leftText} = add(${leftText}, ${rightText})`; - } - - if (op === ts.SyntaxKind.MinusEqualsToken) { - return `${leftText} = subtract(${leftText}, ${rightText})`; - } + // only print the operands when this op is actually handled here; otherwise + // the base printBinaryExpression prints them, and doing it eagerly means + // every unhandled binary expression gets its subtrees printed twice + if (op === ts.SyntaxKind.PlusEqualsToken || op === ts.SyntaxKind.MinusEqualsToken || op in this.binaryExpressionsWrappers) { + const leftText = this.printNode(left, 0); + const rightText = this.printNode(right, 0); + + if (op === ts.SyntaxKind.PlusEqualsToken) { + return `${leftText} = add(${leftText}, ${rightText})`; + } + if (op === ts.SyntaxKind.MinusEqualsToken) { + return `${leftText} = subtract(${leftText}, ${rightText})`; + } - if (op in this.binaryExpressionsWrappers) { const wrapper = this.binaryExpressionsWrappers[op]; const open = wrapper[0]; const close = wrapper[1]; diff --git a/src/goTranspiler.ts b/src/goTranspiler.ts index ef8bd77..c5aa407 100644 --- a/src/goTranspiler.ts +++ b/src/goTranspiler.ts @@ -169,9 +169,12 @@ export class GoTranspiler extends BaseTranspiler { if (text in this.StringLiteralReplacements) { return this.StringLiteralReplacements[text]; } - text = text.replaceAll("'", "\\\\" + "'"); - text = text.replaceAll("\"", "\\" + "\""); - text = text.replaceAll("\n", "\\n"); + // skip the replaceAll passes when there is nothing to escape + if (/['"\n]/.test(text)) { + text = text.replaceAll("'", "\\\\" + "'"); + text = text.replaceAll("\"", "\\" + "\""); + text = text.replaceAll("\n", "\\n"); + } return token + text + token; } @@ -870,19 +873,21 @@ ${this.getIden(identation)}PanicOnError(${varName})`; return `InOp(${this.printNode(right, 0)}, ${this.printNode(left, 0)})`; } - const leftText = this.printNode(left, 0); - const rightText = this.printNode(right, 0); - - if (op === ts.SyntaxKind.PlusEqualsToken) { - return `${leftText} = Add(${leftText}, ${rightText})`; - } + // only print the operands when this op is actually handled here; otherwise + // the base printBinaryExpression prints them, and doing it eagerly means + // every unhandled binary expression gets its subtrees printed twice + if (op === ts.SyntaxKind.PlusEqualsToken || op === ts.SyntaxKind.MinusEqualsToken || op in this.binaryExpressionsWrappers) { + const leftText = this.printNode(left, 0); + const rightText = this.printNode(right, 0); - if (op === ts.SyntaxKind.MinusEqualsToken) { - return `${leftText} = Subtract(${leftText}, ${rightText})`; - } + if (op === ts.SyntaxKind.PlusEqualsToken) { + return `${leftText} = Add(${leftText}, ${rightText})`; + } + if (op === ts.SyntaxKind.MinusEqualsToken) { + return `${leftText} = Subtract(${leftText}, ${rightText})`; + } - if (op in this.binaryExpressionsWrappers) { const wrapper = this.binaryExpressionsWrappers[op]; const open = wrapper[0]; const close = wrapper[1]; diff --git a/src/javaTranspiler.ts b/src/javaTranspiler.ts index 99bf565..db795df 100644 --- a/src/javaTranspiler.ts +++ b/src/javaTranspiler.ts @@ -263,28 +263,30 @@ export class JavaTranspiler extends BaseTranspiler { return this.UNDEFINED_TOKEN; } - // keep the same class-reference typeof-guarding logic as your original file - const type = (global as any).checker.getTypeAtLocation(node); - const symbol = type?.symbol; - if (symbol !== undefined) { - const decl = symbol?.declarations ?? []; - let isBuiltIn = undefined; - if (decl.length > 0) { - isBuiltIn = - decl[0].getSourceFile().fileName.indexOf("typescript") > -1; - } + // keep the same class-reference typeof-guarding logic as your original file, + // but run the syntactic (parent-position) checks first: they exclude the vast + // majority of identifiers without paying for a type-checker lookup + const isInsideNewExpression = + node?.parent?.kind === ts.SyntaxKind.NewExpression; + const isInsideCatch = + node?.parent?.kind === ts.SyntaxKind.ThrowStatement; + const isLeftSide = + node?.parent?.name === node || node?.parent?.left === node; + const isCallOrPropertyAccess = + node?.parent?.kind === ts.SyntaxKind.PropertyAccessExpression || + node?.parent?.kind === ts.SyntaxKind.ElementAccessExpression; + if (!isLeftSide && !isCallOrPropertyAccess && !isInsideCatch && !isInsideNewExpression) { + const type = (global as any).checker.getTypeAtLocation(node); + const typeSymbol = type?.symbol; + if (typeSymbol !== undefined) { + const decl = typeSymbol?.declarations ?? []; + let isBuiltIn = undefined; + if (decl.length > 0) { + isBuiltIn = + decl[0].getSourceFile().fileName.indexOf("typescript") > -1; + } - if (isBuiltIn !== undefined && !isBuiltIn) { - const isInsideNewExpression = - node?.parent?.kind === ts.SyntaxKind.NewExpression; - const isInsideCatch = - node?.parent?.kind === ts.SyntaxKind.ThrowStatement; - const isLeftSide = - node?.parent?.name === node || node?.parent?.left === node; - const isCallOrPropertyAccess = - node?.parent?.kind === ts.SyntaxKind.PropertyAccessExpression || - node?.parent?.kind === ts.SyntaxKind.ElementAccessExpression; - if (!isLeftSide && !isCallOrPropertyAccess && !isInsideCatch && !isInsideNewExpression) { + if (isBuiltIn !== undefined && !isBuiltIn) { const symbol = (global as any).checker.getSymbolAtLocation(node); let isClassDeclaration = false; if (symbol) { @@ -509,11 +511,13 @@ export class JavaTranspiler extends BaseTranspiler { } getVarMethodIfAny(node) { - // should return the name of the method this node belongs to, if any + // should return the name of the method this node belongs to, if any; + // the raw AST name is enough here — the result is only used as a scoping + // key, and printNode on an identifier consults the type checker let current = node?.parent; while (current) { if (ts.isMethodDeclaration(current) || ts.isFunctionDeclaration(current)) { - return this.printNode(current.name, 0); + return String((current.name as any)?.escapedText ?? ''); } current = current.parent; } @@ -521,11 +525,12 @@ export class JavaTranspiler extends BaseTranspiler { } getVarClassIfAny(node) { - // should return the name of the class this node belongs to, if any + // should return the name of the class this node belongs to, if any; + // raw AST name for the same reason as getVarMethodIfAny let current = node?.parent; while (current) { if (ts.isClassDeclaration(current)) { - return this.printNode(current.name, 0); + return String((current.name as any)?.escapedText ?? ''); } current = current.parent; } @@ -631,18 +636,21 @@ export class JavaTranspiler extends BaseTranspiler { return `Helpers.inOp(${this.printNode(right, 0)}, ${this.printNode(left, 0)})`; } - const leftText = this.printNode(left, 0); - const rightText = this.printNode(right, 0); + // only print the operands when this op is actually handled here; otherwise + // the base printBinaryExpression prints them, and doing it eagerly means + // every unhandled binary expression gets its subtrees printed twice + if (op === ts.SyntaxKind.PlusEqualsToken || op === ts.SyntaxKind.MinusEqualsToken || op in this.binaryExpressionsWrappers) { + const leftText = this.printNode(left, 0); + const rightText = this.printNode(right, 0); - if (op === ts.SyntaxKind.PlusEqualsToken) { - return `${leftText} = Helpers.add(${leftText}, ${rightText})`; - } + if (op === ts.SyntaxKind.PlusEqualsToken) { + return `${leftText} = Helpers.add(${leftText}, ${rightText})`; + } - if (op === ts.SyntaxKind.MinusEqualsToken) { - return `${leftText} = Helpers.subtract(${leftText}, ${rightText})`; - } + if (op === ts.SyntaxKind.MinusEqualsToken) { + return `${leftText} = Helpers.subtract(${leftText}, ${rightText})`; + } - if (op in this.binaryExpressionsWrappers) { const wrapper = this.binaryExpressionsWrappers[op]; const open = wrapper[0]; const close = wrapper[1]; diff --git a/src/transpiler.ts b/src/transpiler.ts index f0a1088..8a760af 100644 --- a/src/transpiler.ts +++ b/src/transpiler.ts @@ -76,6 +76,41 @@ function overrideHostForVirtualFiles(host: ts.CompilerHost, files: Map(); + const originalGetTypeAtLocation = checker.getTypeAtLocation.bind(checker); + checker.getTypeAtLocation = (node: ts.Node): ts.Type => { + let type = typeCache.get(node); + if (type === undefined) { + type = originalGetTypeAtLocation(node); + typeCache.set(node, type); + } + return type; + }; + + const symbolCache = new WeakMap(); + const originalGetSymbolAtLocation = checker.getSymbolAtLocation.bind(checker); + checker.getSymbolAtLocation = (node: ts.Node): ts.Symbol | undefined => { + const cached = symbolCache.get(node); + if (cached !== undefined) { + return cached === NO_SYMBOL_SENTINEL ? undefined : cached; + } + const symbol = originalGetSymbolAtLocation(node); + symbolCache.set(node, symbol === undefined ? NO_SYMBOL_SENTINEL : symbol); + return symbol; + }; +} + function getProgramAndTypeCheckerFromMemory (rootDir: string, text: string, options: any = {}): [any,any,any] { options = options || ts.getDefaultCompilerOptions(); const inMemoryFilePath = path.resolve(path.join(rootDir, "__dummy-file.ts")); @@ -95,6 +130,7 @@ function getProgramAndTypeCheckerFromMemory (rootDir: string, text: string, opti }); const typeChecker = program.getTypeChecker(); + memoizeCheckerCalls(typeChecker); const sourceFile = program.getSourceFile(inMemoryFilePath); return [ program, typeChecker, sourceFile]; @@ -184,6 +220,7 @@ export default class Transpiler { this.byPathOldProgram = program; const sourceFile = program.getSourceFile(path); const typeChecker = program.getTypeChecker(); + memoizeCheckerCalls(typeChecker); global.src = sourceFile; global.checker = typeChecker;