Source: lib/compiler/ir/nodes.js

/**
 * @file nodes.js
 * @description The template intermediate representation, and the vocabulary the
 * front and back halves of the compiler agree on.
 *
 * ## Why an IR exists
 *
 * Avenx used to compile a template by rewriting it into different markup.
 * `<@for item in items>` became `<template data-ax-for="items"
 * data-ax-as="item">`, `<@suspense>` became `<div data-ax-suspense>`, and the
 * runtime rediscovered what each of them meant by reading those attributes back
 * off the live DOM with `querySelectorAll`.
 *
 * That is a compiler that throws its own analysis away. Every consequence
 * followed from it: the render-program backend could not compile a list,
 * because by the time it ran the list was an anonymous `<template>` element
 * carrying strings; the runtime had to ship a second renderer to interpret
 * those attributes; and a `>` inside a header expression broke the rewrite
 * because a regex was the only thing left that could read it.
 *
 * The IR is where a construct's meaning is written down instead. A `<@for>`
 * becomes a {@link ForNode} with a list expression, a binding name, an optional
 * key and two child fragments. Nothing downstream has to guess, and nothing has
 * to parse markup a second time.
 *
 * ## The shape, and why fragments are explicit
 *
 * A {@link FragmentNode} is a compile boundary: each one becomes its own
 * skeleton and its own op list in the emitted program, and its own DOM range at
 * run time. Control flow owns fragments rather than containing raw children,
 * because "the body of this loop" is exactly the unit that gets cloned per item
 * and torn down per removal. Making that unit implicit is how a list renderer
 * ends up re-deriving its own boundaries.
 *
 * ## Extension
 *
 * Adding a construct means adding a kind here, a builder case in
 * {@link module:lib/compiler/ir/build}, and a lowering case in
 * {@link module:lib/compiler/ir/lower}. A construct with no lowering case is
 * refused with its own reason rather than silently mis-emitted, which is the
 * same compile-or-refuse rule the render program has always followed -- moved
 * one layer earlier, where the reason is still specific enough to be useful.
 * @module lib/compiler/ir/nodes
 */

/**
 * IR node kinds.
 * @enum {string}
 */
export const IRKind = {
  /** An ordered list of children forming one compile and DOM boundary. */
  FRAGMENT: 'fragment',
  /** A literal element. */
  ELEMENT: 'element',
  /** Literal character data. */
  TEXT: 'text',
  /** A `{{ }}` or `{{{ }}}` interpolation. */
  INTERPOLATION: 'interpolation',
  /** An HTML comment preserved in the output. */
  COMMENT: 'comment',
  /** Conditional rendering: `<@if>` / `<@elseif>` / `<@else>`. */
  IF: 'if',
  /** Iteration: `<@for>` / `<@empty>`. */
  FOR: 'for',
  /** A child component instantiation. */
  COMPONENT: 'component',
  /** A `<slot>` outlet. */
  SLOT: 'slot',
  /** Content rendered once a trigger fires: `<@defer>`. */
  DEFER: 'defer',
};

/**
 * Kinds of value binding an element can carry.
 * @enum {string}
 */
export const BindingKind = {
  /** `attr="{{ expr }}"` -- the whole value is one expression. */
  ATTR: 'attr',
  /** `attr="a {{ b }} c"` -- literal and expression parts. */
  ATTR_PARTS: 'attrParts',
  /** A boolean attribute driven by an expression's truthiness. */
  BOOL: 'bool',
  /** `data-ax-show` -- toggles `display`. */
  SHOW: 'show',
  /** `data-ax-class` -- string or object class binding. */
  CLASS: 'class',
  /** `data-ax-html` -- replaces inner HTML. */
  HTML: 'html',
  /** `data-ax-style` -- string or object inline style binding. */
  STYLE: 'style',
  /** A prop passed to a child component. */
  PROP: 'prop',
};

/**
 * Why a template, or part of one, could not be represented in the IR.
 *
 * These are the constructs the IR does not model yet. They are enumerated
 * rather than free text so a build can group them, and so a reader can tell
 * "not implemented" from "gave up".
 * @enum {string}
 */
export const RefusalReason = {
  SUSPENSE: 'a <@suspense> boundary',
  ERROR_BOUNDARY: 'an <@errorBoundary>',
  DEADLOCK: 'a <@deadlock> boundary',
  DEFER: 'a <@defer> block',
  TRANSITION: 'a transition',
  RESOURCE: 'a <resource> declaration in the template',
  DYNAMIC_COMPONENT: 'a dynamic component tag',
  DYNAMIC_ATTR: 'a dynamic attribute name',
  ROUTER_VIEW: 'a router view',
  VALIDATION: 'declarative form validation',
  REF: 'a template ref',
  VIRTUAL_LIST: 'a virtualised list',
  MALFORMED: 'a malformed template',
  UNKNOWN_DIRECTIVE: 'an unrecognised directive',
};

/**
 * A construct the IR builder declined to represent.
 *
 * Thrown rather than returned because a refusal aborts the whole template --
 * there is no partial IR. A half-built IR would describe a template that does
 * not exist.
 */
export class IRRefusal extends Error {
  /**
   * @param {string} reason - A {@link RefusalReason}.
   * @param {string} detail - What in the template caused it.
   */
  constructor(reason, detail) {
    super(`${reason}: ${detail}`);
    this.name = 'IRRefusal';
    this.reason = reason;
    this.detail = detail;
  }
}

/**
 * Creates a fragment.
 * @param {object[]} [children] - Child IR nodes.
 * @returns {object} The fragment node.
 */
export function fragment(children = []) {
  return { kind: IRKind.FRAGMENT, children };
}

/**
 * Creates a literal text node.
 * @param {string} value - The character data.
 * @returns {object} The text node.
 */
export function text(value) {
  return { kind: IRKind.TEXT, value };
}

/**
 * Creates an interpolation node.
 * @param {string} expr - The expression source.
 * @param {boolean} [raw] - True for `{{{ }}}`, which inserts markup.
 * @returns {object} The interpolation node.
 */
export function interpolation(expr, raw = false) {
  return { kind: IRKind.INTERPOLATION, expr, raw };
}

/**
 * Creates a comment node.
 * @param {string} value - The comment body.
 * @returns {object} The comment node.
 */
export function comment(value) {
  return { kind: IRKind.COMMENT, value };
}

/**
 * Creates an element node.
 * @param {string} tag - The tag name.
 * @param {object} [options] - Element parts.
 * @param {Object<string,string>} [options.attrs] - Static attributes.
 * @param {object[]} [options.bindings] - Value bindings.
 * @param {object[]} [options.events] - Event bindings.
 * @param {object[]} [options.children] - Child nodes.
 * @param {boolean} [options.selfClosing] - Whether the tag is void or self-closed.
 * @param {boolean} [options.isStatic] - Whether the subtree provably never changes.
 * @returns {object} The element node.
 */
export function element(tag, options = {}) {
  return {
    kind: IRKind.ELEMENT,
    tag,
    attrs: options.attrs || {},
    bindings: options.bindings || [],
    events: options.events || [],
    children: options.children || [],
    selfClosing: options.selfClosing === true,
    isStatic: options.isStatic === true,
  };
}

/**
 * Creates a conditional node.
 *
 * Branches are ordered and the first whose test is truthy renders. A branch
 * with a null test is the `<@else>` and may only appear last; the builder
 * enforces that, so nothing downstream has to re-check it.
 * @param {Array<{test: string|null, body: object}>} branches - Ordered branches.
 * @returns {object} The conditional node.
 */
export function conditional(branches) {
  return { kind: IRKind.IF, branches };
}

/**
 * Creates an iteration node.
 *
 * Exactly one of `item` and `destructure` is set: the first for `x in xs`, the
 * second for `[a, b] in pairs`, which destructures each element rather than
 * binding an index. The index is bound implicitly under the name `index` by
 * the runtime and is therefore not part of the node.
 * @param {object} parts - Loop parts.
 * @param {string} parts.list - The list expression source.
 * @param {string|null} parts.item - The name bound to each element.
 * @param {string[]|null} parts.destructure - Names each element is destructured into.
 * @param {string|null} parts.key - The key expression source, when declared.
 * @param {object} parts.body - The per-item fragment.
 * @param {object|null} parts.empty - The fragment rendered for an empty list.
 * @returns {object} The iteration node.
 */
export function iteration(parts) {
  return {
    kind: IRKind.FOR,
    list: parts.list,
    item: parts.item || null,
    destructure: parts.destructure || null,
    key: parts.key || null,
    body: parts.body,
    empty: parts.empty || null,
  };
}

/**
 * Creates a child-component node.
 * @param {string} name - The component's registered PascalCase name.
 * @param {object} [options] - Component parts.
 * @param {object[]} [options.props] - Static and bound props.
 * @param {object[]} [options.children] - Transcluded content.
 * @returns {object} The component node.
 */
export function component(name, options = {}) {
  return {
    kind: IRKind.COMPONENT,
    name,
    props: options.props || [],
    children: options.children || [],
  };
}

/**
 * Creates a slot outlet.
 * @param {string} name - The slot name; `default` when unnamed.
 * @param {object} [fallbackFragment] - Content rendered when nothing is transcluded.
 * @returns {object} The slot node.
 */
export function slot(name, fallbackFragment = null) {
  return { kind: IRKind.SLOT, name, fallback: fallbackFragment };
}

/**
 * Creates a deferred block.
 * @param {string} when - The trigger: `idle`, `visible`, `hover`, `click`,
 *   `interaction`, a duration such as `2s`, or an expression.
 * @param {object} body - The fragment rendered once the trigger fires.
 * @param {object|null} [placeholder] - The fragment rendered until it does.
 * @returns {object} The defer node.
 */
export function deferred(when, body, placeholder = null) {
  return { kind: IRKind.DEFER, when, body, placeholder };
}

/**
 * Whether a node introduces its own compile and DOM boundary.
 *
 * Control flow does; an element does not. Used by the lowering pass to decide
 * where one skeleton ends and the next begins.
 * @param {object} node - An IR node.
 * @returns {boolean} True when the node owns fragments.
 */
export function isBlockNode(node) {
  return node && (node.kind === IRKind.IF || node.kind === IRKind.FOR || node.kind === IRKind.SLOT || node.kind === IRKind.DEFER);
}

/**
 * Walks every node in an IR tree, depth first, including fragment bodies.
 * @param {object} node - The root node.
 * @param {function(object, (object|null)): void} visit - Called with each node and its parent.
 * @param {object|null} [parent] - The parent, for recursive calls.
 */
export function walkIR(node, visit, parent = null) {
  if (!node || typeof node !== 'object') return;
  visit(node, parent);

  if (Array.isArray(node.children)) {
    for (const child of node.children) walkIR(child, visit, node);
  }
  if (node.kind === IRKind.IF) {
    for (const branch of node.branches) walkIR(branch.body, visit, node);
  }
  if (node.kind === IRKind.FOR) {
    walkIR(node.body, visit, node);
    if (node.empty) walkIR(node.empty, visit, node);
  }
  if (node.kind === IRKind.SLOT && node.fallback) {
    walkIR(node.fallback, visit, node);
  }
  if (node.kind === IRKind.DEFER) {
    walkIR(node.body, visit, node);
    if (node.placeholder) walkIR(node.placeholder, visit, node);
  }
}