SyMind / learning

路漫漫其修远兮,吾将上下而求索。
9 stars 1 forks source link

从零开始的 swc 源代码学习 - 第一部分,使用编译器 #71

Open SyMind opened 1 year ago

SyMind commented 1 year ago

写在前面的

万字 esbuild 源代码批判,让你能够直接 PR 文章中详细研究了 esbuild 项目,为达到极限的编译速度,它的编译过程十分紧凑,仅有两次整体 AST 树传递。

这并非没有代价,esbuild 中对 AST 的解析、适配低版本语法、代码压缩等流程紧密耦合,难以基于它开发 vue、svelte 编译器(目前 esbuild 对于 Vue 和 Svelte 都是使用 js 编写的编译器处理后,在通过进程间通信传递转换后的结果)。而且目前 esbuild 中核心的数据结构和类型定义均位于 internal 目录下,开发者无法进行访问。

这就是要研究 swc 的理由。

目录结构

swc 通过 Cargo 的 workspace 管理多个相关的协同开发的 crate,它们位于 crates 目录下。

bindings 目录下的 crate 未放到 workspace 下的原因是,绑定程序使用已发布的 swc_core SDK 来构建特定平台的绑定。若放置到 workspace 下时,可能使用并未发布的依赖的版本,导致混乱。

使用 swc

文章是为了阅读 swc 的源代码,所以这里指的是通过 rust 使用 swc。下面是一个使用 swc 简单的例子,这段代码就位于 swc 项目中 crates/swc/examples/transform.rs

use std::{path::Path, sync::Arc};

use anyhow::Context;
use swc::{self, config::Options, try_with_handler, HandlerOpts};
use swc_common::SourceMap;

fn main() {
    let cm = Arc::<SourceMap>::default();

    let c = swc::Compiler::new(cm.clone());

    let output = try_with_handler(
        cm.clone(),
        HandlerOpts {
            ..Default::default()
        },
        |handler| {
            let fm = cm
                .load_file(Path::new("examples/transform-input.js"))
                .expect("failed to load file");

            c.process_js_file(
                fm,
                handler,
                &Options {
                    ..Default::default()
                },
            )
            .context("failed to process file")
        },
    )
    .unwrap();

    println!("{}", output.code);
}

上面的程序做了以下事情:

  1. 创建 swc SourceMap 实例,由于该数据资源在同一时刻可拥有多个所有者,且可在线程间共享,故通过 Arc 智能指针持有该值。
  2. 创建 swc Compiler 实例。
  3. 调用 cmload_file 方法加载 js 文件 examples/transform-input.js。
  4. cm 作为参数,调用 cprocess_js_file 方法处理 js 文件。
  5. try_with_handler 会创建一个 Handler 实例作为 process_js_file 的参数,来处理错误信息。其返回值为 Result 枚举,通过 unwrap 方法,如果返回成功,就将 Ok(T) 中的值取出来,如果失败,就直接 panic

涉及到 swc 中定义的 SourceMapCompiler 两个结构体,我们详细了解一下它们。

SourceMap 结构体

SourceMap 记录所有所使用的源代码,可以将字节位置映射到源代码中的具体位置。解析过程中的每个源数据(通常是文件、字符串或宏扩展)都存储在 SourceMap 中,表示为 SourceFiles。字节位置存储在 span 中,并在编译器中广泛使用。它是 SourceMap 绝对位置,可以将其转换为行列信息、源代码片段等。

classDiagram
    class SourceMap {
        + files: Lock~SourceMapFiles~
        - start_pos: AtomicUsize
        - file_loader: Box~dyn|FileLoader+Sync+Send~
        - path_mapping: FilePathMapping
        - doctest_offset: Option~[FileName|isize]~
    }

    class SourceMapFiles {
        + source_files: Vec~Lrc~SourceFile~~
    }

    class SourceFile {
        + name: FileName
        + src: Lrc~String~
        + src_hash: u128
        + start_pos: BytePos
        + end_pos: BytePos
        + lines: Vec~BytePos~
        + multibyte_chars: Vec~MultiByteChar~
        + non_narrow_chars: Vec~NonNarrowChar~
        + name_hash: u128
    }

    class FileLoader {
        file_exists(&self, path: &Path) bool
        abs_path(&self, path: &Path) Option~PathBuf~
        read_file(&self, path: &Path) io::Result~String~
    }
    <<Interface>> FileLoader

    class FilePathMapping {
        - mapping: Vec~[PathBuf|PathBuf]~,
    }

    class `BytePos(u32)`

    SourceMap --> FileLoader
    SourceMap --> FilePathMapping
    SourceMap o-- SourceMapFiles
    SourceMapFiles "1" o-- "*" SourceFile
    SourceFile --> `BytePos(u32)`

通过调用 SourceMap 实例的 load_file 方法,传入 js 文件路径获取 SourceFile 实例,SourceFile 的结构如下:

struct SourceFile {
    /// 源文件名。根据惯例,非源自文件系统的源码名位于尖括号之间,例如`<anon>`
    pub name: FileName,
    /// 全部源代码
    pub src: Lrc<String>,
    /// 源代码的 hash
    pub src_hash: u128,
    /// 此源代码位于 `SourceMap` 中的起始位置
    pub start_pos: BytePos,
    /// 此源代码位于 `SourceMap` 中的结束位置
    pub end_pos: BytePos,
    /// 源代码中所有行的起始位置
    pub lines: Vec<BytePos>,
    /// 文件名 hash
    pub name_hash: u128,
}

Compiler 结构体

Compiler 结构体存储 SourceMap,提供用于编译 js 文件的方法。

struct Compiler {
    pub cm: Arc<SourceMap>,
}

Handler 结构体

Compiler 提供的所有方法都需要传入一个 Handler 结构体,除了会导致 swc 立即退出的错误,其他错误会通过 Handler 结构体记录用于后续进行错误报告。Handler 结构体如下:

pub struct Handler {
    // 如何对待不同等级的错误信息
    pub flags: HandlerFlags,

    err_count: AtomicUsize,
    emitter: Lock<Box<dyn Emitter>>,
    continue_after_error: LockCell<bool>,

    // 所有记录的错误信息
    delayed_span_bugs: Lock<Vec<Diagnostic>>,

    taught_diagnostics: Lock<AHashSet<DiagnosticId>>,

    emitted_diagnostic_codes: Lock<AHashSet<DiagnosticId>>,

    // 该集合包含发出的每个错误信息的 hash,这些 hash 用于避免发出两次相同的错误。
    emitted_diagnostics: Lock<AHashSet<u128>>,
}

swc 中还创建了一个 HANDLER 全局变量,它还是一个线程局部变量,try_with_handler 方法中会创建一个 Handler 实例,并将实例注册到 HANDLER 全局变量上。这种设计使得开发 swc 扩展时,让开发者进行错误报告更加简单:

use swc_common::errors::HANDLER;

fn main() {
    HANDLER.with(|handler| {
        // 使用 HANDLER.with 访问当前文件的 Handler 实例。

        // struct_span_err 方法接收错误信息。
        // struct_span_err 方法接收的 span 用于定位错误代码的位置。

        // 还可以通过 span_note 方法提供一些附加信息。
        handler
            .struct_span_err(
                span,
                &format!("`{}` used as parameter more than once", js_word),
            )
            .span_note(
                old_span,
                &format!("previous definition of `{}` here", js_word),
            )
            .emit();
    });
}

处理流程

回到 Compilerprocess_js_file 方法,它的处理流程与其他编译器一般无二,也将经历词法分析、语法分析、代码生成等阶段,简略流程如下:

sequenceDiagram
    participant main
    participant swc_common
    participant swc_ecma_parser
    participant parser as swc_ecma_parser::parser
    participant lexer as swc_ecma_parser::lexer

    main->>+swc_common: Arc::<SourceMap>::default()
    swc_common-->>-main: cm
    main->>+swc_ecma_parser: Compiler::new(cm.clone())
    swc_ecma_parser-->>-main: c
    main->>+swc_common: cm.load_file()
    swc_common-->>-main: fm
    main->>+swc_ecma_parser: c.process_js_file(fm)

    swc_ecma_parser->>+lexer: Lexer::new(fm)
    lexer-->>-swc_ecma_parser: lexer

    swc_ecma_parser->>+parser: Parser::new_from(lexer)
    parser-->>-swc_ecma_parser: p
    swc_ecma_parser->>+parser: p.parse_module()

    parser->>+lexer: lexer.bump()
    lexer-->>-parser: token

    parser-->>-swc_ecma_parser: program_result

    swc_ecma_parser->>+swc_ecma_codegen: emit_with
    swc_ecma_codegen-->>-swc_ecma_parser: src

    swc_ecma_parser-->>-main: output

最后

这部分主要介绍 swc 最基本的使用和 SourceMapCompiler 两个结构体,下一部分会按照 process_js_file 方法的处理流程逐步展开 swc 的内部实现。

Dangerise commented 7 months ago

催更大佬

Dangerise commented 7 months ago

主要是swc的项目感觉有点乱,文档也少,想调用的话根本不知道怎么办

SyMind commented 7 months ago

@Dangerised 剩余文章你可以在掘金中看到

深入理解 swc - 第一部分,使用 https://juejin.cn/post/7218469497585336376 深入理解 swc - 第二部分,词法分析 https://juejin.cn/post/7280000052210876479 深入理解 swc - 第三部分,语法分析 https://juejin.cn/post/7283766466083471396

关于 swc 的调用,更多是关于访问者模式的使用,这部分我还没有写,计划在2月份写完。

PS:我其实都忘记我在 Github 中也同步记录了这些文章。

Dangerise commented 7 months ago

@SyMind 感谢大佬

watsonhaw5566 commented 1 week ago

学习,最近也在研究 Swc 了,感谢分享