Skip to content

Overview of Codegen

Up to this point, we've looked at the process of parsing code, generating an AST, and converting it into IR via the transformer.
Finally, let's look at codegen, which generates code from the IR.
By understanding this, you'll have a substantial grasp of the compiler.

compiler vapor codegen

Implementation Locations

The implementation of codegen (generator) can be found in the following areas:

The structure is similar to the transformer; generate.ts implements the generate function and CodegenContext, and the generators directory contains code generation functions for each node.

99// IR -> JS codegen
100export function generate(
101  ir: RootIRNode,
102  options: CodegenOptions = {},
103): VaporCodegenResult {
22export class CodegenContext {

As usual, let's read about CodegenContext as needed while following the code generation of the component.

generate

First, let's enter the generate function.

99// IR -> JS codegen
100export function generate(
101  ir: RootIRNode,
102  options: CodegenOptions = {},
103): VaporCodegenResult {

The code is appended sequentially using the push function obtained from buildCodeFragment.

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}

First, we push the signature of the render function.

108  const functionName = 'render'
113    push(NEWLINE, `export function ${functionName}(_ctx) {`)

Then, we generate code from the IR using genBlockContent.

116  push(INDENT_START)
117  push(...genBlockContent(ir.block, context, true))
118  push(INDENT_END, NEWLINE)

Since the declarations of templates and import statements are done outside the render function, these are generated as preamble and added to the beginning of the code.

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  }

This code becomes the final code.

141  return {
142    code,
143    ast: ir,
144    preamble,
145    map: map && map.toJSON(),
146    helpers,
147    vaporHelpers,
148  }

Now, let's read genBlockContent.

Tips: About _sfc_render

When checking the output code, the name of the render function was _sfc_render instead of render.
However, in the generate function, it is pushed as render.

In fact, the render function is later rewritten to _sfc_render by the implementation of vite-plugin-vue.
Therefore, the name _sfc_render does not actually appear in compiler-vapor.

71  return {
72    ...result,
73    code: result.code.replace(
74      /\nexport (function|const) (render|ssrRender)/,
75      '\n$1 _sfc_$2',
76    ),

genBlockContent

The implementation is located in 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[] {

We take out each child from block.dynamic.children and generate code.

51  for (const child of dynamic.children) {
52    push(...genChildren(child, context, child.id!))
53  }

block.dynamic.children is generated in transformChildren, and its content directly includes childContext.dynamic.

36    context.dynamic.children[i] = childContext.dynamic

Looking again at what information besides flags is included:

246export interface IRDynamicInfo {
247  id?: number
248  flags: DynamicFlag
249  anchor?: number
250  children: IRDynamicInfo[]
251  template?: number
252}

We can see that it includes information like id and template index.
Using this information, we generate code with genChildren.

genChildren

genChildren is implemented in packages/compiler-vapor/src/generators/template.ts.

18export function genChildren(
19  dynamic: IRDynamicInfo,
20  context: CodegenContext,
21  from: number,
22  paths: number[] = [],
23): CodeFragment[] {

This function generates code like const n${id} = t${template}().
In this case, it generates code like const n0 = t0().

29  if (id !== undefined && template !== undefined) {
30    push(NEWLINE, `const n${id} = t${template}()`)
31    push(...genDirectivesForElement(id, context))
32  }

Here, code like nextSibling and firstChild, which will appear later, is also generated. (You can skip this for now.)

Continuing genBlockContent

Once the code for children is generated.

Next, we generate operations and effects.
These haven't appeared yet, but they involve generating code for things like text updates and event handler registrations.

55  push(...genOperations(operation, context))
56  push(...genEffects(effect, context))

Finally, we generate the return statement.

We map over block.returns, generate identifiers like n${idx}, and generate the return statement.

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))

Surprisingly, that's it for Codegen.
Now, we've been able to follow the compiler implementation needed to compile a simple component.
Let's summarize our objectives, steps, and remaining tasks, and proceed to the next step!