feat(transformer): add utils to make logical_assignment_operators pass (#1017)

This commit is contained in:
Boshen 2023-10-20 16:27:23 +08:00 committed by GitHub
parent 3f06335172
commit dfee8539f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 340 additions and 95 deletions

View file

@ -0,0 +1,82 @@
use crate::ast::*;
use oxc_span::Atom;
// TODO: <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L61>
pub trait GatherNodeParts {
fn gather<F: FnMut(Atom)>(&self, f: &mut F);
}
impl<'a> GatherNodeParts for Expression<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
match self {
Self::Identifier(ident) => f(ident.name.clone()),
Self::MemberExpression(expr) => expr.gather(f),
Self::AssignmentExpression(expr) => expr.left.gather(f),
Self::UpdateExpression(expr) => expr.argument.gather(f),
Self::StringLiteral(lit) => lit.gather(f),
_ => f(Atom::from("ref")),
}
}
}
impl<'a> GatherNodeParts for MemberExpression<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
match self {
MemberExpression::ComputedMemberExpression(expr) => {
expr.object.gather(f);
expr.expression.gather(f);
}
MemberExpression::StaticMemberExpression(expr) => {
expr.object.gather(f);
expr.property.gather(f);
}
MemberExpression::PrivateFieldExpression(expr) => {
expr.object.gather(f);
expr.field.gather(f);
}
}
}
}
impl<'a> GatherNodeParts for AssignmentTarget<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
match self {
AssignmentTarget::SimpleAssignmentTarget(t) => t.gather(f),
AssignmentTarget::AssignmentTargetPattern(_) => {}
}
}
}
impl<'a> GatherNodeParts for SimpleAssignmentTarget<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
match self {
Self::AssignmentTargetIdentifier(ident) => ident.gather(f),
Self::MemberAssignmentTarget(expr) => expr.gather(f),
_ => {}
}
}
}
impl GatherNodeParts for IdentifierReference {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.name.clone());
}
}
impl GatherNodeParts for IdentifierName {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.name.clone());
}
}
impl GatherNodeParts for PrivateIdentifier {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.name.clone());
}
}
impl GatherNodeParts for StringLiteral {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.value.clone());
}
}

View file

@ -1,11 +1,13 @@
//! [ECMA262 Syntax-Directed Operations](https://tc39.es/ecma262/#sec-syntax-directed-operations)
mod bound_names;
mod gather_node_parts;
mod is_simple_parameter_list;
mod private_bound_identifiers;
mod prop_name;
pub use self::{
bound_names::BoundNames, is_simple_parameter_list::IsSimpleParameterList,
bound_names::BoundNames, gather_node_parts::GatherNodeParts,
is_simple_parameter_list::IsSimpleParameterList,
private_bound_identifiers::PrivateBoundIdentifiers, prop_name::PropName,
};

View file

@ -1182,9 +1182,12 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for ArrayExpression<'a> {
impl<'a, const MINIFY: bool> GenExpr<MINIFY> for ObjectExpression<'a> {
fn gen_expr(&self, p: &mut Codegen<{ MINIFY }>, _precedence: Precedence, ctx: Context) {
let n = p.code_len();
let is_multi_line = !self.properties.is_empty();
p.wrap(p.start_of_stmt == n || p.start_of_arrow_expr == n, |p| {
p.print(b'{');
p.indent();
if is_multi_line {
p.indent();
}
for (i, item) in self.properties.iter().enumerate() {
if i != 0 {
p.print_comma();
@ -1193,9 +1196,11 @@ impl<'a, const MINIFY: bool> GenExpr<MINIFY> for ObjectExpression<'a> {
p.print_indent();
item.gen(p, ctx);
}
p.print_soft_newline();
p.dedent();
p.print_indent();
if is_multi_line {
p.print_soft_newline();
p.dedent();
p.print_indent();
}
p.print(b'}');
});
}

View file

@ -1,6 +1,7 @@
use std::hash::BuildHasherDefault;
use indexmap::IndexMap;
use oxc_ast::{ast::Expression, syntax_directed_operations::GatherNodeParts};
use oxc_index::IndexVec;
use oxc_span::Atom;
pub use oxc_syntax::scope::{ScopeFlags, ScopeId};
@ -70,6 +71,10 @@ impl ScopeTree {
self.get_binding(self.root_scope_id(), name)
}
pub fn has_binding(&self, scope_id: ScopeId, name: &Atom) -> bool {
self.bindings[scope_id].get(name).is_some()
}
pub fn get_binding(&self, scope_id: ScopeId, name: &Atom) -> Option<SymbolId> {
self.bindings[scope_id].get(name).copied()
}
@ -96,7 +101,7 @@ impl ScopeTree {
scope_id
}
pub(crate) fn add_binding(&mut self, scope_id: ScopeId, name: Atom, symbol_id: SymbolId) {
pub fn add_binding(&mut self, scope_id: ScopeId, name: Atom, symbol_id: SymbolId) {
self.bindings[scope_id].insert(name, symbol_id);
}
@ -124,4 +129,24 @@ impl ScopeTree {
) -> &mut UnresolvedReferences {
&mut self.unresolved_references[scope_id]
}
// TODO:
// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L543>
pub fn generate_uid_based_on_node(&self, expr: &Expression) -> Atom {
let mut parts = std::vec::Vec::with_capacity(1);
expr.gather(&mut |part| parts.push(part));
let name = parts.join("$");
let name = name.trim_start_matches('_');
for i in 0.. {
let name = Self::generate_uid(name, i);
if !self.has_binding(ScopeId::new(0), &name) {
return name;
}
}
unreachable!()
}
fn generate_uid(name: &str, i: i32) -> Atom {
Atom::from(if i > 1 { format!("_{name}{i}") } else { format!("_{name}") })
}
}

View file

@ -127,10 +127,13 @@ impl SymbolTable {
pub fn is_static(&self, expr: &Expression) -> bool {
match expr {
Expression::ThisExpression(_) | Expression::Super(_) => true,
Expression::Identifier(ident)
if ident.reference_id.get().is_some_and(|id| self.has_binding(id)) =>
{
true
Expression::Identifier(ident) => {
ident.reference_id.get().map_or(false, |reference_id| {
self.get_reference(reference_id).symbol_id().map_or_else(
|| self.has_binding(reference_id),
|symbol_id| self.get_resolved_references(symbol_id).all(|r| !r.is_write()),
)
})
}
_ => false,
}

View file

@ -1,7 +1,11 @@
use std::{cell::RefCell, rc::Rc};
use std::{
cell::{Ref, RefCell},
rc::Rc,
};
use oxc_ast::AstBuilder;
use oxc_semantic::{ScopeTree, SymbolTable};
use oxc_semantic::{ScopeId, ScopeTree, SymbolId, SymbolTable};
use oxc_span::Atom;
#[derive(Clone)]
pub struct TransformerCtx<'a> {
@ -9,3 +13,18 @@ pub struct TransformerCtx<'a> {
pub symbols: Rc<RefCell<SymbolTable>>,
pub scopes: Rc<RefCell<ScopeTree>>,
}
impl<'a> TransformerCtx<'a> {
pub fn symbols(&self) -> Ref<SymbolTable> {
self.symbols.borrow()
}
pub fn scopes(&self) -> Ref<ScopeTree> {
self.scopes.borrow()
}
pub fn add_binding(&self, name: Atom) {
// TODO: use the correct scope and symbol id
self.scopes.borrow_mut().add_binding(ScopeId::new(0), name, SymbolId::new(0));
}
}

View file

@ -228,9 +228,8 @@ impl<'a> ExponentiationOperator<'a> {
expr: Expression<'a>,
nodes: &mut Vec<'a, Expression<'a>>,
) -> Expression<'a> {
let name = self.create_new_var(&expr);
let ident = self.create_new_var(&expr);
// Add new reference `_name = name` to nodes
let ident = IdentifierReference::new(Span::default(), name);
let target = self.ast.simple_assignment_target_identifier(ident.clone());
let target = AssignmentTarget::SimpleAssignmentTarget(target);
let op = AssignmentOperator::Assign;
@ -251,7 +250,7 @@ fn test() {
let tests = &[(
"let x = {}; let y = 0; let z = 0; x[z++] **= y;",
"var _ref; let x = {}; let y = 0; let z = 0; _ref = z++,x[_ref] = Math.pow(x[_ref], y);",
"var _z; let x = {}; let y = 0; let z = 0; _z = z++,x[_z] = Math.pow(x[_z], y);",
)];
Tester::new("test.js", options).test(tests);

View file

@ -30,6 +30,7 @@ pub struct NullishCoalescingOperator<'a> {
ast: Rc<AstBuilder<'a>>,
ctx: TransformerCtx<'a>,
vars: Vec<'a, VariableDeclarator<'a>>,
}
@ -70,12 +71,11 @@ impl<'a> NullishCoalescingOperator<'a> {
let assignment;
// skip creating extra reference when `left` is static
if self.ctx.symbols.borrow().is_static(&logical_expr.left) {
if self.ctx.symbols().is_static(&logical_expr.left) {
reference = self.ast.copy(&logical_expr.left);
assignment = self.ast.copy(&logical_expr.left);
} else {
let name = self.create_new_var(&logical_expr.left);
let ident = IdentifierReference::new(span, name);
let ident = self.create_new_var(&logical_expr.left);
reference = self.ast.identifier_reference_expression(ident.clone());
let left = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(ident),

View file

@ -1,10 +1,15 @@
use std::rc::Rc;
use oxc_allocator::Vec;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, LogicalOperator};
use crate::options::{TransformOptions, TransformTarget};
use crate::{
context::TransformerCtx,
options::{TransformOptions, TransformTarget},
utils::CreateVars,
};
/// ES2021: Logical Assignment Operators
///
@ -13,17 +18,39 @@ use crate::options::{TransformOptions, TransformTarget};
/// * <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-logical-assignment-operators>
pub struct LogicalAssignmentOperators<'a> {
ast: Rc<AstBuilder<'a>>,
ctx: TransformerCtx<'a>,
vars: Vec<'a, VariableDeclarator<'a>>,
}
impl<'a> CreateVars<'a> for LogicalAssignmentOperators<'a> {
fn ctx(&self) -> &TransformerCtx<'a> {
&self.ctx
}
fn vars_mut(&mut self) -> &mut Vec<'a, VariableDeclarator<'a>> {
&mut self.vars
}
}
impl<'a> LogicalAssignmentOperators<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(options.target < TransformTarget::ES2021 || options.logical_assignment_operators)
.then(|| Self { ast })
pub fn new(
ast: Rc<AstBuilder<'a>>,
ctx: TransformerCtx<'a>,
options: &TransformOptions,
) -> Option<Self> {
(options.target < TransformTarget::ES2021 || options.logical_assignment_operators).then(
|| {
let vars = ast.new_vec();
Self { ast, ctx, vars }
},
)
}
pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) {
let Expression::AssignmentExpression(assignment_expr) = expr else { return };
// `&&=` `||=` `??=`
let operator = match assignment_expr.operator {
AssignmentOperator::LogicalAnd => LogicalOperator::And,
AssignmentOperator::LogicalOr => LogicalOperator::Or,
@ -31,18 +58,135 @@ impl<'a> LogicalAssignmentOperators<'a> {
_ => return,
};
// Create the left hand side
// a || (a = b)
// ^ ^
let left1: AssignmentTarget<'a> = self.ast.copy(&assignment_expr.left);
let left2 = match &assignment_expr.left {
// `a &&= c` -> `a && (a = c);`
// ^ ^ assign_target
// ^ left_expr
let left_expr: Expression<'a>;
let assign_target: SimpleAssignmentTarget<'a>;
// TODO: refactor this block, add tests, cover private identifier
match &assignment_expr.left {
AssignmentTarget::SimpleAssignmentTarget(target) => match target {
SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => {
self.ast.identifier_reference_expression((*ident).clone())
left_expr = self.ast.identifier_reference_expression((*ident).clone());
assign_target = self.ast.simple_assignment_target_identifier((*ident).clone());
}
SimpleAssignmentTarget::MemberAssignmentTarget(member_expr) => {
let member_expr = self.ast.copy(&**member_expr);
self.ast.member_expression(member_expr)
let span = Span::default();
let op = AssignmentOperator::Assign;
// `a.b &&= c` -> `var _a; (_a = a).b && (_a.b = c)`
match &**member_expr {
MemberExpression::StaticMemberExpression(static_expr) => {
if let Some(ident) = self.maybe_generate_memoised(&static_expr.object) {
let right = self.ast.copy(&static_expr.object);
let mut expr = self.ast.copy(static_expr);
let target = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(ident.clone()),
);
expr.object =
self.ast.assignment_expression(span, op, target, right);
left_expr = self.ast.member_expression(
MemberExpression::StaticMemberExpression(expr),
);
let mut expr = self.ast.copy(static_expr);
expr.object = self.ast.identifier_reference_expression(ident);
assign_target =
self.ast.simple_assignment_target_member_expression(
MemberExpression::StaticMemberExpression(expr),
);
} else {
left_expr = self.ast.member_expression(
MemberExpression::StaticMemberExpression(
self.ast.copy(static_expr),
),
);
assign_target = SimpleAssignmentTarget::MemberAssignmentTarget(
self.ast.copy(member_expr),
);
};
}
// `a[b.y] &&= c;` ->
// `var _a, _b$y; (_a = a)[_b$y = b.y] && (_a[_b$y] = c);`
MemberExpression::ComputedMemberExpression(computed_expr) => {
if let Some(ident) = self.maybe_generate_memoised(&computed_expr.object)
{
let property_ident =
self.maybe_generate_memoised(&computed_expr.expression);
let right = self.ast.copy(&computed_expr.object);
let mut expr = self.ast.copy(computed_expr);
let target = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(ident.clone()),
);
expr.object =
self.ast.assignment_expression(span, op, target, right);
if let Some(property_ident) = &property_ident {
let left = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(
property_ident.clone(),
),
);
let right = self.ast.copy(&computed_expr.expression);
expr.expression =
self.ast.assignment_expression(span, op, left, right);
}
left_expr = self.ast.member_expression(
MemberExpression::ComputedMemberExpression(expr),
);
// `(_a[_b$y] = c)` part
let mut expr = self.ast.copy(computed_expr);
expr.object = self.ast.identifier_reference_expression(ident);
if let Some(property_ident) = property_ident {
expr.expression =
self.ast.identifier_reference_expression(property_ident);
}
assign_target =
self.ast.simple_assignment_target_member_expression(
MemberExpression::ComputedMemberExpression(expr),
);
} else {
let property_ident =
self.maybe_generate_memoised(&computed_expr.expression);
// let right = self.ast.copy(&computed_expr.object);
let mut expr = self.ast.copy(computed_expr);
// let target = AssignmentTarget::SimpleAssignmentTarget(
// self.ast.simple_assignment_target_identifier(ident.clone()),
// );
// expr.object =
// self.ast.assignment_expression(span, op, target, right);
if let Some(property_ident) = &property_ident {
let left = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(
property_ident.clone(),
),
);
let right = self.ast.copy(&computed_expr.expression);
expr.expression =
self.ast.assignment_expression(span, op, left, right);
}
left_expr = self.ast.member_expression(
MemberExpression::ComputedMemberExpression(expr),
);
let mut expr = self.ast.copy(computed_expr);
// expr.object = self.ast.identifier_reference_expression(ident);
if let Some(property_ident) = property_ident {
expr.expression =
self.ast.identifier_reference_expression(property_ident);
}
assign_target =
self.ast.simple_assignment_target_member_expression(
MemberExpression::ComputedMemberExpression(expr),
);
};
}
MemberExpression::PrivateFieldExpression(_) => return,
}
}
// All other are TypeScript syntax.
_ => return,
@ -52,15 +196,16 @@ impl<'a> LogicalAssignmentOperators<'a> {
AssignmentTarget::AssignmentTargetPattern(_) => return,
};
// Create the right hand side
// a || (a = b)
// ^^^^^^^
let assign_op = AssignmentOperator::Assign;
let right = self.ast.copy(&assignment_expr.right);
let right = self.ast.assignment_expression(Span::default(), assign_op, left1, right);
let right = self.ast.parenthesized_expression(Span::default(), right);
let assign_target = AssignmentTarget::SimpleAssignmentTarget(assign_target);
let right = self.ast.move_expression(&mut assignment_expr.right);
let right =
self.ast.assignment_expression(Span::default(), assign_op, assign_target, right);
let logical_expr = self.ast.logical_expression(Span::default(), left_expr, operator, right);
let logical_expr = self.ast.logical_expression(Span::default(), left2, operator, right);
*expr = logical_expr;
}
}
// TODO: test all permutations

View file

@ -82,7 +82,7 @@ impl<'a> Transformer<'a> {
react_jsx: options.react_jsx.map(|options| ReactJsx::new(Rc::clone(&ast), options)),
regexp_flags: RegexpFlags::new(Rc::clone(&ast), &options),
es2022_class_static_block: es2022::ClassStaticBlock::new(Rc::clone(&ast), &options),
es2021_logical_assignment_operators: LogicalAssignmentOperators::new(Rc::clone(&ast), &options),
es2021_logical_assignment_operators: LogicalAssignmentOperators::new(Rc::clone(&ast), ctx.clone(), &options),
es2020_nullish_coalescing_operators: NullishCoalescingOperator::new(Rc::clone(&ast), ctx.clone(), &options),
es2019_optional_catch_binding: OptionalCatchBinding::new(Rc::clone(&ast), &options),
es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), ctx.clone(), &options),
@ -100,8 +100,10 @@ impl<'a> VisitMut<'a> for Transformer<'a> {
for stmt in stmts.iter_mut() {
self.visit_statement(stmt);
}
self.es2016_exponentiation_operator.as_mut().map(|t| t.add_vars_to_statements(stmts));
// TODO: we need scope id to insert the vars into the correct statements
self.es2021_logical_assignment_operators.as_mut().map(|t| t.add_vars_to_statements(stmts));
self.es2020_nullish_coalescing_operators.as_mut().map(|t| t.add_vars_to_statements(stmts));
self.es2016_exponentiation_operator.as_mut().map(|t| t.add_vars_to_statements(stmts));
}
fn visit_expression(&mut self, expr: &mut Expression<'a>) {

View file

@ -2,57 +2,10 @@ use std::mem;
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_span::{Atom, Span};
use oxc_span::Span;
use crate::context::TransformerCtx;
// TODO:
// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L543>
pub fn generate_uid_based_on_node(expr: &Expression) -> Atom {
let mut parts = std::vec::Vec::with_capacity(1);
expr.gather(&mut |part| parts.push(part));
let name = parts.join("$");
Atom::from(format!("_{name}"))
}
// TODO: <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L61>
pub trait GatherNodeParts {
fn gather<F: FnMut(Atom)>(&self, f: &mut F);
}
impl<'a> GatherNodeParts for Expression<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
match self {
Self::Identifier(ident) => f(ident.name.clone()),
Self::MemberExpression(expr) => expr.gather(f),
_ => f(Atom::from("ref")),
}
}
}
impl<'a> GatherNodeParts for MemberExpression<'a> {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
self.object().gather(f);
match self {
MemberExpression::ComputedMemberExpression(expr) => expr.expression.gather(f),
MemberExpression::StaticMemberExpression(expr) => expr.property.gather(f),
MemberExpression::PrivateFieldExpression(expr) => expr.field.gather(f),
}
}
}
impl GatherNodeParts for IdentifierName {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.name.clone());
}
}
impl GatherNodeParts for PrivateIdentifier {
fn gather<F: FnMut(Atom)>(&self, f: &mut F) {
f(self.name.clone());
}
}
pub trait CreateVars<'a> {
fn ctx(&self) -> &TransformerCtx<'a>;
@ -71,17 +24,29 @@ pub trait CreateVars<'a> {
stmts.insert(0, stmt);
}
fn create_new_var(&mut self, expr: &Expression<'a>) -> Atom {
let name = generate_uid_based_on_node(expr);
// TODO: scope.push({ id: temp });
fn create_new_var(&mut self, expr: &Expression<'a>) -> IdentifierReference {
let name = self.ctx().scopes().generate_uid_based_on_node(expr);
self.ctx().add_binding(name.clone());
// Add `var name` to scope
// TODO: hookup symbol id
let binding_identifier = BindingIdentifier::new(Span::default(), name.clone());
let binding_pattern_kind = self.ctx().ast.binding_pattern_identifier(binding_identifier);
let binding = self.ctx().ast.binding_pattern(binding_pattern_kind, None, false);
let kind = VariableDeclarationKind::Var;
let decl = self.ctx().ast.variable_declarator(Span::default(), kind, binding, None, false);
self.vars_mut().push(decl);
name
// TODO: add reference id and flag
IdentifierReference::new(Span::default(), name)
}
/// Possibly generate a memoised identifier if it is not static and has consequences.
/// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L578>
fn maybe_generate_memoised(&mut self, expr: &Expression<'a>) -> Option<IdentifierReference> {
if self.ctx().symbols().is_static(expr) {
None
} else {
Some(self.create_new_var(expr))
}
}
}

View file

@ -1,4 +1,4 @@
Passed: 159/1091
Passed: 161/1091
# All Passed:
* babel-plugin-transform-numeric-separator
@ -484,10 +484,8 @@ Passed: 159/1091
* Failed: to-native-fields/static-shadow/input.js
* Failed: to-native-fields/static-shadowed-binding/input.js
# babel-plugin-transform-logical-assignment-operators (3/6)
* Failed: logical-assignment/general-semantics/input.js
# babel-plugin-transform-logical-assignment-operators (5/6)
* Failed: logical-assignment/null-coalescing/input.js
* Failed: logical-assignment/null-coalescing-without-other/input.js
# babel-plugin-transform-export-namespace-from (0/4)
* Failed: export-namespace/namespace-default/input.mjs