From 88b4283200fedeb436d34e2be1d72c9eb99408d5 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 6 Jun 2022 05:01:15 +0100 Subject: [PATCH] support `class static` initialization block (#5488) --- lib/ast.js | 23 ++++- lib/compress.js | 76 +++++++++------ lib/output.js | 5 + lib/parse.js | 12 +++ test/compress/classes.js | 202 +++++++++++++++++++++++++++++++++++++++ test/ufuzz/index.js | 17 +++- 6 files changed, 304 insertions(+), 31 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index bc4741fe..8b1c127e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -534,7 +534,7 @@ var AST_With = DEFNODE("With", "expression", { /* -----[ scope and functions ]----- */ var AST_Scope = DEFNODE("Scope", "fn_defs may_call_this uses_eval uses_with", { - $documentation: "Base class for all statements introducing a lexical scope", + $documentation: "Base class for all statements introducing a lambda scope", $propdoc: { uses_eval: "[boolean/S] tells whether this scope contains a direct call to the global `eval`", uses_with: "[boolean/S] tells whether this scope uses the `with` statement", @@ -592,6 +592,10 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); +var AST_ClassInitBlock = DEFNODE("ClassInitBlock", null, { + $documentation: "Value for `class` static initialization blocks", +}, AST_Scope); + var AST_Lambda = DEFNODE("Lambda", "argnames length_read rest safe_ids uses_arguments", { $documentation: "Base class for functions", $propdoc: { @@ -874,7 +878,7 @@ var AST_ClassExpression = DEFNODE("ClassExpression", null, { var AST_ClassProperty = DEFNODE("ClassProperty", "key private static value", { $documentation: "Base class for `class` properties", $propdoc: { - key: "[string|AST_Node] property name (AST_Node for computed property)", + key: "[string|AST_Node?] property name (AST_Node for computed property, null for initialization block)", private: "[boolean] whether this is a private property", static: "[boolean] whether this is a static property", value: "[AST_Node?] property value (AST_Accessor for getters/setters, AST_LambdaExpression for methods, null if not specified for fields)", @@ -888,7 +892,9 @@ var AST_ClassProperty = DEFNODE("ClassProperty", "key private static value", { }, _validate: function() { if (this.TYPE == "ClassProperty") throw new Error("should not instantiate AST_ClassProperty"); - if (typeof this.key != "string") { + if (this instanceof AST_ClassInit) { + if (this.key != null) throw new Error("key must be null"); + } else if (typeof this.key != "string") { if (!(this.key instanceof AST_Node)) throw new Error("key must be string or AST_Node"); must_be_expression(this, "key"); } @@ -928,6 +934,17 @@ var AST_ClassMethod = DEFNODE("ClassMethod", null, { }, }, AST_ClassProperty); +var AST_ClassInit = DEFNODE("ClassInit", null, { + $documentation: "A `class` static initialization block", + _validate: function() { + if (!this.static) throw new Error("static must be true"); + if (!(this.value instanceof AST_ClassInitBlock)) throw new Error("value must be AST_ClassInitBlock"); + }, + initialize: function() { + this.static = true; + }, +}, AST_ClassProperty); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { diff --git a/lib/compress.js b/lib/compress.js index 19bef6d3..4532ea73 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1146,7 +1146,7 @@ Compressor.prototype.compress = function(node) { } } props.forEach(function(prop) { - if (!prop.static || prop instanceof AST_ClassField && prop.value.contains_this()) { + if (!prop.static || is_static_field_or_init(prop) && prop.value.contains_this()) { push(tw); prop.value.walk(tw); pop(tw); @@ -1156,6 +1156,14 @@ Compressor.prototype.compress = function(node) { }); return true; }); + def(AST_ClassInitBlock, function(tw, descend, compressor) { + var node = this; + push(tw); + reset_variables(tw, compressor, node); + descend(); + pop_scope(tw, node); + return true; + }); def(AST_Conditional, function(tw) { this.condition.walk(tw); push(tw); @@ -1843,6 +1851,10 @@ Compressor.prototype.compress = function(node) { || compressor.option("unsafe") && global_names[this.name]; }); + function is_static_field_or_init(prop) { + return prop.static && prop.value && (prop instanceof AST_ClassField || prop instanceof AST_ClassInit); + } + function declarations_only(node) { return all(node.definitions, function(var_def) { return !var_def.value; @@ -1852,8 +1864,7 @@ Compressor.prototype.compress = function(node) { function is_declaration(stat, lexical) { if (stat instanceof AST_DefClass) return lexical && !stat.extends && all(stat.properties, function(prop) { if (prop.key instanceof AST_Node) return false; - if (prop instanceof AST_ClassField && prop.static && prop.value) return false; - return true; + return !is_static_field_or_init(prop); }); if (stat instanceof AST_Definitions) return (lexical || stat instanceof AST_Var) && declarations_only(stat); if (stat instanceof AST_ExportDeclaration) return is_declaration(stat.body, lexical); @@ -6651,7 +6662,7 @@ Compressor.prototype.compress = function(node) { if (prop.key instanceof AST_Node) prop.key.walk(tw); var value = prop.value; if (!value) return; - if (prop instanceof AST_ClassField && prop.static) { + if (is_static_field_or_init(prop)) { if (!used && value.contains_this()) used = true; walk_class_prop(value); } else { @@ -8595,16 +8606,20 @@ Compressor.prototype.compress = function(node) { }); def(AST_ClassExpression, function(compressor, first_in_statement) { var self = this; - var exprs = [], values = []; + var exprs = [], values = [], init = 0; var props = self.properties; for (var i = 0; i < props.length; i++) { var prop = props[i]; if (prop.key instanceof AST_Node) exprs.push(prop.key); - if (prop.static && prop.value - && prop instanceof AST_ClassField - && prop.value.has_side_effects(compressor)) { - if (prop.value.contains_this()) return self; - values.push(prop.value); + if (!is_static_field_or_init(prop)) continue; + var value = prop.value; + if (!value.has_side_effects(compressor)) continue; + if (value.contains_this()) return self; + if (prop instanceof AST_ClassInit) { + init++; + values.push(prop); + } else { + values.push(value); } } var base = self.extends; @@ -8623,33 +8638,40 @@ Compressor.prototype.compress = function(node) { if (base || self.name || !compressor.has_directive("use strict")) { var node = to_class_expr(self); if (!base) node.extends = null; - node.properties = []; - if (values) { - node.properties.push(make_node(AST_ClassField, self, { - static: true, - key: exprs.length ? make_sequence(self, exprs) : "c", - value: make_sequence(self, values), - })); - } else if (exprs.length) { - node.properties.push(make_node(AST_ClassMethod, self, { - key: make_sequence(self, exprs), - value: make_node(AST_Function, self, { - argnames: [], - body: [], - }).init_vars(node), - })); - } + node.properties = values ? values.length == init ? values : [ make_node(AST_ClassField, self, { + static: true, + key: exprs.length ? make_sequence(self, exprs) : "c", + value: make_value(), + }) ] : exprs.length ? [ make_node(AST_ClassMethod, self, { + key: make_sequence(self, exprs), + value: make_node(AST_Function, self, { + argnames: [], + body: [], + }).init_vars(node), + }) ] : []; return node; } if (values) exprs.push(make_node(AST_Call, self, { expression: make_node(AST_Arrow, self, { argnames: [], body: [], - value: make_sequence(self, values), + value: make_value(), }).init_vars(self.parent_scope), args: [], })); return make_sequence(self, exprs); + + function make_value() { + return make_sequence(self, values.map(function(node) { + if (!(node instanceof AST_ClassInit)) return node; + var fn = make_node(AST_Arrow, node, node.value); + fn.argnames = []; + return make_node(AST_Call, node, { + expression: fn, + args: [], + }); + })); + } }); def(AST_Conditional, function(compressor) { var consequent = this.consequent.drop_side_effect_free(compressor); diff --git a/lib/output.js b/lib/output.js index ea233d37..55434c1a 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1257,6 +1257,11 @@ function OutputStream(options) { } print_method(self, output); }); + DEFPRINT(AST_ClassInit, function(output) { + output.print("static"); + output.space(); + print_braced(this.value, output); + }); /* -----[ jumps ]----- */ function print_jump(kind, prop) { diff --git a/lib/parse.js b/lib/parse.js index 67a11ba5..9404e867 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1119,6 +1119,18 @@ function parse($TEXT, options) { })); continue; } + if (fixed && is("punc", "{")) { + props.push(new AST_ClassInit({ + start: start, + value: new AST_ClassInitBlock({ + start: start, + body: block_(), + end: prev(), + }), + end: prev(), + })); + continue; + } var internal = is("name") && /^#/.test(S.token.value); var key = as_property_key(); if (is("punc", "(")) { diff --git a/test/compress/classes.js b/test/compress/classes.js index e42512cb..e019a8f2 100644 --- a/test/compress/classes.js +++ b/test/compress/classes.js @@ -241,6 +241,208 @@ class_super: { node_version: ">=4" } +static_init: { + input: { + var a = "foo"; + var b = null; + class A { + static { + var a = "bar"; + b = true; + var c = 42; + console.log(a, b, c); + } + } + console.log(a, b, typeof c); + } + expect_exact: 'var a="foo";var b=null;class A{static{var a="bar";b=true;var c=42;console.log(a,b,c)}}console.log(a,b,typeof c);' + expect_stdout: [ + "bar true 42", + "foo true undefined", + ] + node_version: ">=16" +} + +static_field_init: { + options = { + side_effects: true, + } + input: { + (class { + static [console.log("foo")] = console.log("bar"); + static { + console.log("baz"); + } + static [console.log("moo")] = console.log("moz"); + }); + } + expect: { + (class { + static [(console.log("foo"), console.log("moo"))] = ( + console.log("bar"), + (() => { + console.log("baz"); + })(), + console.log("moz") + ); + }); + } + expect_stdout: [ + "foo", + "moo", + "bar", + "baz", + "moz", + ] + node_version: ">=16" +} + +static_field_init_strict: { + options = { + side_effects: true, + } + input: { + "use strict"; + (class { + static [console.log("foo")] = console.log("bar"); + static { + console.log("baz"); + } + static [console.log("moo")] = console.log("moz"); + }); + } + expect: { + "use strict"; + console.log("foo"), + console.log("moo"), + (() => ( + console.log("bar"), + (() => { + console.log("baz"); + })(), + console.log("moz") + ))(); + } + expect_stdout: [ + "foo", + "moo", + "bar", + "baz", + "moz", + ] + node_version: ">=16" +} + +static_init_side_effects_1: { + options = { + merge_vars: true, + side_effects: true, + } + input: { + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect: { + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=16" +} + +static_init_side_effects_1_strict: { + options = { + merge_vars: true, + side_effects: true, + } + input: { + "use strict"; + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect: { + "use strict"; + var a = "FAIL"; + (() => (() => { + a = "PASS"; + })())(); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=16" +} + +static_init_side_effects_2: { + options = { + hoist_props: true, + reduce_vars: true, + side_effects: true, + } + input: { + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect: { + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=16" +} + +static_init_side_effects_2_strict: { + options = { + hoist_props: true, + reduce_vars: true, + side_effects: true, + } + input: { + "use strict"; + var a = "FAIL"; + (class { + static { + a = "PASS"; + } + }); + console.log(a); + } + expect: { + "use strict"; + var a = "FAIL"; + (() => (() => { + a = "PASS"; + })())(); + console.log(a); + } + expect_stdout: "PASS" + node_version: ">=16" +} + block_scoped: { options = { evaluate: true, diff --git a/test/ufuzz/index.js b/test/ufuzz/index.js index 396a5fe0..fd11005a 100644 --- a/test/ufuzz/index.js +++ b/test/ufuzz/index.js @@ -140,6 +140,7 @@ var SUPPORT = function(matrix) { class: "class C { f() {} }", class_field: "class C { p = 0; }", class_private: "class C { #f() {} }", + class_static_init: "class C { static {} }", computed_key: "({[0]: 0});", const_block: "var a; { const a = 0; }", default_value: "[ a = 0 ] = [];", @@ -1181,7 +1182,11 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn unique_vars.length = unique_len; }); } - if (n !== 0) s += " finally { " + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + " }"; + if (n !== 0) s += [ + " finally { ", + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), + " }", + ].join(""); return s; case STMT_C: return "c = c + 1;"; @@ -1839,6 +1844,16 @@ function createClassLiteral(recurmax, stmtDepth, canThrow, name) { async = save_async; } s += ";\n"; + } else if (SUPPORT.class_static_init && fixed && !internal && rng(10) == 0) { + async = false; + generator = false; + s += [ + "{ ", + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, stmtDepth), + " }\n", + ].join(""); + generator = save_generator; + async = save_async; } else { if (!fixed && !internal && constructor && rng(10) == 0) { internal = constructor;