Source: lib/compiler/render/program.js

/**
 * @file program.js
 * @description The shape of a compiled render program, and the vocabulary the
 * compiler and the runtime agree on.
 *
 * ## Why a program at all
 *
 * The previous rendering contract between the compiler and the runtime was a
 * single string: the compiler handed over a template with `{{ }}` still in it,
 * and the runtime worked out what that meant on every update. Working it out
 * meant interpolating the whole template into HTML, handing that HTML to
 * `DOMParser`, and diffing the resulting tree against the live DOM — so the
 * cost of changing one value was proportional to the size of the template, and
 * the runtime had to carry a template engine to pay it.
 *
 * None of that work depends on runtime values. Which text nodes are dynamic,
 * which attributes are bound, which elements carry an event handler, and which
 * subtrees can never change are all properties of the template, and the
 * compiler already has the template. A render program is the compiler writing
 * those answers down once, so the runtime can stop rediscovering them.
 *
 * ## The two halves
 *
 * A program is a **skeleton**, a list of **ops**, and a list of **blocks**.
 *
 * The skeleton is the template with every dynamic part removed: interpolations
 * become comment markers, bound attributes are dropped. It is valid HTML that
 * is parsed exactly once per component *class* and then cloned per instance,
 * so a 2000-node template is parsed once for the life of the page rather than
 * once per update.
 *
 * An op says what to do and where. `{ k: 'text', t: 3, x: 'count' }` reads
 * "evaluate `count`, write it to text marker 3". Each op becomes one reactive
 * effect at mount, so a write to `count` wakes that op and nothing else.
 *
 * ## Addressing, and why markers rather than paths
 *
 * The compiler cannot number nodes by walking its own AST and assume the
 * browser will produce the same tree: HTML parsing inserts implied elements
 * (`<tbody>`), relocates misplaced content, and closes tags the author left
 * open. A path computed from the compiler's tree can therefore address a
 * different node than the one the compiler meant.
 *
 * So the compiler emits *markers* — `data-axb="n"` on a dynamic element,
 * `<!--axt:n-->` where dynamic text goes — and the runtime resolves them
 * against the tree the browser actually built. That resolution happens once per
 * component class ({@link module:lib/core/renderer/program/CompiledTemplate}),
 * is cached as index paths, and every later instance resolves by path with no
 * search.
 *
 * ## What the compiler refuses to compile
 *
 * A program is emitted only when every construct in the template is one the
 * program runtime implements. Anything else and the component keeps the string
 * renderer, with the reason recorded. Partial compilation is not offered: a
 * template that is half compiled and half diffed has two sources of truth for
 * the same DOM, and they will disagree.
 *
 * That fallback is visible rather than silent — `avenx build` reports which
 * components did not compile and why, which is the same house rule Atlas
 * follows when its analysis is incomplete.
 *
 * The refusal reasons themselves live with the layer that decides them, in
 * {@link module:lib/compiler/ir/nodes}. They used to live here, next to a
 * second template compiler that read already-rewritten markup; that compiler
 * has been replaced by the IR and its lowering pass, so this module is now the
 * format and the vocabulary and nothing else.
 * @module lib/compiler/render/program
 */

/**
 * The program format version.
 *
 * A bundle carries programs produced by the compiler that built it, and the
 * runtime is concatenated into that same bundle, so the two can never be
 * mismatched in a deployed application. The version exists for the case that
 * can happen: a stored or transported program (a trace fixture, a cached
 * build) read back by a newer runtime. A runtime that does not recognise the
 * version falls back to the string renderer rather than guessing.
 * @type {number}
 */
export const PROGRAM_VERSION = 2;

/**
 * Op kinds a program may contain.
 *
 * Kept as short string constants rather than numbers: a program is JSON in a
 * bundle a developer may well end up reading, and `'text'` costs three bytes
 * more than `0` while costing nothing to understand.
 * @enum {string}
 */
export const OpKind = {
  /** Write an expression's value into a text marker. */
  TEXT: 'text',
  /** Write an expression's value into a text marker without escaping. */
  RAW: 'raw',
  /** Set an attribute to an expression's value. */
  ATTR: 'attr',
  /** Set an attribute from a mix of literal parts and expressions. */
  ATTR_PARTS: 'attrp',
  /** Set or remove a boolean attribute from an expression's truthiness. */
  BOOL: 'bool',
  /** Bind a DOM event handler declared in the template. */
  EVENT: 'event',
  /** Toggle `display` from an expression's truthiness (`data-ax-show`). */
  SHOW: 'show',
  /** Apply a string or object class binding (`data-ax-class`). */
  CLASS: 'class',
  /** Replace an element's inner HTML (`data-ax-html`). */
  HTML: 'html',
  /** Evaluate a prop for a child component mounted at this element. */
  PROP: 'prop',
  /** Set or update an inline style from a string or object (`data-ax-style`). */
  STYLE: 'style',
  /** Render the first arm whose test is truthy, at a text anchor. */
  IF: 'if',
  /** Render one copy of a block per list element, at a text anchor. */
  FOR: 'for',
  /** Render transcluded content, or a fallback block, at a text anchor. */
  SLOT: 'slot',
  /** Render a block once its trigger fires, at a text anchor. */
  DEFER: 'defer',
};

/**
 * Op kinds that own one or more blocks and therefore a DOM range.
 *
 * Everything else writes to a single node the skeleton already contains. A
 * range op inserts and removes nodes, so it needs an anchor and a teardown
 * path, and the runtime dispatches on this rather than re-listing the kinds.
 * @type {Set<string>}
 */
export const RANGE_OPS = new Set([OpKind.IF, OpKind.FOR, OpKind.SLOT, OpKind.DEFER]);

/**
 * Whether a value looks like a render program this runtime can execute.
 * @param {any} value - The candidate.
 * @returns {boolean} True when it is a program of a known version.
 */
export function isProgram(value) {
  return !!(
    value &&
    typeof value === 'object' &&
    value.v === PROGRAM_VERSION &&
    typeof value.html === 'string' &&
    Array.isArray(value.ops)
  );
}

/**
 * Returns a program's blocks, tolerating a program that declares none.
 *
 * A template with no control flow lowers to a root block and nothing else, and
 * omitting the empty array keeps that program byte-identical to what the
 * previous format produced for the same template.
 * @param {object} program - A render program.
 * @returns {object[]} The blocks.
 */
export function programBlocks(program) {
  return Array.isArray(program && program.blocks) ? program.blocks : [];
}