From 419d5aa6ee9db9f81a8e27725215c1223753a1d0 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 16 Sep 2023 16:05:35 +0800 Subject: [PATCH] feat(transformer): transformer prototype (#918) This PR introduces the oxc transformer. The code is purposely arranged without any Rust trickery such as traits and macros. I intend to keep the code style this way until something blows up. The next steps are: * unit tests * conformance tests with TypeScript and Babel * benchmarks --- Cargo.lock | 12 +++ crates/oxc_ast/src/ast/js.rs | 6 ++ crates/oxc_ast/src/ast/literal.rs | 6 ++ crates/oxc_ast/src/ast_builder.rs | 10 ++- crates/oxc_transformer/Cargo.toml | 23 ++++++ .../oxc_transformer/examples/transformer.rs | 41 ++++++++++ .../src/es2016/exponentiation_operator.rs | 42 ++++++++++ crates/oxc_transformer/src/es2016/mod.rs | 3 + crates/oxc_transformer/src/es2019/mod.rs | 3 + .../src/es2019/optional_catch_binding.rs | 29 +++++++ crates/oxc_transformer/src/lib.rs | 76 +++++++++++++++++++ 11 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 crates/oxc_transformer/Cargo.toml create mode 100644 crates/oxc_transformer/examples/transformer.rs create mode 100644 crates/oxc_transformer/src/es2016/exponentiation_operator.rs create mode 100644 crates/oxc_transformer/src/es2016/mod.rs create mode 100644 crates/oxc_transformer/src/es2019/mod.rs create mode 100644 crates/oxc_transformer/src/es2019/optional_catch_binding.rs create mode 100644 crates/oxc_transformer/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a323cfb55..0785cedf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1718,6 +1718,18 @@ dependencies = [ "url", ] +[[package]] +name = "oxc_transformer" +version = "0.2.0" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_formatter", + "oxc_parser", + "oxc_span", + "oxc_syntax", +] + [[package]] name = "oxc_type_synthesis" version = "0.0.0" diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index b1e014581..d27f8bc55 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -257,6 +257,12 @@ pub struct IdentifierName { pub name: Atom, } +impl IdentifierName { + pub fn new(span: Span, name: Atom) -> Self { + Self { span, name } + } +} + /// Identifier Reference #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))] diff --git a/crates/oxc_ast/src/ast/literal.rs b/crates/oxc_ast/src/ast/literal.rs index 7fe8ab75c..389924d97 100644 --- a/crates/oxc_ast/src/ast/literal.rs +++ b/crates/oxc_ast/src/ast/literal.rs @@ -43,6 +43,12 @@ impl Hash for NullLiteral { } } +impl NullLiteral { + pub fn new(span: Span) -> Self { + Self { span } + } +} + #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))] pub struct NumberLiteral<'a> { diff --git a/crates/oxc_ast/src/ast_builder.rs b/crates/oxc_ast/src/ast_builder.rs index f955d71e2..664b940ce 100644 --- a/crates/oxc_ast/src/ast_builder.rs +++ b/crates/oxc_ast/src/ast_builder.rs @@ -6,13 +6,14 @@ )] use oxc_allocator::{Allocator, Box, String, Vec}; -use oxc_span::{Atom, SourceType, Span}; +use oxc_span::{Atom, GetSpan, SourceType, Span}; use oxc_syntax::{ operator::{ AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator, UpdateOperator, }, NumberBase, }; +use std::mem; #[allow(clippy::wildcard_imports)] use crate::ast::*; @@ -54,6 +55,13 @@ impl<'a> AstBuilder<'a> { String::from_str_in(value, self.allocator).into_bump_str() } + /// Moves the expression out by replacing it with a null expression. + pub fn move_expression(&self, expr: &mut Expression<'a>) -> Expression<'a> { + let null_literal = NullLiteral::new(expr.span()); + let null_expr = self.literal_null_expression(null_literal); + mem::replace(expr, null_expr) + } + pub fn program( &self, span: Span, diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml new file mode 100644 index 000000000..cf2c913b8 --- /dev/null +++ b/crates/oxc_transformer/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "oxc_transformer" +version = "0.2.0" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +categories.workspace = true + +[dependencies] +oxc_ast = { workspace = true } +oxc_span = { workspace = true } +oxc_allocator = { workspace = true } +oxc_syntax = { workspace = true } + +[dev-dependencies] +oxc_parser = { workspace = true } +oxc_formatter = { workspace = true } diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs new file mode 100644 index 000000000..448b7ea09 --- /dev/null +++ b/crates/oxc_transformer/examples/transformer.rs @@ -0,0 +1,41 @@ +use std::{env, path::Path}; + +use oxc_allocator::Allocator; +use oxc_formatter::{Formatter, FormatterOptions}; +use oxc_parser::Parser; +use oxc_span::SourceType; +use oxc_transformer::{TransformOptions, Transformer}; + +// Instruction: +// create a `test.js`, +// run `cargo run -p oxc_transformer --example transformer` +// or `just watch "run -p oxc_transformer --example transformer"` + +fn main() { + let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string()); + let path = Path::new(&name); + let source_text = std::fs::read_to_string(path).expect("{name} not found"); + let allocator = Allocator::default(); + let source_type = SourceType::from_path(path).unwrap(); + let ret = Parser::new(&allocator, &source_text, source_type).parse(); + + if !ret.errors.is_empty() { + for error in ret.errors { + let error = error.with_source_code(source_text.clone()); + println!("{error:?}"); + } + return; + } + + let formatter_options = FormatterOptions::default(); + let program = allocator.alloc(ret.program); + let printed = Formatter::new(source_text.len(), formatter_options.clone()).build(program); + println!("Original:\n"); + println!("{printed}"); + + let transform_options = TransformOptions::default(); + Transformer::new(&allocator, &transform_options).build(program); + let printed = Formatter::new(source_text.len(), formatter_options).build(program); + println!("Transformed:\n"); + println!("{printed}"); +} diff --git a/crates/oxc_transformer/src/es2016/exponentiation_operator.rs b/crates/oxc_transformer/src/es2016/exponentiation_operator.rs new file mode 100644 index 000000000..445fd0b0d --- /dev/null +++ b/crates/oxc_transformer/src/es2016/exponentiation_operator.rs @@ -0,0 +1,42 @@ +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::Span; +use oxc_syntax::operator::BinaryOperator; + +use std::rc::Rc; + +/// ES2016: Exponentiation Operator +/// +/// References: +/// * +/// * +/// +pub struct ExponentiationOperator<'a> { + ast: Rc>, +} + +impl<'a> ExponentiationOperator<'a> { + pub fn new(ast: Rc>) -> Self { + Self { ast } + } + + pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) { + let Expression::BinaryExpression(binary_expression) = expr else { return }; + if binary_expression.operator != BinaryOperator::Exponential { + return; + } + // left ** right + let left = self.ast.move_expression(&mut binary_expression.left); + let right = self.ast.move_expression(&mut binary_expression.right); + // Math.pow + let ident_math = IdentifierReference::new("Math".into(), Span::default()); + let object = self.ast.identifier_expression(ident_math); + let property = IdentifierName::new(Span::default(), "pow".into()); + let callee = self.ast.static_member_expression(Span::default(), object, property, false); + // Math.pow(left, right) + let mut arguments = self.ast.new_vec_with_capacity(2); + arguments.push(Argument::Expression(left)); + arguments.push(Argument::Expression(right)); + let call_expr = self.ast.call_expression(Span::default(), callee, arguments, false, None); + *expr = call_expr; + } +} diff --git a/crates/oxc_transformer/src/es2016/mod.rs b/crates/oxc_transformer/src/es2016/mod.rs new file mode 100644 index 000000000..f2ea3e064 --- /dev/null +++ b/crates/oxc_transformer/src/es2016/mod.rs @@ -0,0 +1,3 @@ +mod exponentiation_operator; + +pub use exponentiation_operator::ExponentiationOperator; diff --git a/crates/oxc_transformer/src/es2019/mod.rs b/crates/oxc_transformer/src/es2019/mod.rs new file mode 100644 index 000000000..6b7b65c77 --- /dev/null +++ b/crates/oxc_transformer/src/es2019/mod.rs @@ -0,0 +1,3 @@ +mod optional_catch_binding; + +pub use optional_catch_binding::OptionalCatchBinding; diff --git a/crates/oxc_transformer/src/es2019/optional_catch_binding.rs b/crates/oxc_transformer/src/es2019/optional_catch_binding.rs new file mode 100644 index 000000000..45d681829 --- /dev/null +++ b/crates/oxc_transformer/src/es2019/optional_catch_binding.rs @@ -0,0 +1,29 @@ +use oxc_ast::{ast::*, AstBuilder}; +use oxc_span::Span; + +use std::rc::Rc; + +/// ES2019: Optional Catch Binding +/// +/// References: +/// * +/// * +pub struct OptionalCatchBinding<'a> { + ast: Rc>, +} + +impl<'a> OptionalCatchBinding<'a> { + pub fn new(ast: Rc>) -> Self { + Self { ast } + } + + pub fn transform_catch_clause<'b>(&mut self, clause: &'b mut CatchClause<'a>) { + if clause.param.is_some() { + return; + } + let binding_identifier = BindingIdentifier::new("unused".into(), Span::default()); + let binding_pattern_kind = self.ast.binding_identifier(binding_identifier); + let binding_pattern = self.ast.binding_pattern(binding_pattern_kind, None, false); + clause.param = Some(binding_pattern); + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs new file mode 100644 index 000000000..4f5b19b1b --- /dev/null +++ b/crates/oxc_transformer/src/lib.rs @@ -0,0 +1,76 @@ +#![allow(clippy::wildcard_imports, clippy::option_map_unit_fn)] + +//! Transformer / Transpiler +//! +//! References: +//! * +//! * +//! * + +mod es2016; +mod es2019; + +use oxc_allocator::Allocator; +use oxc_ast::{ast::*, AstBuilder, VisitMut}; +use std::rc::Rc; + +use es2016::ExponentiationOperator; +use es2019::OptionalCatchBinding; + +#[derive(Debug, Default, Clone)] +pub struct TransformOptions { + target: TransformTarget, +} + +/// See +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +pub enum TransformTarget { + ES2016, + ES2019, + #[default] + ESNext, +} + +#[derive(Default)] +pub struct Transformer<'a> { + // es2016 + es2016_exponentiation_operator: Option>, + // es2019 + es2019_optional_catch_binding: Option>, +} + +impl<'a> Transformer<'a> { + pub fn new(allocator: &'a Allocator, options: &TransformOptions) -> Self { + let ast = Rc::new(AstBuilder::new(allocator)); + + let mut t = Self::default(); + if options.target < TransformTarget::ES2016 { + t.es2016_exponentiation_operator.replace(ExponentiationOperator::new(Rc::clone(&ast))); + } + if options.target < TransformTarget::ES2019 { + t.es2019_optional_catch_binding.replace(OptionalCatchBinding::new(Rc::clone(&ast))); + } + t + } + + pub fn build<'b>(mut self, program: &'b mut Program<'a>) { + self.visit_program(program); + } +} + +impl<'a, 'b> VisitMut<'a, 'b> for Transformer<'a> { + fn visit_expression(&mut self, expr: &'b mut Expression<'a>) { + self.es2016_exponentiation_operator.as_mut().map(|t| t.transform_expression(expr)); + + self.visit_expression_match(expr); + } + + fn visit_catch_clause(&mut self, clause: &'b mut CatchClause<'a>) { + self.es2019_optional_catch_binding.as_mut().map(|t| t.transform_catch_clause(clause)); + + if let Some(param) = &mut clause.param { + self.visit_binding_pattern(param); + } + self.visit_statements(&mut clause.body.body); + } +}