Skip to content

Overview of Transformer Part 2

I split it because it got a bit long, but here's the continuation.
Although it's called an overview, before I realized it, I was reading the details of the specific implementation, but well, perhaps it's a good natural introduction (laughs).

Up to the previous page, we looked at the implementation that generates the template string passed as an argument to the template function.
Here, we'll look at index management of Block and so on.

registerTemplate

There is a function called registerTemplate in TransformContext.

131  registerTemplate(): number {
132    if (!this.template) return -1
133    const id = this.pushTemplate(this.template)
134    return (this.dynamic.template = id)
135  }

This registerTemplate calls a function called pushTemplate.

123  pushTemplate(content: string): number {
124    const existing = this.ir.template.findIndex(
125      template => template === content,
126    )
127    if (existing !== -1) return existing
128    this.ir.template.push(content)
129    return this.ir.template.length - 1
130  }

Templates (strings) are registered into this.ir.template (array).

this.ir is a RootIRNode.

87  constructor(
88    public ir: RootIRNode,

That is, here.

56export interface RootIRNode {
57  type: IRNodeTypes.ROOT
58  node: RootNode
59  source: string
60  template: string[]

And, this registerTemplate is called in three places.

  1. At the end of transformNode (only when root)
264  if (context.node.type === NodeTypes.ROOT) {
265    context.registerTemplate()
266  }
  1. After processing children in transformChildren (only when it's a Fragment)
22    if (isFragment) {
23      childContext.reference()
24      childContext.registerTemplate()
  1. When context.enterBlock is called (onExit)
104    return () => {
105      // exit
106      this.registerTemplate()
107      this.block = block
108      this.template = template
109      this.dynamic = dynamic
110      this.childrenTemplate = childrenTemplate
111      this.slots = slots
112      isVFor && this.inVFor--
113    }

context.enterBlock is a function called when entering a Block in transformVFor or transformVIf.
We'll look at this when we see the implementation of compiling v-for and v-if, but for now, it's fine to just grasp 1 and 2.

Regarding 1, there's nothing special, I think.
In the small component we're reading now, the template is registered here.
In other words, at this point, this.ir.template is in the state of ["<p>Hello, Vapor!</p>"].

If we have this, we know the index of the template, so,

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

It seems we can generate such code. (We'll actually see this again during codegen.)

In the case of 2, the Fragment, it's when we write a template like:

vue
<template>
  <p>Hello, Vapor 1</p>
  <p>Hello, Vapor 2</p>
  <p>Hello, Vapor 3</p>
</template>

In this case, three templates are registered at timing 2.

js
// this.ir.template
["<p>Hello, Vapor 1</p>", "<p>Hello, Vapor 2</p>", "<p>Hello, Vapor 3</p>"];

const t0 = template("<p>Hello, Vapor 1</p>");
const t1 = template("<p>Hello, Vapor 2</p>");
const t2 = template("<p>Hello, Vapor 3</p>");

Return Value of the render Function

Returning to the discussion about

vue
<template>
  <p>Hello, Vapor!</p>
</template>

Let's look back again at the IR obtained from this.
(Unnecessary parts are omitted)

json
{
  "type": "RootIRNode",
  "template": ["<p>Hello, Vapor!</p>"],
  "block": {
    "type": "BlockIRNode",
    "returns": [0]
  }
}

Looking closely, there's something rather suspicious called "returns": [0].
From this information, we can understand that the node at index 0 seems to be the return value of the render function.

This is done in transformChildren.

26      if (
27        !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28        childContext.dynamic.flags & DynamicFlag.INSERT
29      ) {
30        context.block.returns.push(childContext.dynamic.id!)
31      }

Under certain conditions, the id of that node is pushed into block.returns.
This id is calculated from the length when pushTemplate is called.

123  pushTemplate(content: string): number {
124    const existing = this.ir.template.findIndex(
125      template => template === content,
126    )
127    if (existing !== -1) return existing
128    this.ir.template.push(content)
129    return this.ir.template.length - 1
130  }
131  registerTemplate(): number {
132    if (!this.template) return -1
133    const id = this.pushTemplate(this.template)
134    return (this.dynamic.template = id)
135  }

And,

Under certain conditions

As to what these conditions are, the first condition is when isFragment is true.
This is when the node executing transformChildren is one of Root, Element, Template, or Component.

10  const isFragment =
11    node.type === NodeTypes.ROOT ||
12    (node.type === NodeTypes.ELEMENT &&
13      (node.tagType === ElementTypes.TEMPLATE ||
14        node.tagType === ElementTypes.COMPONENT))

The second condition is when dynamic.flags is not NON_TEMPLATE or is INSERT.
(Note: It might look a bit confusing at first glance, but since it's a bitmask, each flag is not exclusive.)

27        !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28        childContext.dynamic.flags & DynamicFlag.INSERT

When these two conditions are met, id is pushed into block.returns.
I think the first condition is fine.
Regarding the second condition, about dynamic.flags.

dynamic.flags

dynamic is a property of TransformContext.

74  dynamic: IRDynamicInfo = this.ir.block.dynamic
246export interface IRDynamicInfo {
247  id?: number
248  flags: DynamicFlag
249  anchor?: number
250  children: IRDynamicInfo[]
251  template?: number
252}

Since context.ir holds this information, its reference is held in TransformContext.
In particular, this time, the information called DynamicFlag that IRDynamicInfo holds is important, so let's focus on that.

DynamicFlag is a flag that represents what kind of properties a node has.
The properties are as described in the comments.

230export enum DynamicFlag {
231  NONE = 0,
232  /**
233   * This node is referenced and needs to be saved as a variable.
234   */
235  REFERENCED = 1,
236  /**
237   * This node is not generated from template, but is generated dynamically.
238   */
239  NON_TEMPLATE = 1 << 1,
240  /**
241   * This node needs to be inserted back into the template.
242   */
243  INSERT = 1 << 2,
244}

Since it's expressed with bitmasking, each property can coexist.

Let's see when each flag is marked.

DynamicFlag.REFERENCED

This node is referenced and needs to be saved as a variable.

As stated.

There are two places where DynamicFlag.REFERENCED is set.

  1. When context.reference is called.
117  reference(): number {
118    if (this.dynamic.id !== undefined) return this.dynamic.id
119    this.dynamic.flags |= DynamicFlag.REFERENCED
120    return (this.dynamic.id = this.increaseId())
121  }
  1. When IRDynamicInfo is generated via newDynamic (as a default value)
20export const newDynamic = (): IRDynamicInfo => ({
21  flags: DynamicFlag.REFERENCED,
22  children: [],
23})

First, in case 1, context.reference is called in quite a few places.
For example, it's called in the conditional branch when isFragment is true in transformChildren we looked at earlier.

22    if (isFragment) {
23      childContext.reference()
24      childContext.registerTemplate()

Then, as for what this flag is used for, it's to generate id when generating code.
We'll look at this in detail later when we see the implementation of codegen, but nodes with this flag set will generate an id and store it in a variable.

needs to be saved as a variable.

As stated.

It's understandable that this flag is set in isFragment in transformChildren.
By doing this,

js
const n0 = t0();
const n1 = t1();
const n2 = t2();

We can output code that holds in variables like n${id}.
Conversely, nodes that don't need to be stored in variables don't have this flag set.

In this case,

js
const t0 = template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0(); // here
  return n0;
}

Since we need to hold n0 in a variable, this flag is set.

DynamicFlag.NON_TEMPLATE

Next is DynamicFlag.NON_TEMPLATE.
Whether this flag is set is quite important; if it's not set, id will be pushed into block.returns.

26      if (
27        !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28        childContext.dynamic.flags & DynamicFlag.INSERT
29      ) {
30        context.block.returns.push(childContext.dynamic.id!)
31      }

This node is not generated from template, but is generated dynamically.

As stated, it seems this flag is set for nodes that are not generated from templates but are generated dynamically.

For example, this flag is set in transformComponentElement, transformSlotOutlet, transformVFor, etc.

96  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
31  context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
50  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT

Skipping a bit, an important point used in combination with this flag is DynamicFlag.INSERT.

DynamicFlag.INSERT

Whether to push the id into returns is first determined by checking whether DynamicFlag.NON_TEMPLATE is not set.
If it's not set, it is pushed into returns at this point.

If it is set, we check whether DynamicFlag.INSERT is set.

Looking at

96  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
31  context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
50  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT

you can see that Component, SlotOutlet, and v-for have this flag set from the start.

However,

40  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
115  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE

do not have this flag set at this point.

Regarding if, in the case of v-if (not v-else-if or v-else), this flag is set.

41  if (dir.name === 'if') {
42    const id = context.reference()
43    context.dynamic.flags |= DynamicFlag.INSERT

And in the case of inserted slots like <template #foo>, this flag is not set.

110function transformTemplateSlot(
111  node: ElementNode,
112  dir: VaporDirectiveNode,
113  context: TransformContext<ElementNode>,
114) {
115  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE

In this way, they select what to push into block.returns and what not to.


In the case of our small component, since the DynamicFlag.NON_TEMPLATE flag is not set, id is pushed into block.returns. With this, we've generated (transformed) all the IR that seems necessary for codegen!

Next, let's look at the implementation of codegen!