vize_atelier_core 0.220.0

Atelier Core - The core workshop for Vize Vue template parsing, transform lanes, and code generation
Documentation
//! Template transform lane for Vue template AST.
//!
//! This module provides the transform context, traversal, and base transform traits.

#[path = "../transform/context.rs"]
mod context;
#[path = "../transform/element.rs"]
pub mod element;
#[path = "../transform/structural.rs"]
pub mod structural;
#[path = "../transform/traverse.rs"]
pub mod traverse;

use vize_carton::{Box, Bump, SmallVec, String, Vec, profile};
use vize_croquis::{Croquis, ScopeChain};

use crate::errors::CompilerError;
use crate::options::TransformOptions;
use crate::runtime_helpers::RuntimeHelpers;
use crate::{
    CacheExpression, DirectiveNode, ElementNode, ForNode, IfBranchNode, IfNode, JsChildNode,
    PropNode, RootNode, RuntimeHelper, TemplateChildNode,
};

use traverse::traverse_children;

/// Transform function for nodes - returns optional exit function(s)
pub type NodeTransform<'a> =
    fn(&mut TransformContext<'a>, &mut TemplateChildNode<'a>) -> Option<ExitFns<'a>>;

/// Exit function called after children are processed
pub type ExitFn<'a> = std::boxed::Box<dyn FnOnce(&mut TransformContext<'a>) + 'a>;
pub type ExitFns<'a> = SmallVec<[ExitFn<'a>; 2]>;

/// Transform function for directives
pub type DirectiveTransform<'a> = fn(
    &mut TransformContext<'a>,
    &mut ElementNode<'a>,
    &DirectiveNode<'a>,
) -> Option<DirectiveTransformResult<'a>>;

/// Result of a directive transform
pub struct DirectiveTransformResult<'a> {
    /// Props to add to the element
    pub props: Vec<'a, PropNode<'a>>,
    /// Whether to remove the directive
    pub remove_directive: bool,
    /// SSR tag type hint
    pub ssr_tag_type: Option<u8>,
}

/// Structural directive transform (v-if, v-for)
pub type StructuralDirectiveTransform<'a> =
    fn(&mut TransformContext<'a>, &mut ElementNode<'a>, &DirectiveNode<'a>) -> Option<ExitFn<'a>>;

/// Transform context for AST traversal
pub struct TransformContext<'a> {
    /// Arena allocator
    pub allocator: &'a Bump,
    /// Transform options
    pub options: TransformOptions,
    /// Source code
    pub source: String,
    /// Root node reference
    pub root: Option<*mut RootNode<'a>>,
    /// Parent node stack
    pub parent: Option<ParentNode<'a>>,
    /// Grandparent node
    pub grandparent: Option<ParentNode<'a>>,
    /// Current node being transformed
    pub current_node: Option<*mut TemplateChildNode<'a>>,
    /// Child index in parent
    pub child_index: usize,
    /// Helpers used
    pub helpers: RuntimeHelpers,
    /// Components used (Vec to maintain template order for code generation)
    pub components: std::vec::Vec<String>,
    /// Directives used (Vec to maintain template order for code generation)
    pub directives: std::vec::Vec<String>,
    /// Vue 2 pipe filters referenced by the template, first-seen order.
    /// Flushed to [`RootNode::filters`](crate::RootNode) and emitted as
    /// `_resolveFilter` asset declarations. Legacy-only and dialect-gated.
    #[cfg(feature = "legacy")]
    pub(crate) filters: std::vec::Vec<String>,
    /// Hoisted expressions
    pub hoists: Vec<'a, Option<JsChildNode<'a>>>,
    /// Cached expressions
    pub cached: Vec<'a, Option<Box<'a, CacheExpression<'a>>>>,
    /// Temp variable count
    pub temps: u32,
    /// Scope chain for tracking variable visibility
    pub scope_chain: ScopeChain,
    /// Scoped slots
    pub scoped_slots: u32,
    /// Whether in v-once
    pub in_v_once: bool,
    /// Whether in SSR
    pub in_ssr: bool,
    /// Errors collected
    pub errors: std::vec::Vec<CompilerError>,
    /// Enables compatibility for template syntax edge-case behavior.
    pub(crate) template_syntax_quirks: bool,
    /// Node was removed flag
    pub(crate) node_removed: bool,
    /// Semantic analysis summary (optional, for enhanced transforms)
    pub(crate) analysis: Option<&'a Croquis>,
    /// Scope ID to bake into static VNodes hoisted outside render scope.
    pub(crate) hoisted_scope_id: Option<String>,
}

/// Enum for parent node types
#[derive(Clone, Copy)]
pub enum ParentNode<'a> {
    Root(*mut RootNode<'a>),
    Element(*mut ElementNode<'a>),
    If(*mut IfNode<'a>),
    IfBranch(*mut IfBranchNode<'a>),
    For(*mut ForNode<'a>),
}

impl<'a> ParentNode<'a> {
    /// Get mutable access to children through raw pointer.
    ///
    /// # Safety
    /// `ParentNode` stores pointers into the bump-allocated template tree so the
    /// traversal can mutate parents without cloning children on every visit. The
    /// active traversal loop owns the only mutable child slice for the current
    /// parent, and nested calls only use descendants created from that slice.
    /// Keeping the invariant here avoids an allocation-heavy zipper structure in
    /// the hottest template transform path.
    #[allow(clippy::mut_from_ref)]
    pub fn children_mut(&self) -> &mut Vec<'a, TemplateChildNode<'a>> {
        // SAFETY: every pointer is produced from a live `RootNode`, `ElementNode`,
        // `IfBranchNode`, or `ForNode` borrowed by the transform driver. The
        // transform is single-threaded and never keeps two active mutable child
        // slices for the same parent; sibling traversal advances only after the
        // previous borrow has been consumed. `ParentNode::If` intentionally has no
        // children slice, so that variant is rejected before any pointer deref.
        unsafe {
            match self {
                ParentNode::Root(r) => &mut (*(*r)).children,
                ParentNode::Element(e) => &mut (*(*e)).children,
                ParentNode::If(_) => {
                    // Panic path by design: callers must traverse concrete
                    // branches (`ParentNode::IfBranch`) because an `IfNode`
                    // itself is only a branch container and has no direct child
                    // vector to return.
                    panic!("IfNode doesn't have direct children")
                }
                ParentNode::IfBranch(b) => &mut (*(*b)).children,
                ParentNode::For(f) => &mut (*(*f)).children,
            }
        }
    }
}

/// Transform the root AST node.
///
/// Returns the diagnostics collected during the transform (e.g. invalid
/// directive usage or unparseable expressions) so callers can surface them
/// alongside parse errors instead of silently dropping them.
pub fn transform<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
    transform_inner(allocator, root, options, analysis, false, None)
}

/// Transform the root AST node with template syntax quirk compatibility enabled.
pub fn transform_with_template_syntax_quirks<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
    transform_inner(allocator, root, options, analysis, true, None)
}

/// Transform the root AST node with Vue parser quirk compatibility enabled.
#[deprecated(note = "use transform_with_template_syntax_quirks instead")]
pub fn transform_with_vue_parser_quirks<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
) -> std::vec::Vec<CompilerError> {
    transform_with_template_syntax_quirks(allocator, root, options, analysis)
}

/// Transform the root AST node with an explicit scope ID for hoisted VNodes.
#[doc(hidden)]
pub fn transform_with_hoisted_scope_id<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
    hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
    transform_inner(allocator, root, options, analysis, false, hoisted_scope_id)
}

/// Transform the root AST node with template syntax quirks and an explicit hoisted scope ID.
#[doc(hidden)]
pub fn transform_with_template_syntax_quirks_and_hoisted_scope_id<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
    hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
    transform_inner(allocator, root, options, analysis, true, hoisted_scope_id)
}

/// Transform the root AST node with Vue parser quirks and an explicit hoisted scope ID.
#[doc(hidden)]
#[deprecated(note = "use transform_with_template_syntax_quirks_and_hoisted_scope_id instead")]
pub fn transform_with_vue_parser_quirks_and_hoisted_scope_id<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
    hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
    transform_with_template_syntax_quirks_and_hoisted_scope_id(
        allocator,
        root,
        options,
        analysis,
        hoisted_scope_id,
    )
}

fn transform_inner<'a>(
    allocator: &'a Bump,
    root: &mut RootNode<'a>,
    options: TransformOptions,
    analysis: Option<&'a Croquis>,
    template_syntax_quirks: bool,
    hoisted_scope_id: Option<String>,
) -> std::vec::Vec<CompilerError> {
    let source = root.source.clone();
    let mut ctx = if let Some(analysis) = analysis {
        TransformContext::with_analysis_and_template_syntax_quirks(
            allocator,
            source,
            options,
            analysis,
            template_syntax_quirks,
        )
    } else {
        TransformContext::new_with_template_syntax_quirks(
            allocator,
            source,
            options,
            template_syntax_quirks,
        )
    };
    ctx.hoisted_scope_id = hoisted_scope_id;
    ctx.root = Some(root as *mut _);

    // Legacy (Vue 2 / 2.7) template-sugar pre-transform. Resolved once per file
    // from the dialect; a no-op for the default Vue 3 dialect (the resolved
    // capability set is the all-off `VUE3` set, so this returns before touching
    // the tree). Compiled only under the `legacy` cargo feature — the default
    // Vue 3 build never sees it, keeping the hot path byte-identical.
    #[cfg(feature = "legacy")]
    {
        use vize_armature::legacy::LegacyDialectCapabilities;
        let caps = LegacyDialectCapabilities::for_dialect(ctx.options.dialect);
        crate::steps::legacy::desugar_legacy_template(allocator, root, caps);
    }

    // Transform the root children
    profile!(
        "atelier.transform.traverse_children",
        traverse_children(&mut ctx, ParentNode::Root(root as *mut _))
    );

    // Apply static hoisting after traversal (before codegen)
    use crate::steps::hoist_static::hoist_static;
    profile!(
        "atelier.transform.hoist_static",
        hoist_static(&mut ctx, &mut root.children)
    );

    // Register helpers implied by the root shape.
    profile!(
        "atelier.transform.register_root_helpers",
        register_root_helpers(&mut ctx, root)
    );

    // Update root with context results
    for helper in ctx.helpers.iter() {
        root.helpers.push(helper);
    }
    for component in ctx.components.into_iter() {
        root.components.push(component);
    }
    for directive in ctx.directives.into_iter() {
        root.directives.push(directive);
    }
    // Transfer Vue 2 pipe filters (legacy-only; empty for Vue 3).
    #[cfg(feature = "legacy")]
    for filter in ctx.filters.into_iter() {
        root.filters.push(filter);
    }
    // Transfer hoisted nodes to root
    for hoist in ctx.hoists.into_iter() {
        root.hoists.push(hoist);
    }
    root.temps = ctx.temps;
    root.transformed = true;

    ctx.errors
}

fn register_root_helpers<'a>(ctx: &mut TransformContext<'a>, root: &mut RootNode<'a>) {
    if root.children.is_empty() {
        return;
    }

    if root.children.len() > 1 {
        // Multiple root children need to be wrapped in a fragment
        ctx.helper(RuntimeHelper::OpenBlock);
        ctx.helper(RuntimeHelper::CreateElementBlock);
        ctx.helper(RuntimeHelper::Fragment);
    }
}

#[cfg(test)]
mod tests;