First class block scope

- Make let, const, and class symbols be declared in a block scope.
- Piggy back on existing catch symbol implementation to get block-aware mangling working
- Make sure unused block-scoped declarations can be dropped
- Don't eliminate a block if it has a block-scoped declaration
- Remove silly empty anonymous blocks left over from drop_unused
- AST_Toplevel now gets to call drop_unused too, since block-scoped variables aren't global!
- Don't consider block declarations global
This commit is contained in:
Fábio Santos
2016-02-28 14:06:51 +00:00
committed by Richard van Velzen
parent 6702cae918
commit 634f231b78
7 changed files with 266 additions and 30 deletions

View File

@@ -282,9 +282,10 @@ var AST_With = DEFNODE("With", "expression", {
/* -----[ scope and functions ]----- */
var AST_Scope = DEFNODE("Scope", "directives variables functions uses_with uses_eval parent_scope enclosed cname", {
var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions uses_with uses_eval parent_scope enclosed cname", {
$documentation: "Base class for all statements introducing a lexical scope",
$propdoc: {
is_block_scope: "[boolean] identifies a block scope",
directives: "[string*/S] an array of directives declared in this scope",
variables: "[Object/S] a map of name -> SymbolDef for all variables/functions defined in this scope",
functions: "[Object/S] like `variables`, but only lists function declarations",
@@ -1077,9 +1078,17 @@ var AST_SymbolVar = DEFNODE("SymbolVar", null, {
$documentation: "Symbol defining a variable",
}, AST_SymbolDeclaration);
var AST_SymbolBlockDeclaration = DEFNODE("SymbolBlockDeclaration", null, {
$documentation: "Base class for block-scoped declaration symbols"
}, AST_SymbolDeclaration);
var AST_SymbolConst = DEFNODE("SymbolConst", null, {
$documentation: "A constant declaration"
}, AST_SymbolDeclaration);
}, AST_SymbolBlockDeclaration);
var AST_SymbolLet = DEFNODE("SymbolLet", null, {
$documentation: "A block-scoped `let` declaration"
}, AST_SymbolBlockDeclaration);
var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, {
$documentation: "Symbol naming a function argument",
@@ -1099,7 +1108,7 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, {
var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, {
$documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class."
}, AST_SymbolDeclaration);
}, AST_SymbolBlockDeclaration);
var AST_SymbolClass = DEFNODE("SymbolClass", null, {
$documentation: "Symbol naming a class's name. Lexically scoped to the class."
@@ -1107,11 +1116,11 @@ var AST_SymbolClass = DEFNODE("SymbolClass", null, {
var AST_SymbolCatch = DEFNODE("SymbolCatch", null, {
$documentation: "Symbol naming the exception in catch",
}, AST_SymbolDeclaration);
}, AST_SymbolBlockDeclaration);
var AST_SymbolImport = DEFNODE("SymbolImport", null, {
$documentation: "Symbol refering to an imported name",
}, AST_SymbolDeclaration);
}, AST_SymbolBlockDeclaration);
var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, {
$documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes",

View File

@@ -190,6 +190,14 @@ merge(Compressor.prototype, {
return false;
};
function can_be_evicted_from_block(node) {
return !(
node instanceof AST_DefClass ||
node instanceof AST_Let ||
node instanceof AST_Const
);
}
function loop_body(x) {
if (x instanceof AST_Switch) return x;
if (x instanceof AST_For || x instanceof AST_ForIn || x instanceof AST_DWLoop) {
@@ -311,7 +319,7 @@ merge(Compressor.prototype, {
function eliminate_spurious_blocks(statements) {
var seen_dirs = [];
return statements.reduce(function(a, stat){
if (stat instanceof AST_BlockStatement) {
if (stat instanceof AST_BlockStatement && all(stat.body, can_be_evicted_from_block)) {
CHANGED = true;
a.push.apply(a, eliminate_spurious_blocks(stat.body));
} else if (stat instanceof AST_EmptyStatement) {
@@ -633,7 +641,7 @@ merge(Compressor.prototype, {
function extract_declarations_from_unreachable_code(compressor, stat, target) {
compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start);
stat.walk(new TreeWalker(function(node){
if (node instanceof AST_Definitions) {
if (node instanceof AST_Var) {
compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start);
node.remove_initializers();
target.push(node);
@@ -957,6 +965,8 @@ merge(Compressor.prototype, {
});
def(AST_Defun, function(compressor){ return true });
def(AST_Function, function(compressor){ return false });
def(AST_Class, function(compressor){ return false });
def(AST_DefClass, function(compressor){ return true });
def(AST_Binary, function(compressor){
return this.left.has_side_effects(compressor)
|| this.right.has_side_effects(compressor);
@@ -1067,7 +1077,11 @@ merge(Compressor.prototype, {
OPT(AST_BlockStatement, function(self, compressor){
self.body = tighten_body(self.body, compressor);
switch (self.body.length) {
case 1: return self.body[0];
case 1:
if (can_be_evicted_from_block(self.body[0])) {
return self.body[0];
}
break;
case 0: return make_node(AST_EmptyStatement, self);
}
return self;
@@ -1077,7 +1091,6 @@ merge(Compressor.prototype, {
var self = this;
if (compressor.has_directive("use asm")) return self;
if (compressor.option("unused")
&& !(self instanceof AST_Toplevel)
&& !self.uses_eval
) {
var in_use = [];
@@ -1166,8 +1179,11 @@ merge(Compressor.prototype, {
}
}
}
if (node instanceof AST_Defun && node !== self) {
if (!member(node.name.definition(), in_use)) {
if ((node instanceof AST_Defun || node instanceof AST_DefClass) && node !== self) {
var keep =
member(node.name.definition(), in_use) ||
node.name.definition().global;
if (!keep) {
compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", {
name : node.name.name,
file : node.name.start.file,
@@ -1182,6 +1198,7 @@ merge(Compressor.prototype, {
var def = node.definitions.filter(function(def){
if (def.is_destructuring()) return true;
if (member(def.name.definition(), in_use)) return true;
if (def.name.definition().global) return true;
var w = {
name : def.name.name,
file : def.name.start.file,
@@ -1260,6 +1277,12 @@ merge(Compressor.prototype, {
});
}
}
if (node instanceof AST_BlockStatement) {
descend(node, this);
if (in_list && all(node.body, can_be_evicted_from_block)) {
return MAP.splice(node.body);
}
}
if (node instanceof AST_Scope && node !== self)
return node;
}

View File

@@ -1214,11 +1214,14 @@ function parse($TEXT, options) {
});
};
function vardefs(no_in, in_const) {
function vardefs(no_in, kind) {
var a = [];
var def;
for (;;) {
var sym_type = in_const ? AST_SymbolConst : AST_SymbolVar;
var sym_type =
kind === "var" ? AST_SymbolVar :
kind === "const" ? AST_SymbolConst :
kind === "let" ? AST_SymbolLet : null;
if (is("punc", "{") || is("punc", "[")) {
def = new AST_VarDef({
start: S.token,
@@ -1287,7 +1290,7 @@ function parse($TEXT, options) {
var var_ = function(no_in) {
return new AST_Var({
start : prev(),
definitions : vardefs(no_in, false),
definitions : vardefs(no_in, "var"),
end : prev()
});
};
@@ -1295,7 +1298,7 @@ function parse($TEXT, options) {
var let_ = function(no_in) {
return new AST_Let({
start : prev(),
definitions : vardefs(no_in, false),
definitions : vardefs(no_in, "let"),
end : prev()
});
};
@@ -1303,7 +1306,7 @@ function parse($TEXT, options) {
var const_ = function() {
return new AST_Const({
start : prev(),
definitions : vardefs(false, true),
definitions : vardefs(false, "const"),
end : prev()
});
};

View File

@@ -106,11 +106,15 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
var in_destructuring = null;
var in_export;
var tw = new TreeWalker(function(node, descend){
if (options.screw_ie8 && node instanceof AST_Catch) {
var create_a_block_scope =
(options.screw_ie8 && node instanceof AST_Catch) ||
((node instanceof AST_Block) && node.creates_block_scope());
if (create_a_block_scope) {
var save_scope = scope;
scope = new AST_Scope(node);
scope.init_scope_vars(nesting);
scope.parent_scope = save_scope;
scope.is_block_scope = true;
descend();
scope = save_scope;
return true;
@@ -174,7 +178,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
// scope when we encounter the AST_Defun node (which is
// instanceof AST_Scope) but we get to the symbol a bit
// later.
(node.scope = defun.parent_scope).def_function(node, in_export);
var parent_lambda = defun.parent_scope;
while (parent_lambda.is_block_scope) {
parent_lambda = parent_lambda.parent_scope;
}
(node.scope = parent_lambda).def_function(node, in_export);
}
else if (node instanceof AST_SymbolClass) {
defun.def_variable(node, in_export);
@@ -188,8 +196,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){
(node.scope = defun.parent_scope).def_function(node, in_export);
}
else if (node instanceof AST_SymbolVar
|| node instanceof AST_SymbolConst) {
var def = defun.def_variable(node, in_export);
|| node instanceof AST_SymbolConst
|| node instanceof AST_SymbolLet) {
var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node, in_export);
def.constant = node instanceof AST_SymbolConst;
def.destructuring = in_destructuring;
def.init = tw.parent().value;
@@ -279,6 +288,14 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){
this.nesting = nesting; // the nesting level of this scope (0 means toplevel)
});
AST_Block.DEFMETHOD("creates_block_scope", function() {
return (
!(this instanceof AST_Lambda) &&
!(this instanceof AST_Toplevel) &&
!(this instanceof AST_Class)
);
});
AST_Lambda.DEFMETHOD("init_scope_vars", function(){
AST_Scope.prototype.init_scope_vars.apply(this, arguments);
this.uses_arguments = false;
@@ -312,17 +329,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){
def = new SymbolDef(this, this.variables.size(), symbol);
this.variables.set(symbol.name, def);
def.object_destructuring_arg = symbol.object_destructuring_arg;
def.global = !this.parent_scope;
if (symbol instanceof AST_SymbolImport) {
// Imports are not global
def.global = false;
// TODO The real fix comes with block scoping being first class in uglifyJS,
// enabling import definitions to behave like module-level let declarations
}
if (!this.parent_scope && in_export) {
def.global = false;
if (in_export) {
def.export = true;
}
def.global = !this.parent_scope && !(symbol instanceof AST_SymbolBlockDeclaration);
} else {
def = this.variables.get(symbol.name);
def.orig.push(symbol);
@@ -466,7 +476,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){
node.mangled_name = name;
return true;
}
if (options.screw_ie8 && node instanceof AST_SymbolCatch) {
var mangle_with_block_scope =
(options.screw_ie8 && node instanceof AST_SymbolCatch) ||
node instanceof AST_SymbolBlockDeclaration;
if (mangle_with_block_scope) {
to_mangle.push(node.definition());
return;
}

View File

@@ -31,3 +31,103 @@ do_not_hoist_let: {
}
}
do_not_remove_anon_blocks_if_they_have_decls: {
input: {
function x() {
{
let x;
}
{
var x;
}
{
const y;
class Zee {};
}
}
{
let y;
}
{
var y;
}
}
expect: {
function x(){
{
let x
}
var x;
{
const y;
class Zee {}
}
}
{
let y
}
var y;
}
}
remove_unused_in_global_block: {
options = {
unused: true,
}
input: {
{
let x;
const y;
class Zee {};
var w;
}
let ex;
const why;
class Zed {};
var wut;
console.log(x, y, Zee);
}
expect: {
var w;
var wut;
console.log(x, y, Zee);
}
}
regression_block_scope_resolves: {
mangle = { };
options = {
dead_code: false
};
input: {
(function () {
if(1) {
let x;
const y;
class Zee {};
}
if(1) {
let ex;
const why;
class Zi {};
}
console.log(x, y, Zee, ex, why, Zi);
}());
}
expect: {
(function () {
if (1) {
let a;
const b;
class c {};
}
if (1) {
let a;
const b;
class c {};
}
console.log(x, y, Zee, ex, why, Zi);
}());
}
}

View File

@@ -87,3 +87,25 @@ dead_code_constant_boolean_should_warn_more: {
var moo;
}
}
dead_code_block_decls_die: {
options = {
dead_code : true,
conditionals : true,
booleans : true,
evaluate : true
};
input: {
if (0) {
let foo = 6;
const bar = 12;
class Baz {};
var qux;
}
console.log(foo, bar, Baz);
}
expect: {
var qux;
console.log(foo, bar, Baz);
}
}

View File

@@ -164,6 +164,72 @@ used_var_in_catch: {
}
}
unused_block_decls_in_catch: {
options = { unused: true };
input: {
function foo() {
try {
foo();
} catch(ex) {
let x = 10;
const y = 10;
class Zee {};
}
}
}
expect: {
function foo() {
try {
foo();
} catch(ex) {}
}
}
}
used_block_decls_in_catch: {
options = { unused: true };
input: {
function foo() {
try {
foo();
} catch(ex) {
let x = 10;
const y = 10;
class Zee {};
}
console.log(x, y, Zee);
}
}
expect: {
function foo() {
try {
foo();
} catch(ex) {}
console.log(x, y, Zee);
}
}
}
unused_block_decls: {
options = { unused: true };
input: {
function foo() {
{
const x;
}
{
let y;
}
console.log(x, y);
}
}
expect: {
function foo() {
console.log(x, y);
}
}
}
unused_keep_harmony_destructuring: {
options = { unused: true };
input: {