Codegen の概要
ここまででコードをパースし,AST を生成し,transformer によって IR に変換するまでの流れを見てきました.
最後は IR からコードを生成する codegen について見ていきましょう.
ここまで見れればコンパイラに関してはかなりの部分を理解できるでしょう.
実装箇所
codegen (generator) の実装は以下のあたりにあります.
transformer と構成は似ていて,generate.ts
に generate
関数や CodegenContext
が実装されていて,generators
ディレクトリには各ノードのコード生成関数が実装されています.
99// IR -> JS codegen
100export function generate(
101 ir: RootIRNode,
102 options: CodegenOptions = {},
103): VaporCodegenResult {
22export class CodegenContext {
例の如く,CodegenContext
に関しては実際にコンポーネントのコード生成を追いながら必要なところを随時読んでいきましょう.
generate
まずは generate
関数に入ります.
99// IR -> JS codegen
100export function generate(
101 ir: RootIRNode,
102 options: CodegenOptions = {},
103): VaporCodegenResult {
code は buildCodeFragment
によって得られる push
という関数で次々 append していきます.
104 const [frag, push] = buildCodeFragment()
30export function buildCodeFragment(
31 ...frag: CodeFragment[]
32): [CodeFragment[], (...items: CodeFragment[]) => number] {
33 const push = frag.push.bind(frag)
34 return [frag, push]
35}
まずは render 関数のシグネチャを push します.
108 const functionName = 'render'
113 push(NEWLINE, `export function ${functionName}(_ctx) {`)
そして genBlockContent
によって ir からコードを生成します.
116 push(INDENT_START)
117 push(...genBlockContent(ir.block, context, true))
118 push(INDENT_END, NEWLINE)
template の宣言や import 文の宣言は render 関数の外で行われるので,これらは,preamble
として生成され,コードの先頭に追加されます.
126 const delegates = genDelegates(context)
127 const templates = genTemplates(ir.template, context)
128 const imports = genHelperImports(context)
129 const preamble = imports + templates + delegates
136 let [code, map] = codeFragmentToString(frag, context)
137 if (!inline) {
138 code = preamble + code
139 }
そしてこの code
が最終的なコードになります.
141 return {
142 code,
143 ast: ir,
144 preamble,
145 map: map && map.toJSON(),
146 helpers,
147 vaporHelpers,
148 }
それでは genBlockContent
を読んでいきましょう.
Tips: _sfc_render
について
出力コードを確認した時,render 関数の名前は render
ではなく _sfc_render
でした.
しかし,generate
関数内で render
として push しています.
実は,render
という関数は vite-plugin-vue
の実装によってのちに _sfc_render
にリライトされます.
なので,_sfc_render
という名前は compiler-vapor
には実は登場しないのです.
71 return {
72 ...result,
73 code: result.code.replace(
74 /\nexport (function|const) (render|ssrRender)/,
75 '\n$1 _sfc_$2',
76 ),
genBlockContent
実装は packages/compiler-vapor/src/generators/block.ts にあります.
36export function genBlockContent(
37 block: BlockIRNode,
38 context: CodegenContext,
39 root?: boolean,
40 customReturns?: (returns: CodeFragment[]) => CodeFragment[],
41): CodeFragment[] {
block.dynamic.children
から child を一つづ取り出して generate します.\
51 for (const child of dynamic.children) {
52 push(...genChildren(child, context, child.id!))
53 }
block.dynamic.children
は transformChildren
で生成されるもので,中身としては childContext.dynamic
がそのまま入る形になっています.
36 context.dynamic.children[i] = childContext.dynamic
改めて,flags 以外にどのような情報があるのか見ておくと,
246export interface IRDynamicInfo {
247 id?: number
248 flags: DynamicFlag
249 anchor?: number
250 children: IRDynamicInfo[]
251 template?: number
252}
id や template の index の情報が入っていることが分かります.
この情報を使って,genChildren
によりコードを生成します.
genChildren
genChildren
は packages/compiler-vapor/src/generators/template.ts に実装されています.
18export function genChildren(
19 dynamic: IRDynamicInfo,
20 context: CodegenContext,
21 from: number,
22 paths: number[] = [],
23): CodeFragment[] {
この関数では const n${id} = t${template}()
のようなコードが生成されます. つまり,今回でいうところの const n0 = t0()
のようなコードが生成されるわけです.
29 if (id !== undefined && template !== undefined) {
30 push(NEWLINE, `const n${id} = t${template}()`)
31 push(...genDirectivesForElement(id, context))
32 }
ここで,後々登場する nextSibling
や firstChild
のようなコードも生成されます.(今は読み飛ばします.)
genBlockContent の続き
children のコードが生成できたら.
次に operation と effect を generate します.
ここはまだ登場していませんが,テキストの書き換えやイベントハンドラの登録などのコードの生成です.
55 push(...genOperations(operation, context))
56 push(...genEffects(effect, context))
最後に,return
文の生成です.
block.returns
を map し,n${idx}
の識別子を生成しつつ return
文を生成します.
58 push(NEWLINE, `return `)
59
60 const returnNodes = returns.map(n => `n${n}`)
61 const returnsCode: CodeFragment[] =
62 returnNodes.length > 1
63 ? genMulti(DELIMITERS_ARRAY, ...returnNodes)
64 : [returnNodes[0] || 'null']
65 push(...(customReturns ? customReturns(returnsCode) : returnsCode))
なんと,ざっくり Codegen はこれで終わりです.
さて,これで単純なコンポーネントをコンパイルするためのコンパイラの実装は一通り追うことができました.
改めて,今回の目的と手順,残りやりたいことをまとめつつ次のステップに進みましょう!