← 返回首页

ClojureScript 编译器核心原理

2026/3/23

ClojureScript 编译器 函数式编程

ClojureScript 编译器核心原理

从源码层面理解 ClojureScript 如何将 Lisp 编译为 JavaScript。


编译流程概览

ClojureScript 的编译流程是经典的三阶段设计:

CLJS 源码 → Reader → AST (analyzer) → JavaScript (compiler)
阶段输入输出职责
Reader字符串数据结构词法分析,处理宏
Analyzer数据结构AST语义分析,类型推断
CompilerASTJavaScript代码生成

核心编译器文件:src/main/clojure/cljs/compiler.cljc(约 74KB)


emit* 多方法:编译的分发核心

ClojureScript 使用 Clojure 的**多方法(multimethod)**实现编译分发:

(defmethod emit* :if [...])      ; 条件表达式
(defmethod emit* :fn [...])      ; 函数定义
(defmethod emit* :def [...])     ; 变量定义
(defmethod emit* :invoke [...])  ; 函数调用
(defmethod emit* :let [...])     ; let 绑定
(defmethod emit* :loop [...])    ; loop/recur
(defmethod emit* :recur [...])   ; 尾递归
(defmethod emit* :try [...])     ; 异常处理
(defmethod emit* :throw [...])   ; 抛出异常
(defmethod emit* :new [...])     ; 构造对象

每个 AST 节点类型有对应的 emit* 实现。这种设计让编译器可扩展——添加新节点类型只需定义新的多方法实现。


if 表达式编译

ClojureScript 的 if 需要处理 truthy/falsey 语义:

源码位置

compiler.cljc 第 687-702 行

实现逻辑

(defmethod emit* :if
  [{:keys [test then else env unchecked]}]
  (let [context (:context env)]
    ;; 1. 常量折叠优化
    (if (and (constant? test) (not unchecked))
      (if (truthy? test)
        (emit then)  ; 条件永远为真,只编译 then
        (emit else)) ; 条件永远为假,只编译 else
      
      ;; 2. 正常编译
      (do
        (emit-wrap context
          (emits "((cljs.core.truth_(" test ")) ? " then " : " else ")"))))))

生成的 JavaScript

// 表达式上下文(需要返回值)
(cljs.core.truth_(test) ? then : else)

// 语句上下文(不需要返回值)
if(cljs.core.truth_(test)){
  then
} else {
  else
}

关键点

  1. 常量折叠:如果 test 是常量,编译时直接选择分支,零运行时开销
  2. truth_ 包装:Clojure 的 truthy 语义(nil 和 false 为假,其他为真)与 JavaScript 不同,需要包装
  3. unchecked 优化:如果用户标记 ^boolean 类型,跳过 truth_ 包装

多元函数编译

ClojureScript 支持同名函数多个 arity(参数个数不同),这是 Lisp 的经典特性。

单一 arity

(defn add [x y] (+ x y))

生成简单的 JavaScript 函数:

var add = function(x, y) {
  return (x + y);
};

多 arity

(defn greet
  ([name] (str "Hello, " name))
  ([name greeting] (str greeting ", " name)))

生成 switch 分发

(function() {
  var greet = null;
  
  // 1 参数版本
  var greet__1 = function(name) {
    return ["Hello, ", cljs.core.str(name)].join('');
  };
  
  // 2 参数版本
  var greet__2 = function(name, greeting) {
    return [greeting, ", ", cljs.core.str(name)].join('');
  };
  
  // 分发函数
  greet = function(name, greeting) {
    switch(arguments.length) {
      case 1: return greet__1.call(this, name);
      case 2: return greet__2.call(this, name, greeting);
    }
    throw new Error('Invalid arity: ' + arguments.length);
  };
  
  // 附加 arity 信息(允许直接调用)
  greet.cljs$core$IFn$_invoke$arity$1 = greet__1;
  greet.cljs$core$IFn$_invoke$arity$2 = greet__2;
  
  return greet;
})()

关键设计

  1. 闭包封装:整个结构包在 IIFE 中,避免污染全局
  2. arity 缓存:每个 arity 版本附加到函数对象上(cljs$core$IFn$_invoke$arity$N
  3. 直接调用优化:知道 arity 时可以直接调用特定版本,避免 switch 开销

变参函数编译

ClojureScript 支持变参函数(variadic functions):

(defn sum [x & rest]
  (apply + x rest))

生成:

var sum = function(x) {
  // 将剩余参数转为 IndexedSeq
  var rest = new cljs.core.IndexedSeq(
    Array.prototype.slice.call(arguments, 1),  // 从第 2 个参数开始
    0,
    null
  );
  return cljs.core.apply.cljs$core$IFn$_invoke$arity$3(
    cljs.core._PLUS_,
    x,
    rest
  );
};

为什么是 IndexedSeq?

Clojure 的序列是惰性的,IndexedSeq 是对 JavaScript 数组的惰性包装。这保持了 Clojure 的语义,同时避免了不必要的数组复制。


编译上下文

编译器根据上下文生成不同风格的代码:

上下文含义生成的代码
:expr表达式需要返回值,用三元运算符等
:statement语句不需要返回值,用 if/else 语句
:return返回需要 return 语句

示例

(if test then else)
// :expr 上下文
(cljs.core.truth_(test) ? then : else)

// :statement 上下文
if(cljs.core.truth_(test)){ then } else { else }

// :return 上下文
return (cljs.core.truth_(test) ? then : else);

辅助函数

munge:符号转换

Clojure 符号允许 -.,但 JavaScript 不允许。munge 函数处理转换:

Clojure 符号JavaScript 标识符
my-fnmy_fn
str.lengthstr.length
classclass$(保留字加 $)

emit-wrap:上下文包装

(defn emit-wrap [context body]
  (case context
    :expr (emits "(" body ")")
    :return (emits "return " body ";")
    :statement body))

emits / emitln

(emits "a" "b" "c")  ; 输出 "abc"(不换行)
(emitln "a" "b" "c") ; 输出 "abc\n"(换行)

loop/recur:零开销尾递归

ClojureScript 的 loop/recur 编译为真正的循环,无函数调用开销:

(loop [x 0]
  (if (< x 10)
    (recur (inc x))
    x))

生成:

(function() {
  while(true) {
    if((x < 10)) {
      var G__x = (x + 1);
      x = G__x;
      continue;
    } else {
      return x;
    }
    break;
  }
})()

关键点

  1. while(true):无限循环
  2. continuerecur 变成 continue,更新绑定后继续
  3. break:出口条件触发时跳出
  4. 零开销:无函数调用,纯循环

let 编译

let 编译为 IIFE 或直接赋值,取决于上下文:

(let [x 1
      y 2]
  (+ x y))
// 表达式上下文
(function() {
  var x = 1;
  var y = 2;
  return (x + y);
})()

// 语句上下文
var x = 1;
var y = 2;
(x + y);

实用技巧

1. 查看编译输出

(require '[cljs.compiler :as comp])

(comp/emit '(if true 1 2))
;; => "((cljs.core.truth_(true))?1:2)"

2. 理解命名规范

  • G__ 前缀:编译器生成的临时变量
  • $ 后缀:保留字转义
  • _ 替换:连字符转义

3. 性能提示

  • 标记 ^boolean 类型可以跳过 truth_ 包装
  • 单 arity 函数比多 arity 更快
  • loop/recur 比递归函数调用更高效

小结

ClojureScript 编译器的核心设计:

  1. 多方法分发 - 可扩展的 AST 处理
  2. 上下文感知 - 根据上下文生成不同代码
  3. 零开销抽象 - loop/recur 编译为循环
  4. 语义保持 - truthy/falsey、多元函数等语义在 JavaScript 中正确实现

理解编译器原理有助于:

  • 写出更高效的 ClojureScript 代码
  • 调试编译问题
  • 贡献编译器改进

参考资料

📝 文章反馈

你的反馈能帮助我写出更好的文章