Skip to content

Vapor Mode の IR

さて続いては IR です.
この辺りから Vapor Mode の固有実装になります.

compiler-vapor-ir

先に IR をみてから,transformer のソースコードリーディングに進むことにします.

IR とは

IR は Intermediate Representation の略で,中間表現のことです.
SFCDescriptorAST が概ねユーザー(Web アプリケーション開発者)の入力コードを構造化したものだったのに対し,IR は言わば「出力コードを構造化したもの」です.
IR の定義は ir/index.ts にあります.

packages/compiler-vapor/src/ir/index.ts

最初に読んだ小さいコンポーネントのコンパイラ結果を思い出して欲しいのですが,

js
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

これらを AST からそのまま出力するのはやや難しいです.
戦略として,上記のコードの表現するためのオブジェクトを用意 (IR) し,AST を操作することで IR を生成し,その IR を codegen に渡すことでプログラマブルにコンパイラを設計することができます.

実際に,上記のコンポーネントがどのような IR になるか試しに見てみます.
手元のコンパイラにログを仕込むことで確認します.

以下のあたりに transform 関数があるので,transform 後の ir を出力してみます.

76  const ir = transform(
77    ast,
78    extend({}, resolvedOptions, {
79      nodeTransforms: [
80        ...nodeTransforms,
81        ...(options.nodeTransforms || []), // user transforms
82      ],
83      directiveTransforms: extend(
84        {},
85        directiveTransforms,
86        options.directiveTransforms || {}, // user transforms
87      ),
88    }),
89  )
json
{
  "type": 0,
  "node": {
    "type": 0,
    "source": "\n  <p>Hello, Vapor!</p>\n",
    "children": [
      {
        "type": 1,
        "tag": "p",
        "ns": 0,
        "tagType": 0,
        "props": [],
        "children": [
          {
            "type": 2,
            "content": "Hello, Vapor!"
          }
        ]
      }
    ],
    "helpers": {},
    "components": [],
    "directives": [],
    "hoists": [],
    "imports": [],
    "cached": [],
    "temps": 0
  },
  "source": "\n  <p>Hello, Vapor!</p>\n",
  "template": ["<p>Hello, Vapor!</p>"],
  "component": {},
  "directive": {},
  "block": {
    "type": 1,
    "node": {
      "type": 0,
      "source": "\n  <p>Hello, Vapor!</p>\n",
      "children": [
        {
          "type": 1,
          "tag": "p",
          "ns": 0,
          "tagType": 0,
          "props": [],
          "children": [
            {
              "type": 2,
              "content": "Hello, Vapor!"
            }
          ]
        }
      ],
      "helpers": {},
      "components": [],
      "directives": [],
      "hoists": [],
      "imports": [],
      "cached": [],
      "temps": 0
    },
    "dynamic": {
      "flags": 1,
      "children": [
        {
          "flags": 1,
          "children": [
            {
              "flags": 1,
              "children": []
            }
          ],
          "id": 0,
          "template": 0
        }
      ]
    },
    "effect": [],
    "operation": [],
    "returns": [0]
  }
}

これが実際の IR です.
IR の type が enum で表現されている兼ね合いで数値になってしまい分かりづらいので,具体的な IR Node 名に置き換えてみつつ,余計なところを削除してみます.
そうすると以下のようなものになります.

json
{
  "type": "RootIRNode",
  "node": {
    "type": "RootNode",
    "source": "\n  <p>Hello, Vapor!</p>\n",
    "children": [
      {
        "type": "ElementNode",
        "tag": "p",
        "ns": 0,
        "tagType": 0,
        "children": [
          {
            "type": "TextNode",
            "content": "Hello, Vapor!"
          }
        ]
      }
    ],
    "temps": 0
  },
  "source": "\n  <p>Hello, Vapor!</p>\n",
  "template": ["<p>Hello, Vapor!</p>"],
  "block": {
    "type": "BlockIRNode",
    "node": {
      "type": "ElementNode",
      "source": "\n  <p>Hello, Vapor!</p>\n",
      "children": [
        {
          "type": "ElementNode",
          "tag": "p",
          "ns": 0,
          "tagType": "Element",
          "children": [
            {
              "type": "TextNode",
              "content": "Hello, Vapor!"
            }
          ]
        }
      ],
      "temps": 0
    },
    "returns": [0]
  }
}

まずルートに RootIRNode があります.これが IR の Root になります.
この RootIRNodenode, template, block の情報を持っていて,
node は AST の RootNode になっています.

56export interface RootIRNode {
57  type: IRNodeTypes.ROOT
58  node: RootNode
59  source: string
60  template: string[]
61  component: Set<string>
62  directive: Set<string>
63  block: BlockIRNode
64}

そして,block には BlockIRNode があり,これが Vapor で扱う要素の単位である,Block を表現したものになります.

47export interface BlockIRNode extends BaseIRNode {
48  type: IRNodeTypes.BLOCK
49  node: RootNode | TemplateChildNode
50  dynamic: IRDynamicInfo
51  effect: IREffect[]
52  operation: OperationNode[]
53  returns: number[]
54}

ここで少し Block の説明です.

Block とは

Block は Vapor Mode で扱う単位です.
非 Vapor で言うところの VNode (仮想 DOM の Node) に近いものです.

Block の定義は runtime-vapor にあるので少しみてみましょう.

26export type Block = Node | Fragment | ComponentInternalInstance | Block[]
27export type Fragment = {
28  nodes: Block
29  anchor?: Node
30  [fragmentKey]: true
31}

これをみると Block が概ねどういうものがわかるはずです.
Block は Node (DOM Node), Fragment, Component または Block の配列を取ります.
基本的には Vapor はこの Block という単位で UI を構築していきます.

例えば,

ts
const t0 = template("<p>Hello, Vapor!</p>");
const n0 = t0();

n0 は Node (Element) という Block になります.
詳しくはまたランタイムの解説の方でみますが,ちらっと template という関数を見てましょう.

2export function template(html: string) {
3  let node: ChildNode
4  const create = () => {
5    // eslint-disable-next-line no-restricted-globals
6    const t = document.createElement('template')
7    t.innerHTML = html
8    return t.content.firstChild!
9  }
10  return (): Node => (node || (node = create())).cloneNode(true)
11}

単に,innerHTML に template を挿入し,その firstChild を返しているだけです.
つまりこれはただの ElementNode です.

時には Element だったり,Component だったり,またそれらの配列から構成されたものだったり,Block というのはこの UI の構築するための最小単位です.
将来的にはこの Block に対してイベントリスナを登録したり,テキストの更新を行ったり,といった操作を行うことになります.

IR の定義はこれからいろんなコンポーネントを読んでいきながら登場したものを随時読んでいこうと思うので,ここで IR の説明は終わりにします.
とりあえず,IR というものが雰囲気どういうものだったというのと,最初に読んでいる小さいコンポーネントがどのような IR で表現されているのかということがわかれば十分です.