initial version of using ts compiler to remove unused locals

This commit is contained in:
Simon 2020-07-31 21:22:56 -07:00
parent 6d9d6ff34b
commit cd443ce10d
9 changed files with 2286 additions and 11802 deletions

7
.vscode/launch.json vendored
View File

@ -6,6 +6,13 @@
"name": "run testcases",
"runtimeArgs": ["-r", "ts-node/register/transpile-only.js"],
"args": ["${workspaceFolder}/src/compile-testcases.ts"]
},
{
"type": "node",
"request": "launch",
"name": "run index",
"runtimeArgs": ["-r", "ts-node/register/transpile-only.js"],
"args": ["${workspaceFolder}/src/index.ts"]
}
]
}

View File

@ -21,6 +21,7 @@ import {
convertFunctionExpressionsToArrowFuncs,
NativeSpread,
} from './experiments/modernizeJS';
import { createRemoveUnusedLocalsTransform } from './experiments/removeUnusedLocals';
type TransformOptions = {
prepack: boolean;
@ -69,21 +70,25 @@ export const compileAndTransform = (
Mode.Prod
);
const inlineListFromArrayCalls = createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
);
const [result] = ts.transform(source, [
const {
transformed: [result],
diagnostics,
} = ts.transform(source, [
normalizeVariantShapes,
createFunctionInlineTransformer(reportInlineTransformResult),
inlineListFromArrayCalls,
createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
// InlineMode.UsingLiteralObjects(Mode.Prod)
),
createReplaceUtilsUpdateWithObjectSpread(
NativeSpread.UseSpreadOnlyToMakeACopy
NativeSpread.UseSpreadForUpdateAndOriginalRecord
),
// Arrow functions are disabled because somethings not quite right with them.
convertFunctionExpressionsToArrowFuncs,
]).transformed;
createRemoveUnusedLocalsTransform(),
]);
const printer = ts.createPrinter();

View File

@ -78,23 +78,6 @@ const LIST_CONS_F_NAME = '_List_cons';
const listNil = ts.createIdentifier(LIST_NIL_NAME);
const listConsCall = ts.createIdentifier(LIST_CONS_F_NAME);
const appendToFront = (inlineMode: InlineMode) => (
list: ts.Expression,
element: ts.Expression
): ts.Expression => {
return InlineMode.match(inlineMode, {
UsingConsFunc: (): ts.Expression =>
ts.createCall(listConsCall, undefined, [element, list]),
UsingLiteralObjects: mode =>
ts.createObjectLiteral([
ts.createPropertyAssignment('$', listElementMarker(mode)),
ts.createPropertyAssignment('a', element),
ts.createPropertyAssignment('b', list),
]),
});
};
export const createInlineListFromArrayTransformer = (
inlineMode: InlineMode
): ts.TransformerFactory<ts.SourceFile> => context => {
@ -114,7 +97,22 @@ export const createInlineListFromArrayTransformer = (
// detects _List_fromArray([..])
if (ts.isArrayLiteralExpression(arrayLiteral)) {
return arrayLiteral.elements.reduceRight(
appendToFront(inlineMode),
(list: ts.Expression, element: ts.Expression): ts.Expression => {
return InlineMode.match(inlineMode, {
UsingConsFunc: (): ts.Expression =>
ts.createCall(listConsCall, undefined, [
ts.visitNode(element, visitor),
list,
]),
UsingLiteralObjects: mode =>
ts.createObjectLiteral([
ts.createPropertyAssignment('$', listElementMarker(mode)),
ts.createPropertyAssignment('a', element),
ts.createPropertyAssignment('b', list),
]),
});
},
listNil
);
}

View File

@ -0,0 +1,135 @@
import ts from 'typescript';
export const createRemoveUnusedLocalsTransform = (): ts.TransformerFactory<ts.SourceFile> => context => {
return sourceFile => {
const printer = ts.createPrinter();
const sourceCopy = ts.createSourceFile(
'elm.js',
printer.printFile(sourceFile),
ts.ScriptTarget.ES2018
);
let unused = collectUnusedVariables(sourceCopy);
console.log('found unused:', unused.length);
let removedCount = 0;
const visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
// detects function f(..){..}
if (
ts.isFunctionDeclaration(node) &&
node.name &&
isUnused(unused, node.name.pos, node.name.end)
) {
removedCount++;
return undefined;
}
if (ts.isVariableStatement(node)) {
const declList = node.declarationList;
const filteredDeclarations = declList.declarations.filter(
decl => !isUnused(unused, decl.name.pos, decl.name.end)
);
if (filteredDeclarations.length !== declList.declarations.length) {
if (filteredDeclarations.length === 0) {
// means that there is nothing left, thus delete the entire thing
removedCount += declList.declarations.length;
return undefined;
}
// only update remove some of the declarations
removedCount +=
declList.declarations.length - filteredDeclarations.length;
return ts.updateVariableStatement(
node,
undefined,
ts.updateVariableDeclarationList(declList, filteredDeclarations)
);
}
}
return ts.visitEachChild(node, visitor, context);
};
// TODO make this code pretty
let result = ts.visitNode(sourceCopy, visitor);
unused = collectUnusedVariables(result);
while (unused.length > 0) {
console.log('found unused nextRound:', unused.length);
result = ts.visitNode(result, visitor);
unused = collectUnusedVariables(result);
}
console.log('totalRemoveCount:', removedCount);
return result;
};
};
const defaultCompilerHost = ts.createCompilerHost({});
const cache = new Map<string, ts.SourceFile | undefined>();
function serveLibFile(
name: string,
languageVersion: ts.ScriptTarget
): ts.SourceFile | undefined {
const cached = cache.get(name);
if (cached) return cached;
const val = defaultCompilerHost.getSourceFile(name, languageVersion);
cache.set(name, val);
return val;
}
function collectUnusedVariables(
sourceFile: ts.SourceFile
): readonly ts.Diagnostic[] {
const customCompilerHost: ts.CompilerHost = {
getSourceFile: (name, languageVersion) => {
// console.log(`getSourceFile ${name}`);
if (name === 'elm.js') {
return sourceFile;
} else {
return serveLibFile(name, languageVersion);
}
},
writeFile: () => {},
getDefaultLibFileName: () =>
'node_modules/typescript/lib/lib.es2018.full.d.ts',
useCaseSensitiveFileNames: () => false,
getCanonicalFileName: filename => filename,
getCurrentDirectory: () => '',
getNewLine: () => '\n',
getDirectories: () => [],
fileExists: () => true,
readFile: () => '',
};
const program = ts.createProgram(
['elm.js'],
{
allowJs: true,
noUnusedLocals: true,
checkJs: true,
outDir: 'yo',
},
customCompilerHost
);
const res = ts.getPreEmitDiagnostics(program);
return res.filter(d => d.reportsUnnecessary);
}
function isUnused(
unused: readonly ts.Diagnostic[],
start: number,
end: number
) {
return unused.some(d => {
const dstart = d.start ?? -1;
const dend = dstart + (d.length ?? -2);
return (dstart < end && dend > start) || (start < dend && end > dstart);
});
}

View File

@ -1,119 +1,110 @@
import ts from 'typescript';
import { createCustomTypesTransformer } from './experiments/variantShapes';
import { createFunctionInlineTransformer } from './experiments/inlineWrappedFunctions';
import { Mode, ElmVariant } from './types';
import {
createInlineListFromArrayTransformer,
InlineMode,
} from './experiments/inlineListFromArray';
import { convertFunctionExpressionsToArrowFuncs } from './experiments/modernizeJS';
const filename = 'test.js';
const code = `
(function (){
function f () {}
const f2 = () => 1;
const test = 1 + 2;
console.log(test);
})()
`;
// (function() {
// function f() {}
// const f2 = () => 1;
// const test: number = 1 + 2;
// })();
const elmOutput = `
var $elm$core$Maybe$Nothing = {$: 'Nothing'};
const sourceFile = ts.createSourceFile(filename, code, ts.ScriptTarget.Latest);
var $elm$core$Maybe$Just = function (a) {
return {$: 'Just', a: a};
const defaultCompilerHost = ts.createCompilerHost({});
const customCompilerHost: ts.CompilerHost = {
getSourceFile: (name, languageVersion) => {
console.log(`getSourceFile ${name}`);
if (name === filename) {
return sourceFile;
} else {
return defaultCompilerHost.getSourceFile(name, languageVersion);
}
},
writeFile: () => {},
getDefaultLibFileName: () =>
'node_modules/typescript/lib/lib.es2018.full.d.ts',
useCaseSensitiveFileNames: () => false,
getCanonicalFileName: filename => filename,
getCurrentDirectory: () => '',
getNewLine: () => '\n',
getDirectories: () => [],
fileExists: () => true,
readFile: () => '',
};
var $author$project$Main$Three = F3(
function (a, b, c) {
return {$: 'Three', a: a, b: b, c: c};
});
const program = ts.createProgram(
[filename],
{ allowJs: true, noUnusedLocals: true, outDir: 'yo', checkJs: true },
customCompilerHost
);
var _v1 = A3($author$project$Main$Three, a, b, c);
const diagnostics = ts.getPreEmitDiagnostics(program);
_List_fromArray(['a', 'b', 'c']);
for (const diagnostic of diagnostics) {
const message = diagnostic.messageText;
const file = diagnostic.file;
const filename = file!.fileName;
function _List_Cons(hd, tl) {
return { $: 1, a: hd, b: tl };
const lineAndChar = file!.getLineAndCharacterOfPosition(diagnostic!.start!);
const line = lineAndChar.line + 1;
const character = lineAndChar.character + 1;
console.log(message);
console.log(
`(${filename}:${line}:${character}), pos = (${
diagnostic?.start
},${(diagnostic?.start || 0) + (diagnostic?.length || 0)})`
);
}
var _List_cons = F2(_List_Cons);
console.log('--------A--------------');
const typeChecker = program.getTypeChecker();
var $elm$core$List$cons = _List_cons;
A2($elm$core$List$cons, key, keyList);
$elm$core$String$join_raw("\n\n", A2($elm$core$List$cons, introduction, A2($elm$core$List$indexedMap, $elm$json$Json$Decode$errorOneOf, errors)));
`;
function recursivelyPrintVariableDeclarations(
node: ts.Node,
sourceFile: ts.SourceFile
) {
if (ts.isVariableDeclaration(node)) {
const nodeText = node.getText(sourceFile);
const type = typeChecker.getTypeAtLocation(node);
const typeName = typeChecker.typeToString(type, node);
const source = ts.createSourceFile('elm.js', elmOutput, ts.ScriptTarget.ES2018);
console.log(nodeText);
console.log(`(${typeName})`);
}
const replacements: ElmVariant[] = [
{
jsName: '$elm$core$Maybe$Nothing',
typeName: 'Maybe',
name: 'Nothing',
slots: [],
index: 1,
totalTypeSlotCount: 2,
},
node.forEachChild(child =>
recursivelyPrintVariableDeclarations(child, sourceFile)
);
}
{
jsName: '$elm$core$Maybe$Just',
typeName: 'Maybe',
name: 'Just',
slots: [],
index: 0,
totalTypeSlotCount: 2,
},
{
jsName: '$author$project$Main$Three',
typeName: 'Bla',
name: 'Three',
slots: ['a', 'b', 'c'],
index: 100500,
totalTypeSlotCount: 4,
},
];
recursivelyPrintVariableDeclarations(sourceFile, sourceFile);
const customTypeTransformer = createCustomTypesTransformer(
replacements,
Mode.Prod
);
const [newFile] = ts.transform(source, [customTypeTransformer]).transformed;
console.log('--------B--------------');
function printRecursiveFrom(
node: ts.Node,
indentLevel: number,
sourceFile: ts.SourceFile
) {
const indentation = '-'.repeat(indentLevel);
const syntaxKind = ts.SyntaxKind[node.kind];
const nodeText = node.getText(sourceFile);
console.log(`${indentation}${syntaxKind}: ${nodeText}`);
console.log(`pos: (${node.pos}, ${node.end})`);
const printer = ts.createPrinter();
console.log('----------AFTER CUSTOM TYPE SHAPES TRANSFORM ----------------');
console.log(printer.printFile(source));
console.log(printer.printFile(newFile));
node.forEachChild(child =>
printRecursiveFrom(child, indentLevel + 1, sourceFile)
);
}
console.log('----------AFTER INLINE A(n) TRANSFORM ----------------');
const funcInlineTransformer = createFunctionInlineTransformer();
const [sourceWithInlinedFuntioncs] = ts.transform(newFile, [
funcInlineTransformer,
]).transformed;
console.log(printer.printFile(sourceWithInlinedFuntioncs));
console.log(
'----------AFTER INLINE _List_fromArray TRANSFORM ----------------'
);
const inlineListFromArrayCalls = createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
// InlineMode.UsingConsFunc
);
const [sourceWithInlinedListFromArr] = ts.transform(
sourceWithInlinedFuntioncs,
[inlineListFromArrayCalls]
).transformed;
console.log(printer.printFile(sourceWithInlinedListFromArr));
const funcSource = ts.createSourceFile(
'elm.js',
`
function F3(fun) {
return F(3, fun, function (a) {
return function (b) { return function (c) { return fun(a, b, c); }; };
});
}
`,
ts.ScriptTarget.ES2018
);
console.log('---------- Arrow Transformation ----------------');
const [fRes] = ts.transform(funcSource, [
convertFunctionExpressionsToArrowFuncs,
]).transformed;
console.log(printer.printFile(fRes));
printRecursiveFrom(sourceFile, 0, sourceFile);

View File

@ -0,0 +1,40 @@
import ts from 'typescript';
import { createRemoveUnusedLocalsTransform } from '../src/experiments/removeUnusedLocals';
test('it can process nested calls of A2 with non identifiers as the first arg ', () => {
const initialCode = `
(function (){
function f () {return 2;}
const f2 = () => 1 + f();
const test = 1 + 2, bla="bla";
console.log(test);
})()
`;
const expectedOutputCode = `
(function (){
const test = 1 + 2;
console.log(test);
})()
`;
const source = ts.createSourceFile(
'elm.js',
initialCode,
ts.ScriptTarget.ES2018
);
const printer = ts.createPrinter();
const [output] = ts.transform(source, [
createRemoveUnusedLocalsTransform(),
]).transformed;
const expectedOutput = printer.printFile(
ts.createSourceFile('elm.js', expectedOutputCode, ts.ScriptTarget.ES2018)
);
const printedOutput = printer.printFile(output);
expect(printedOutput).toBe(expectedOutput);
});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff