Skip to content

Commit 9109691

Browse files
committed
descend into basic control structures
1 parent faa345e commit 9109691

File tree

5 files changed

+156
-58
lines changed

5 files changed

+156
-58
lines changed

lib/extractor.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,18 @@ export class StaticExtractor {
315315
return this.addCodeToStatic({ node: c, analysis });
316316
}
317317

318-
liftExpression(e: acorn.Expression) {
318+
/**
319+
* Lifts the given expression.
320+
* If any of the variables used in `dirty` are used (r/rw), skip.
321+
*/
322+
liftExpression(e: acorn.Expression, dirty: string[] = []) {
319323
const analysis = analyzeBlock(createBlock(createExpressionStatement(e)));
324+
for (const d of dirty) {
325+
if (analysis.vars.has(d)) {
326+
return null;
327+
}
328+
}
329+
320330
return this.addCodeToStatic({ node: e, analysis, var: true });
321331
}
322332

lib/internal/analyze/block.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export type AnalyzeBlock = {
189189
hasNested: boolean;
190190
};
191191

192-
export function analyzeBlock(b: acorn.BlockStatement): AnalyzeBlock {
192+
export function analyzeBlock(b: acorn.BlockStatement, args?: { nest?: boolean }): AnalyzeBlock {
193193
const out: AnalyzeBlock = { vars: new Map(), hasNested: false };
194194
const mark: MarkIdentifierFn = (name, { nested, writes }) => {
195195
if (name === '') {
@@ -220,6 +220,10 @@ export function analyzeBlock(b: acorn.BlockStatement): AnalyzeBlock {
220220
const simple = reductifyStatement(raw) ?? { type: 'EmptyStatement' };
221221
switch (simple.type) {
222222
case 'BlockStatement': {
223+
if (args?.nest === false) {
224+
break; // we can skip descending in some cases
225+
}
226+
223227
const inner = analyzeBlock(simple);
224228

225229
for (const [key, info] of inner.vars) {

lib/internal/analyze/expression.ts

+22-16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ function createIife(body: acorn.Statement[]): acorn.CallExpression {
3838
};
3939
}
4040

41+
export function patternsToDeclaration(...p: acorn.Pattern[]): acorn.VariableDeclaration {
42+
const decl: acorn.VariableDeclaration = {
43+
type: 'VariableDeclaration',
44+
start: -1,
45+
end: -1,
46+
kind: 'var',
47+
declarations: p.map((id): acorn.VariableDeclarator => {
48+
return {
49+
type: 'VariableDeclarator',
50+
start: id.start,
51+
end: id.end,
52+
id,
53+
};
54+
}),
55+
};
56+
return decl;
57+
}
58+
59+
export function namesFromDeclaration(d: acorn.VariableDeclaration) {}
60+
4161
/**
4262
* Returns the given "class" as a number of simple component parts.
4363
* This can't be used or run but is the same from an analysis point of view.
@@ -84,7 +104,7 @@ function reductifyClassParts(c: acorn.Class): acorn.Expression {
84104
return e.length === 1 ? e[0] : createSequenceExpression(...e);
85105
}
86106

87-
function reductifyFunction(f: acorn.Function): acorn.BlockStatement {
107+
export function reductifyFunction(f: acorn.Function): acorn.BlockStatement {
88108
const body: acorn.Statement[] = [];
89109

90110
// our own function name becomes something we can reference
@@ -107,21 +127,7 @@ function reductifyFunction(f: acorn.Function): acorn.BlockStatement {
107127
}
108128

109129
if (f.params.length) {
110-
const decl: acorn.VariableDeclaration = {
111-
type: 'VariableDeclaration',
112-
start: -1,
113-
end: -1,
114-
kind: 'var',
115-
declarations: f.params.map((id): acorn.VariableDeclarator => {
116-
return {
117-
type: 'VariableDeclarator',
118-
start: id.start,
119-
end: id.end,
120-
id,
121-
};
122-
}),
123-
};
124-
body.push(decl);
130+
body.push(patternsToDeclaration(...f.params));
125131
} else if (f.body.type === 'BlockStatement') {
126132
return f.body;
127133
}

lib/lift.ts

+105-40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { StaticExtractor } from './extractor.ts';
22
import type * as acorn from 'acorn';
3+
import { analyzeBlock } from './internal/analyze/block.ts';
4+
import { reductifyFunction } from './internal/analyze/expression.ts';
5+
import { createBlock } from './internal/analyze/helper.ts';
36

47
export function liftDefault(e: StaticExtractor, minSize: number) {
58
const stats = {
@@ -10,6 +13,106 @@ export function liftDefault(e: StaticExtractor, minSize: number) {
1013
_skip: 0,
1114
};
1215

16+
// lift a subpath of a complex statement
17+
const innerLiftMaybeBlock = (b: acorn.Statement | null | undefined, dirty: string[]) => {
18+
if (!b) {
19+
return;
20+
}
21+
if (b.type !== 'BlockStatement') {
22+
if (b.type === 'VariableDeclaration') {
23+
// only valid thing is 'if (1) var x = ...', nope nope nope
24+
return;
25+
}
26+
b = createBlock(b); // treat as miniblock
27+
} else {
28+
const a = analyzeBlock(b, { nest: false });
29+
const declaredHere: string[] = [];
30+
a.vars.forEach((info, key) => {
31+
if (info.local?.kind) {
32+
declaredHere.push(key);
33+
}
34+
});
35+
dirty = dirty.concat(declaredHere);
36+
}
37+
38+
innerLift(b, dirty);
39+
};
40+
41+
const innerLift = (b: acorn.BlockStatement, dirty: string[]) => {
42+
const maybeLift = (expr: acorn.Expression | null | undefined, ok: () => void) => {
43+
if (!expr) {
44+
return;
45+
}
46+
const size = expr.end - expr.start;
47+
if (size < minSize) {
48+
return;
49+
}
50+
if (e.liftExpression(expr, dirty)) {
51+
ok();
52+
} else {
53+
++stats._skip;
54+
}
55+
};
56+
57+
for (const part of b.body) {
58+
switch (part.type) {
59+
case 'ExpressionStatement':
60+
if (
61+
part.expression.type === 'CallExpression' &&
62+
(part.expression.callee.type === 'FunctionExpression' ||
63+
part.expression.callee.type === 'ArrowFunctionExpression')
64+
) {
65+
// IIFE
66+
const r = reductifyFunction(part.expression.callee);
67+
innerLiftMaybeBlock(r, dirty);
68+
} else if (part.expression.type === 'AssignmentExpression') {
69+
// find things on the right of "="
70+
// this won't lift normally (the left part changes)
71+
maybeLift(part.expression.right, () => stats.assignment++);
72+
} else {
73+
// try the whole thing? :shrug:
74+
maybeLift(part.expression, () => stats.expr++);
75+
}
76+
continue;
77+
78+
case 'VariableDeclaration':
79+
for (const decl of part.declarations) {
80+
maybeLift(decl.init, () => stats.assignment++);
81+
}
82+
continue;
83+
84+
case 'ReturnStatement':
85+
// why not? might be big
86+
maybeLift(part.argument, () => stats.expr++);
87+
continue;
88+
89+
// -- nested control statements below here
90+
91+
case 'WhileStatement':
92+
case 'DoWhileStatement':
93+
innerLiftMaybeBlock(part.body, dirty);
94+
break;
95+
96+
case 'IfStatement':
97+
innerLiftMaybeBlock(part.consequent, dirty);
98+
innerLiftMaybeBlock(part.alternate, dirty);
99+
break;
100+
101+
case 'BlockStatement':
102+
innerLiftMaybeBlock(part, dirty);
103+
break;
104+
105+
case 'TryStatement':
106+
innerLiftMaybeBlock(part.block, dirty);
107+
// TODO: include handler (maybe declares var)
108+
innerLiftMaybeBlock(part.finalizer, dirty);
109+
break;
110+
111+
// TODO: include for/etc which can declare vars
112+
}
113+
}
114+
};
115+
13116
// lift top-level fn blocks
14117
for (const part of e.block.body) {
15118
const size = part.end - part.start;
@@ -37,46 +140,8 @@ export function liftDefault(e: StaticExtractor, minSize: number) {
37140
}
38141
}
39142

40-
// lift expressions in a few places (all top-level though?)
41-
const maybeLift = (expr: acorn.Expression | null | undefined, ok: () => void) => {
42-
if (!expr) {
43-
return;
44-
}
45-
const size = expr.end - expr.start;
46-
if (size < minSize) {
47-
return;
48-
}
49-
if (e.liftExpression(expr)) {
50-
ok();
51-
} else {
52-
++stats._skip;
53-
}
54-
};
55-
for (const part of e.block.body) {
56-
switch (part.type) {
57-
case 'ExpressionStatement':
58-
if (part.expression.type === 'AssignmentExpression') {
59-
// find things on the right of "="
60-
// this won't lift normally (the left part changes)
61-
maybeLift(part.expression.right, () => stats.assignment++);
62-
} else {
63-
// try the whole thing? :shrug:
64-
maybeLift(part.expression, () => stats.expr++);
65-
}
66-
continue;
67-
68-
case 'VariableDeclaration':
69-
for (const decl of part.declarations) {
70-
maybeLift(decl.init, () => stats.assignment++);
71-
}
72-
continue;
73-
74-
case 'ReturnStatement':
75-
// why not? might be big
76-
maybeLift(part.argument, () => stats.expr++);
77-
continue;
78-
}
79-
}
143+
// follow top-level statements
144+
innerLift(e.block, []);
80145

81146
return stats;
82147
}

test/cases/iife.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
(function (q) {
2+
console.info('Kuto should extract this long statement', q);
3+
something_long_call(123, 'hello there long');
4+
foo.bar(q);
5+
})();
6+
7+
if (1) {
8+
var x = 123;
9+
console.info('long statement that uses inner var', x);
10+
console.info('long statement that is otherwise boring AF');
11+
}
12+
13+
if (1) console.info('long thing that can be yeetyed');

0 commit comments

Comments
 (0)