Clojure 运行原理之编译器剖析 | Keep Writing Codes
<p>Clojure is a compiled language, yet remains completely dynamic — every feature supported by Clojure is supported at runtime.</p> <p>Rich Hickey https://clojure.org/</p> <p>这里的 runtime 指的是 JVM,JVM 之初是为运行 Java 语言而设计,而现在已经发展成一重量级平台,除了 Clojure 之外, 很多动态语言 也都选择基于 JVM 去实现。</p> <p>为了更加具体描述 Clojure 运行原理,会分两篇文章来介绍。</p> <p>本文为第一篇,涉及到的主要内容有:编译器工作流程、Lisp 的宏机制。</p> <p>第二篇将主要分析 Clojure 程序编译成的 bytecode 如何保证动态语言的特性以及如何加速 Clojure 程序执行速度,这会涉及到 JVM 的类加载机制、反射机制。</p> <h2>编译型 VS. 解释型</h2> <p>SO 上有个问题 Is Clojure compiled or interpreted ,根据本文开始部分的官网引用,说明 Clojure 是门编译型语言,就像 Java、Scala。但是 Clojure 与 Java 不一样的地方在于,Clojure 可以在运行时进行编译然后加载,而 Java 明确区分编译期与运行期。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f457ac8d72b2e50a21418395ca9d5e9f.png"> <img src="https://simg.open-open.com/show/3d94680e5fc6b478c2f7bc90b76124ff.png"></p> <h2>编译器工作流程</h2> <p>与解释型语言里的解释器类似,编译型语言通过编译器(Compiler)来将源程序编译为字节码。一般来说,编译器包括 <a href="/misc/goto?guid=4959736053537168920" rel="nofollow,noindex">两</a>个部分 :</p> <ul> <li>前端: 词法分析 —> 语法分析 —> 语义分析</li> <li>后端: 分析、优化 —> 目标代码生成</li> </ul> <p>Clojure 的编译器也遵循这个模式,大致可以分为以下两个模块:</p> <ul> <li>读取 Clojure 源程序 —> 分词 —> 构造 S-表达式,由 LispReader.java 类实现</li> <li>宏扩展 —> 语义分析 —> 生成 JVM 字节码,由 Compiler.java 类实现</li> </ul> <p style="text-align:center"><img src="https://simg.open-open.com/show/88c431c2680440d1f4586b0f78218039.png"></p> <p>上图给出了不同阶段的输入输出,具体实现下面一一讲解。</p> <h3><a href="/misc/goto?guid=4959736053631131655" rel="nofollow,noindex">LispReader.java</a></h3> <p>一般来说,具有复杂语法的编程语言会把词法分析与语法分析分开实现为 Lexer 与 Parser,但在 Lisp 家族中,源程序的语法就已经是 AST 了,所以会把 Lexer 与 Parser 合并为一个过程 Reader, 核心代码 实现如下:</p> <pre> <code class="language-java">for (; ; ) { if (pendingForms instanceof List && !((List) pendingForms).isEmpty()) return ((List) pendingForms).remove(0); int ch = read1(r); while (isWhitespace(ch)) ch = read1(r); if (ch == -1) { if (eofIsError) throw Util.runtimeException("EOF while reading"); return eofValue; } if (returnOn != null && (returnOn.charValue() == ch)) { return returnOnValue; } if (Character.isDigit(ch)) { Object n = readNumber(r, (char) ch); return n; } IFn macroFn = getMacro(ch); if (macroFn != null) { Object ret = macroFn.invoke(r, (char) ch, opts, pendingForms); //no op macros return the reader if (ret == r) continue; return ret; } if (ch == '+' || ch == '-') { int ch2 = read1(r); if (Character.isDigit(ch2)) { unread(r, ch2); Object n = readNumber(r, (char) ch); return n; } unread(r, ch2); } String token = readToken(r, (char) ch); return interpretToken(token); } </code></pre> <p>Reader 的行为是由内置构造器(目前有数字、字符、Symbol 这三类)与一个称为 read table 的扩展机制(getMacro)驱动的, read table 里面每项记录提供了由特性符号(称为 macro characters )到特定读取行为(称为 reader macros )的映射。</p> <p>与 Common Lisp 不同,普通用户无法扩展 Clojure 里面的 read table 。关于扩展 read table 的好处,可以参考 StackOverflow 上的 What advantage does common lisp reader macros have that Clojure does not have? 。Rich Hickey 在 <a href="/misc/goto?guid=4959736053712781808" rel="nofollow,noindex">一 Google Group</a> 里面有阐述不开放 read table 的理由,这里摘抄如下:</p> <p>I am unconvinced that reader macros are needed in Clojure at this</p> <p>time. They greatly reduce the readability of code that uses them (by</p> <p>people who otherwise know Clojure), encourage incompatible custom mini-</p> <p>languages and dialects (vs namespace-partitioned macros), and</p> <p>complicate loading and evaluation.</p> <p>To the extent I’m willing to accommodate common needs different from</p> <p>my own (e.g. regexes), I think many things that would otherwise have</p> <p>forced people to reader macros may end up in Clojure, where everyone</p> <p>can benefit from a common approach.</p> <p>Clojure is arguably a very simple language, and in that simplicity</p> <p>lies a different kind of power.</p> <p>I’m going to pass on pursuing this for now,</p> <p>截止到 Clojure 1.8 版本,共有如下九个 macro characters :</p> <pre> <code class="language-java">Quote (') Character (\) Comment (;) Deref (@) Metadata (^) Dispatch (#) Syntax-quote (`) Unquote (~) Unquote-splicing (~@) </code></pre> <h3><a href="/misc/goto?guid=4959736053800414934" rel="nofollow,noindex">Compiler.java</a></h3> <p>Compiler 类主要有三个入口函数:</p> <ul> <li><a href="/misc/goto?guid=4959736053877272022" rel="nofollow,noindex">compile</a> ,当调用 clojure.core/compile 时使用</li> <li><a href="/misc/goto?guid=4959736053970756095" rel="nofollow,noindex">load</a> ,当调用 clojure.core/require 、 clojure.core/use 时使用</li> <li><a href="/misc/goto?guid=4959736054056537913" rel="nofollow,noindex">eval</a> ,当调用 clojure.core/eval 时使用</li> </ul> <p style="text-align:center"><img src="https://simg.open-open.com/show/fc1510938a62e8ba485217d3b2071a86.png"></p> <p>这三个入口函数都会依次调用 macroexpand 、 analyze 方法,生成 Expr 对象,compile 函数还会额外调用 emit 方法生成 bytecode。</p> <p>macroexpand</p> <p>Macro 毫无疑问是 Lisp 中的屠龙刀,可以在 编译时 自动生成代码:</p> <pre> <code class="language-java">static Object macroexpand(Object form) { Object exf = macroexpand1(form); if (exf != form) return macroexpand(exf); return form; } </code></pre> <p>macroexpand1 函数进行主要的扩展工作,它会调用 isMacro 判断当前 Var 是否为一个宏,而这又是通过检查 var 是否为一个函数,并且元信息中 macro 是否为 true 。</p> <p>Clojure 里面通过 defmacro 函数创建宏,它会调用 var 的 setMacro 函数来设置元信息 macro 为 true 。</p> <p>analyze</p> <pre> <code class="language-java">interfaceExpr{ Object eval(); void emit(C context, ObjExpr objx, GeneratorAdapter gen); boolean hasJavaClass(); Class getJavaClass(); } private static Expr analyze(C context, Object form, String name) </code></pre> <p>analyze 进行主要的语义分析, form 参数即是宏展开后的各种数据结构(String/ISeq/IPersistentList 等),返回值类型为 Expr ,可以猜测出, Expr 的子类是程序的主体,遵循模块化的编程风格,每个子类都知道如何对其自身求值(eval)或输出 bytecode(emit)。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a2bc3a902b7ebeeea6d9e23867bcd572.png"></p> <p>emit</p> <p>这里需要明确一点的是,Clojure 编译器并没有把 Clojure 代码转为相应的 Java 代码,而是借助 bytecode 操作库 ASM 直接生成可运行在 JVM 上的 bytecode。</p> <p>根据 JVM bytecode 的规范,每个 .class 文件都必须由类组成,而 Clojure 作为一个函数式语言,主体是函数,通过 namespace 来封装、隔离函数,你可能会想当然的认为每个 namespace 对应一个类,namespace 里面的每个函数对应类里面的方法,而实际上并不是这样的,根据 Clojure 官方文档 ,对应关系是这样的:</p> <ul> <li>每个文件、函数、 gen-class 都会生成一个 .class 文件</li> <li>每个文件生成一个 <filename>__init 的加载类</li> <li>gen-class 生成固定名字的类,方便与 Java 交互</li> </ul> <p>生成的 bytecode 会在本系列第二篇文章中详细介绍,敬请期待。</p> <p>eval</p> <p>每个 Expr 的子类都有 eval 方法的相应实现。下面的代码片段为 LispExpr.eval 的实现,其余子类实现也类似,这里不在赘述。</p> <pre> <code class="language-java">public Object eval() { IPersistentVector ret = PersistentVector.EMPTY; for (int i = 0; i < args.count(); i++) // 这里递归的求列表中每项的值 ret = (IPersistentVector) ret.cons(((Expr) args.nth(i)).eval()); return ret.seq(); } </code></pre> <h2>总结</h2> <p>之前看 SICP 后实现过几个解释器,但是相对来说都比较简单,通过分析 Clojure 编译器的实现,加深了对eval-apply 循环的理解,还有一点就是揭开了宏的真实面貌,之前一直认为宏是个很神奇的东西,其实它只不过是 编译时运行的函数 而已,输入与输出的内容既是构成程序的数据结构,同时也是程序内在的 AST。</p> <h2>参考</h2> <ul> <li><a href="/misc/goto?guid=4959736054142805256" rel="nofollow,noindex">Decompiling Clojure II, the Compiler</a></li> <li><a href="/misc/goto?guid=4959736054224445808" rel="nofollow,noindex">Clojure Compilation: Parenthetical Prose to Bewildering Bytecode</a></li> <li><a href="/misc/goto?guid=4959736054307735839" rel="nofollow,noindex">The ClojureScript Compilation Pipeline</a></li> <li><a href="/misc/goto?guid=4959736054389173438" rel="nofollow,noindex">Ahead-of-time Compilation and Class Generation</a></li> <li><a href="/misc/goto?guid=4959736054480327010" rel="nofollow,noindex">The Reader</a></li> </ul> <p> </p> <p> </p> <p>来自:http://liujiacai.net/blog/2017/02/05/clojure-compiler-analyze/</p> <p> </p>
本文由用户 karoizi 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!