Skip to content

Commit

Permalink
feat(oxc): add Compiler and CompilerInterface (#4954)
Browse files Browse the repository at this point in the history
This PR adds a full compiler pipeline to the `oxc` crate, to stop us
from implementing the same pipeline over and over again.

relates #4455
  • Loading branch information
Boshen authored Aug 19, 2024
1 parent b58413f commit 6800e69
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 111 deletions.
21 changes: 15 additions & 6 deletions crates/oxc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ workspace = true
test = false
doctest = false

[[example]]
name = "compiler"
path = "examples/compiler.rs"
required-features = ["full"]

[dependencies]
oxc_allocator = { workspace = true }
oxc_ast = { workspace = true }
Expand All @@ -37,14 +42,18 @@ oxc_sourcemap = { workspace = true, optional = true }
oxc_isolated_declarations = { workspace = true, optional = true }

[features]
serialize = ["oxc_ast/serialize", "oxc_semantic?/serialize", "oxc_span/serialize", "oxc_syntax/serialize"]
semantic = ["oxc_semantic"]
transformer = ["oxc_transformer"]
minifier = ["oxc_mangler", "oxc_minifier"]
codegen = ["oxc_codegen"]
isolated_declarations = ["oxc_isolated_declarations"]
full = ["codegen", "minifier", "semantic", "transformer"]

semantic = ["oxc_semantic"]
transformer = ["oxc_transformer"]
minifier = ["oxc_mangler", "oxc_minifier"]
codegen = ["oxc_codegen"]

serialize = ["oxc_ast/serialize", "oxc_semantic?/serialize", "oxc_span/serialize", "oxc_syntax/serialize"]

sourcemap = ["oxc_sourcemap"]
sourcemap_concurrent = ["oxc_sourcemap/concurrent", "sourcemap"]

isolated_declarations = ["oxc_isolated_declarations"]

wasm = ["oxc_transformer/wasm"]
32 changes: 32 additions & 0 deletions crates/oxc/examples/compiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#![allow(clippy::print_stdout)]

use std::{env, io, path::Path};

use oxc::{span::SourceType, Compiler};

// Instruction:
// 1. create a `test.js`
// 2. run either
// * `cargo run -p oxc --example compiler --features="full"`
// * `just watch 'run -p oxc --example compiler --features="full"'`

fn main() -> io::Result<()> {
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)?;
let source_type = SourceType::from_path(path).unwrap();

match Compiler::default().execute(&source_text, source_type, path) {
Ok(printed) => {
println!("{printed}");
}
Err(errors) => {
for error in errors {
let error = error.with_source_code(source_text.to_string());
println!("{error:?}");
}
}
}

Ok(())
}
225 changes: 225 additions & 0 deletions crates/oxc/src/compiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
use std::{mem, ops::ControlFlow, path::Path};

use oxc_allocator::Allocator;
use oxc_ast::{ast::Program, Trivias};
use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions, WhitespaceRemover};
use oxc_diagnostics::OxcDiagnostic;
use oxc_parser::{ParseOptions, Parser, ParserReturn};
use oxc_span::SourceType;

use oxc_minifier::{CompressOptions, Compressor};
use oxc_semantic::{ScopeTree, SemanticBuilder, SemanticBuilderReturn, SymbolTable};
use oxc_transformer::{TransformOptions, Transformer, TransformerReturn};

#[derive(Default)]
pub struct Compiler {
printed: String,
errors: Vec<OxcDiagnostic>,
}

impl CompilerInterface for Compiler {
fn handle_errors(&mut self, errors: Vec<OxcDiagnostic>) {
self.errors.extend(errors);
}

fn after_codegen(&mut self, printed: String) {
self.printed = printed;
}
}

impl Compiler {
/// # Errors
///
/// * A list of [OxcDiagnostic].
pub fn execute(
&mut self,
source_text: &str,
source_type: SourceType,
source_path: &Path,
) -> Result<String, Vec<OxcDiagnostic>> {
self.compile(source_text, source_type, source_path);
if self.errors.is_empty() {
Ok(mem::take(&mut self.printed))
} else {
Err(mem::take(&mut self.errors))
}
}
}

pub trait CompilerInterface {
fn handle_errors(&mut self, _errors: Vec<OxcDiagnostic>) {}

fn parser_options(&self) -> ParseOptions {
ParseOptions::default()
}

fn transform_options(&self) -> Option<TransformOptions> {
Some(TransformOptions::default())
}

fn compress_options(&self) -> Option<CompressOptions> {
Some(CompressOptions::all_true())
}

fn codegen_options(&self) -> Option<CodegenOptions> {
Some(CodegenOptions::default())
}

fn remove_whitespace(&self) -> bool {
false
}

fn after_parse(&mut self, _parser_return: &mut ParserReturn) -> ControlFlow<()> {
ControlFlow::Continue(())
}

fn after_semantic(
&mut self,
_program: &mut Program<'_>,
_semantic_return: &mut SemanticBuilderReturn,
) -> ControlFlow<()> {
ControlFlow::Continue(())
}

fn after_transform(
&mut self,
_program: &mut Program<'_>,
_transformer_return: &mut TransformerReturn,
) -> ControlFlow<()> {
ControlFlow::Continue(())
}

fn after_codegen(&mut self, _printed: String) {}

fn compile(&mut self, source_text: &str, source_type: SourceType, source_path: &Path) {
let allocator = Allocator::default();

/* Parse */

let mut parser_return = self.parse(&allocator, source_text, source_type);
if self.after_parse(&mut parser_return).is_break() {
return;
}
if !parser_return.errors.is_empty() {
self.handle_errors(parser_return.errors);
}

/* Semantic */

let mut program = parser_return.program;
let trivias = parser_return.trivias;

let mut semantic_return = self.semantic(&program, source_text, source_type, source_path);
if !semantic_return.errors.is_empty() {
self.handle_errors(semantic_return.errors);
return;
}
if self.after_semantic(&mut program, &mut semantic_return).is_break() {
return;
}

let (symbols, scopes) = semantic_return.semantic.into_symbol_table_and_scope_tree();

/* Transform */

if let Some(options) = self.transform_options() {
let mut transformer_return = self.transform(
options,
&allocator,
&mut program,
source_path,
source_text,
source_type,
&trivias,
symbols,
scopes,
);

if !transformer_return.errors.is_empty() {
self.handle_errors(transformer_return.errors);
return;
}

if self.after_transform(&mut program, &mut transformer_return).is_break() {
return;
}
}

if let Some(options) = self.compress_options() {
self.compress(&allocator, &mut program, options);
}

if let Some(options) = self.codegen_options() {
let printed = self.codegen(&program, source_text, &trivias, options);
self.after_codegen(printed);
}
}

fn parse<'a>(
&self,
allocator: &'a Allocator,
source_text: &'a str,
source_type: SourceType,
) -> ParserReturn<'a> {
Parser::new(allocator, source_text, source_type).with_options(self.parser_options()).parse()
}

fn semantic<'a>(
&self,
program: &Program<'a>,
source_text: &'a str,
source_type: SourceType,
source_path: &Path,
) -> SemanticBuilderReturn<'a> {
SemanticBuilder::new(source_text, source_type)
.with_check_syntax_error(true)
.build_module_record(source_path.to_path_buf(), program)
.build(program)
}

#[allow(clippy::too_many_arguments)]
fn transform<'a>(
&self,
options: TransformOptions,
allocator: &'a Allocator,
program: &mut Program<'a>,
source_path: &Path,
source_text: &'a str,
source_type: SourceType,
trivias: &Trivias,
symbols: SymbolTable,
scopes: ScopeTree,
) -> TransformerReturn {
Transformer::new(allocator, source_path, source_type, source_text, trivias.clone(), options)
.build_with_symbols_and_scopes(symbols, scopes, program)
}

fn compress<'a>(
&self,
allocator: &'a Allocator,
program: &mut Program<'a>,
options: CompressOptions,
) {
Compressor::new(allocator, options).build(program);
}

fn codegen<'a>(
&self,
program: &Program<'a>,
source_text: &'a str,
trivias: &Trivias,
options: CodegenOptions,
) -> String {
let comment_options = CommentOptions { preserve_annotate_comments: true };

if self.remove_whitespace() {
WhitespaceRemover::new().with_options(options).build(program).source_text
} else {
CodeGenerator::new()
.with_options(options)
.enable_comment(source_text, trivias.clone(), comment_options)
.build(program)
.source_text
}
}
}
6 changes: 6 additions & 0 deletions crates/oxc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
//!
//! <https://github.com/oxc-project/oxc>
#[cfg(feature = "full")]
mod compiler;

#[cfg(feature = "full")]
pub use compiler::{Compiler, CompilerInterface};

pub mod allocator {
#[doc(inline)]
pub use oxc_allocator::*;
Expand Down
18 changes: 12 additions & 6 deletions crates/oxc_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub struct ParserReturn<'a> {

/// Parser options
#[derive(Clone, Copy)]
struct ParserOptions {
pub struct ParseOptions {
pub allow_return_outside_function: bool,
/// Emit `ParenthesizedExpression` in AST.
///
Expand All @@ -117,7 +117,7 @@ struct ParserOptions {
pub preserve_parens: bool,
}

impl Default for ParserOptions {
impl Default for ParseOptions {
fn default() -> Self {
Self { allow_return_outside_function: false, preserve_parens: true }
}
Expand All @@ -130,16 +130,22 @@ pub struct Parser<'a> {
allocator: &'a Allocator,
source_text: &'a str,
source_type: SourceType,
options: ParserOptions,
options: ParseOptions,
}

impl<'a> Parser<'a> {
/// Create a new parser
pub fn new(allocator: &'a Allocator, source_text: &'a str, source_type: SourceType) -> Self {
let options = ParserOptions::default();
let options = ParseOptions::default();
Self { allocator, source_text, source_type, options }
}

#[must_use]
pub fn with_options(mut self, options: ParseOptions) -> Self {
self.options = options;
self
}

/// Allow return outside of function
///
/// By default, a return statement at the top level raises an error.
Expand Down Expand Up @@ -279,7 +285,7 @@ impl<'a> ParserImpl<'a> {
allocator: &'a Allocator,
source_text: &'a str,
source_type: SourceType,
options: ParserOptions,
options: ParseOptions,
unique: UniquePromise,
) -> Self {
Self {
Expand Down Expand Up @@ -347,7 +353,7 @@ impl<'a> ParserImpl<'a> {
Ok(self.ast.program(span, self.source_type, hashbang, directives, statements))
}

fn default_context(source_type: SourceType, options: ParserOptions) -> Context {
fn default_context(source_type: SourceType, options: ParseOptions) -> Context {
let mut ctx = Context::default().and_ambient(source_type.is_typescript_definition());
if source_type.module_kind() == ModuleKind::Module {
// for [top-level-await](https://tc39.es/proposal-top-level-await/)
Expand Down
12 changes: 2 additions & 10 deletions tasks/coverage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,8 @@ test = false
doctest = false

[dependencies]
oxc = { workspace = true, features = [
"codegen",
"isolated_declarations",
"minifier",
"semantic",
"serialize",
"sourcemap",
"transformer",
] }
oxc_prettier = { workspace = true }
oxc = { workspace = true, features = ["full", "isolated_declarations", "serialize", "sourcemap"] }
oxc_prettier = { workspace = true }
oxc_tasks_common = { workspace = true }

serde = { workspace = true, features = ["derive"] }
Expand Down
Loading

0 comments on commit 6800e69

Please sign in to comment.