mirror of
https://github.com/mdgriffith/elm-optimize-level-2.git
synced 2024-08-16 15:40:49 +03:00
initial version of using ts compiler to remove unused locals
This commit is contained in:
parent
6d9d6ff34b
commit
cd443ce10d
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
135
src/experiments/removeUnusedLocals.ts
Normal file
135
src/experiments/removeUnusedLocals.ts
Normal 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);
|
||||
});
|
||||
}
|
193
src/index.ts
193
src/index.ts
@ -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);
|
||||
|
40
test/removedUnusedLocals.test.ts
Normal file
40
test/removedUnusedLocals.test.ts
Normal 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
Loading…
Reference in New Issue
Block a user