vue_compiler_core/converter/
mod.rs

1/*!
2IR Converter module takes AST and produces intermediate representation.
3All core template syntax conversion happens here. IR is later used for
4optimizing transformation and code generation. As we decouple codegen
5node from AST, Vue's transformation passes are broken down to two parts.
6Convert module roughly corresponds to following transform in vue-next.
7
8# IR Convert
9* transformElement
10* transformSlotOutlet
11* transformTextCall
12* vFor
13* vIf
14* vSlot
15* vOnce (noop)
16* vMemo (noop)
17
18# Transform directive
19* noopDirectiveTransform
20* vModel
21* vBind
22* vOn (noop)
23*/
24
25use crate::{
26    flags::{HelperCollector, PatchFlag, RuntimeHelper, SlotFlag, StaticLevel},
27    parser::{SourceNode, TextNode},
28    util::{find_dir, get_core_component, VStr},
29    Name,
30};
31
32pub use crate::error::{CompilationError, ErrorHandler};
33pub use crate::parser::{AstNode, AstRoot, Directive, Element};
34use rustc_hash::{FxHashMap, FxHashSet};
35use smallvec::{smallvec, SmallVec};
36use std::hash::Hash;
37use std::{marker::PhantomData, ops::Deref, rc::Rc};
38
39#[cfg(feature = "serde")]
40use serde::Serialize;
41
42mod build_props;
43mod cache_dir;
44mod convert_element;
45mod convert_slot_outlet;
46mod v_bind;
47mod v_for;
48mod v_if;
49mod v_slot;
50
51use cache_dir::{pre_convert_memo, pre_convert_once};
52use v_for::pre_convert_for;
53use v_if::{pre_group_v_if, PreGroup};
54
55/// Converts template ast node to intermediate representation.
56/// It defines the most generic Converter interface.
57/// The IR format can be platform specific.
58/// e.g Platfroms other than DOM/SSR can have different IR
59pub trait Converter<'a>: Sized {
60    type IR;
61    fn convert_ir(&self, ast: AstRoot<'a>) -> Self::IR;
62}
63
64#[cfg(feature = "serde")]
65pub trait ConvertInfo {
66    type TopType: Default + Serialize;
67    // TextType should be a slice of JsExpressions
68    type TextType: AsMut<[Self::JsExpression]> + Serialize;
69    type IfBranchType: Serialize;
70    type CommentType: Serialize;
71    type JsExpression: Default + Serialize;
72    type StrType: Serialize + Eq + Hash;
73}
74#[cfg(not(feature = "serde"))]
75pub trait ConvertInfo {
76    type TopType: Default;
77    // TextType should be a slice of JsExpressions
78    type TextType: AsMut<[Self::JsExpression]>;
79    type IfBranchType;
80    type CommentType;
81    type JsExpression: Default;
82    type StrType: Eq + Hash;
83}
84
85#[cfg_attr(feature = "serde", derive(Serialize))]
86pub enum IRNode<T: ConvertInfo> {
87    /// interpolation or text node
88    TextCall(TextIR<T>),
89    /// v-if, else-if, else
90    If(IfNodeIR<T>),
91    /// v-for
92    For(ForNodeIR<T>),
93    /// component/template/plain element
94    VNodeCall(VNodeIR<T>),
95    /// <slot> slot outlet
96    RenderSlotCall(RenderSlotIR<T>),
97    /// v-slot used on component or template
98    VSlotUse(VSlotIR<T>),
99    // internal type for v-slot to reuse v-if/for
100    AlterableSlot(Slot<T>),
101    /// comment
102    CommentCall(T::CommentType),
103}
104
105#[cfg_attr(feature = "serde", derive(Serialize))]
106pub struct TextIR<T: ConvertInfo> {
107    pub fast_path: bool,  // without createTextCall
108    pub need_patch: bool, // PatchFlag::TEXT
109    pub texts: T::TextType,
110}
111#[cfg_attr(feature = "serde", derive(Serialize))]
112pub struct IfNodeIR<T: ConvertInfo> {
113    pub branches: Vec<IfBranch<T>>,
114}
115#[cfg_attr(feature = "serde", derive(Serialize))]
116pub struct IfBranch<T: ConvertInfo> {
117    pub condition: Option<T::JsExpression>,
118    pub child: Box<IRNode<T>>,
119    pub info: T::IfBranchType,
120}
121#[cfg_attr(feature = "serde", derive(Serialize))]
122pub struct ForNodeIR<T: ConvertInfo> {
123    pub source: T::JsExpression,
124    pub parse_result: ForParseResult<T>,
125    pub child: Box<IRNode<T>>,
126    pub is_stable: bool,
127    pub fragment_flag: PatchFlag,
128    pub key: Option<T::JsExpression>,
129}
130// TODO: optimize as vec to save memory
131// (value, key, index) in source
132#[cfg_attr(feature = "serde", derive(Serialize))]
133pub struct ForParseResult<T: ConvertInfo> {
134    pub value: T::JsExpression,
135    pub key: Option<T::JsExpression>,
136    pub index: Option<T::JsExpression>,
137}
138#[cfg_attr(feature = "serde", derive(Serialize))]
139pub struct RenderSlotIR<T: ConvertInfo> {
140    pub slot_obj: T::JsExpression,
141    pub slot_name: T::JsExpression,
142    pub slot_props: Option<T::JsExpression>,
143    pub fallbacks: Vec<IRNode<T>>,
144    pub no_slotted: bool,
145}
146#[cfg_attr(feature = "serde", derive(Serialize))]
147pub struct RuntimeDir<T: ConvertInfo> {
148    pub name: T::JsExpression,
149    pub expr: Option<T::JsExpression>,
150    pub arg: Option<T::JsExpression>,
151    pub mods: Option<T::JsExpression>,
152}
153#[derive(Default)]
154#[cfg_attr(feature = "serde", derive(Serialize))]
155pub struct VNodeIR<T: ConvertInfo> {
156    pub tag: T::JsExpression,
157    pub props: Option<T::JsExpression>,
158    pub children: Vec<IRNode<T>>,
159    pub patch_flag: PatchFlag,
160    pub dynamic_props: FxHashSet<T::StrType>,
161    pub directives: Vec<RuntimeDir<T>>,
162    pub is_block: bool,
163    pub disable_tracking: bool,
164    pub is_component: bool,
165}
166#[cfg_attr(feature = "serde", derive(Serialize))]
167pub struct Slot<T: ConvertInfo> {
168    pub name: T::JsExpression,
169    pub param: Option<T::JsExpression>,
170    pub body: Vec<IRNode<T>>,
171}
172// note the diffrence between stable and static, dynamic and alterable.
173// static = static template name, capturing no identifier
174// stable = no if nor for
175#[cfg_attr(feature = "serde", derive(Serialize))]
176pub struct VSlotIR<T: ConvertInfo> {
177    /// stable v-slots declared statically in the template
178    pub stable_slots: Vec<Slot<T>>,
179    /// v-slots templates dynamically declared with v-if/v-for
180    pub alterable_slots: Vec<IRNode<T>>,
181    pub slot_flag: SlotFlag,
182}
183
184pub type Prop<'a> = (JsExpr<'a>, JsExpr<'a>);
185#[derive(Clone)]
186#[cfg_attr(feature = "serde", derive(Serialize))]
187pub enum JsExpr<'a> {
188    /// Source. output to generated code as is.
189    Src(&'a str),
190    /// representing a number, either id or key
191    Num(usize),
192    /// String Literal. output after quoted, used by attr/static arg.
193    // TODO: StaticLevel + Simple can mock StrLit?
194    StrLit(VStr<'a>),
195    /// non-string js expression, will be processed like prefixing
196    Simple(VStr<'a>, StaticLevel),
197    /// variable in parameter
198    Param(Name<'a>),
199    /// alternative to join string as JsExpr
200    Compound(Vec<JsExpr<'a>>),
201    Props(Vec<Prop<'a>>),
202    /// for calling runtime helper, e.g. resolveComponent()
203    Call(RuntimeHelper, Vec<JsExpr<'a>>),
204    /// for builtin component called as symbol
205    Symbol(RuntimeHelper),
206    /// array of JsExpr
207    Array(Vec<JsExpr<'a>>),
208}
209
210impl<'a> Default for JsExpr<'a> {
211    fn default() -> Self {
212        Self::Src("")
213    }
214}
215
216impl<'a> JsExpr<'a> {
217    /// a convenient util for creating JsExpr::Simple
218    pub fn simple<V: Into<VStr<'a>>>(v: V) -> Self {
219        JsExpr::Simple(v.into(), StaticLevel::NotStatic)
220    }
221    pub fn str_lit<V: Into<VStr<'a>>>(v: V) -> Self {
222        JsExpr::StrLit(v.into())
223    }
224    pub fn static_level(&self) -> StaticLevel {
225        use JsExpr::*;
226        use StaticLevel as S;
227        match self {
228            Src(_) | StrLit(_) => S::CanStringify,
229            Simple(_, level) => *level,
230            Compound(v) | Array(v) | Call(_, v) => v
231                .iter()
232                .map(Self::static_level)
233                .min()
234                .unwrap_or(S::CanHoist),
235            _ => S::NotStatic,
236        }
237    }
238}
239
240#[derive(PartialEq, Eq)]
241pub enum BindingTypes {
242    /// returned from data()
243    Data,
244    /// declared as a prop
245    Props,
246    /// a let binding (may or may not be a ref)
247    SetupLet,
248    ///a const binding that can never be a ref.
249    ///these bindings don't need `unref()` calls when processed in inlined
250    ///template expressions.
251    SetupConst,
252    /// a const binding that may be a ref.
253    SetupMaybeRef,
254    /// bindings that are guaranteed to be refs
255    SetupRef,
256    /// declared by other options, e.g. computed, inject
257    Options,
258}
259
260impl BindingTypes {
261    pub fn get_js_prop<'a>(&self, name: VStr<'a>, lvl: StaticLevel) -> JsExpr<'a> {
262        use BindingTypes::*;
263        let obj_dot = JsExpr::Src(match self {
264            Data => "$data.",
265            Props => "$props.",
266            Options => "$options.",
267            _ => "$setup.",
268        });
269        let prop = JsExpr::Simple(name, lvl);
270        JsExpr::Compound(vec![obj_dot, prop])
271    }
272}
273
274#[cfg_attr(feature = "serde", derive(Serialize))]
275pub struct IRRoot<T: ConvertInfo> {
276    pub body: Vec<IRNode<T>>,
277    /// entities to define/import in top level scope
278    pub top_scope: T::TopType,
279}
280
281/// Default implementation  sketch can be used in DOM/SSR.
282/// Other platform might invent and use their own IR.
283pub trait CoreConverter<'a, T: ConvertInfo> {
284    fn convert_core_ir(&self, ast: AstRoot<'a>) -> IRRoot<T> {
285        let body = self.convert_children(ast.children);
286        IRRoot {
287            body,
288            top_scope: T::TopType::default(),
289        }
290    }
291    fn convert_children(&self, children: Vec<AstNode<'a>>) -> Vec<IRNode<T>> {
292        let mut key = 0;
293        // pre group adjacent v-if here to avoid access siblings
294        pre_group_v_if(children)
295            .map(|pre| match pre {
296                PreGroup::VIfGroup(to_convert) => {
297                    let len = to_convert.len();
298                    let converted = self.convert_if(to_convert, key);
299                    key += len;
300                    converted
301                }
302                PreGroup::StandAlone(n) => self.dispatch_ast(n),
303            })
304            .collect()
305    }
306
307    fn dispatch_ast(&self, n: AstNode<'a>) -> IRNode<T> {
308        match n {
309            AstNode::Text(t) => self.convert_text(t),
310            AstNode::Interpolation(i) => self.convert_interpolation(i),
311            AstNode::Comment(c) => self.convert_comment(c),
312            // all element like node needs pre-convert structural dirs
313            AstNode::Element(e) => self.pre_convert_element(e),
314        }
315    }
316    fn pre_convert_element(&self, mut e: Element<'a>) -> IRNode<T> {
317        // order is defined as @vue/compiler-core/src/compile.ts
318        let once = pre_convert_once(&mut e);
319        let memo = pre_convert_memo(&mut e);
320        let vfor = pre_convert_for(&mut e);
321        let mut n = self.dispatch_element(e);
322        if let Some(d) = vfor {
323            n = self.convert_for(d, n);
324        }
325        if let Some(d) = memo {
326            n = self.convert_memo(d, n);
327        }
328        if let Some(d) = once {
329            n = self.convert_once(d, n);
330        }
331        // reverse order
332        n
333    }
334    fn dispatch_element(&self, e: Element<'a>) -> IRNode<T> {
335        use super::parser::ElementType::{SlotOutlet, Template};
336        match e.tag_type {
337            Template => self.convert_template(e),
338            SlotOutlet => self.convert_slot_outlet(e),
339            _ => self.convert_element(e),
340        }
341    }
342
343    // emit error
344    fn emit_error(&self, error: CompilationError);
345    // platform specific options
346    fn get_builtin_component(&self, tag: &str) -> Option<RuntimeHelper>;
347
348    // core template syntax conversion
349    fn convert_directive(
350        &self,
351        dir: &mut Directive<'a>,
352        e: &mut Element<'a>,
353    ) -> DirectiveConvertResult<T::JsExpression>;
354    fn convert_if(&self, elems: Vec<Element<'a>>, key: usize) -> IRNode<T>;
355    fn convert_for(&self, d: Directive<'a>, n: IRNode<T>) -> IRNode<T>;
356    fn convert_memo(&self, d: Directive<'a>, n: IRNode<T>) -> IRNode<T>;
357    fn convert_once(&self, d: Directive<'a>, n: IRNode<T>) -> IRNode<T>;
358    fn convert_slot_outlet(&self, e: Element<'a>) -> IRNode<T>;
359    fn convert_element(&self, e: Element<'a>) -> IRNode<T>;
360    fn convert_text(&self, t: TextNode<'a>) -> IRNode<T>;
361    fn convert_interpolation(&self, i: SourceNode<'a>) -> IRNode<T>;
362    fn convert_template(&self, e: Element<'a>) -> IRNode<T>;
363    fn convert_comment(&self, c: SourceNode<'a>) -> IRNode<T>;
364}
365
366/// Directive's prop argument passed to VNodeCall after conversion.
367/// Use Dropped if the directive is dropped implicitly without codegen.
368/// NB: this is not 100% translation from TS. `value` accepts both Props and Object.
369// This design decouples v-bind/on from transform_element.
370pub enum DirectiveConvertResult<Expr> {
371    Converted {
372        value: Expr,
373        /// Ok if it needs builtin runtime helper
374        /// Err(bool) indicates if it is user defined runtime dir
375        runtime: Result<RuntimeHelper, bool>,
376    },
377    Preserve,
378    Dropped,
379}
380
381pub fn no_op_directive_convert<'a>(
382    _: &mut Directive<'a>,
383    _: &Element<'a>,
384    _: &dyn ErrorHandler,
385) -> DirectiveConvertResult<JsExpr<'a>> {
386    DirectiveConvertResult::Dropped
387}
388
389// Base Converter for DOM and SSR Fallback
390#[derive(Default)]
391#[cfg_attr(feature = "serde", derive(Serialize))]
392pub struct BaseConvertInfo<'a>(PhantomData<&'a ()>);
393
394#[derive(Default)]
395#[cfg_attr(feature = "serde", derive(Serialize))]
396pub struct TopScope<'a> {
397    /// runtime helpers used in template
398    pub helpers: HelperCollector,
399    /// components that requires resolveComponent call
400    pub components: FxHashSet<VStr<'a>>,
401    /// directives that requires resolveDirecitve call
402    pub directives: FxHashSet<VStr<'a>>,
403    /// hoisted vnode/text/js object
404    pub hoists: Vec<BaseIR<'a>>,
405    /// counters for cached instance, increment per v-once/memo
406    pub cached: usize,
407    /// counters for temporary variables created in template
408    pub temps: usize,
409}
410
411impl<'a> ConvertInfo for BaseConvertInfo<'a> {
412    type TopType = TopScope<'a>;
413    type TextType = SmallVec<[JsExpr<'a>; 1]>;
414    type IfBranchType = usize;
415    type CommentType = &'a str;
416    type JsExpression = JsExpr<'a>;
417    type StrType = VStr<'a>;
418}
419
420pub type CoreDirConvRet<'a> = DirectiveConvertResult<JsExpr<'a>>;
421/// Returns the conversion of a directive. Value could be props or object.
422// NB: we pass &dyn ErrorHandler to monomorphize the dir converter to pay
423// the minimal cost of dynamism only when error occurs. otherwise we will
424// incur the overhead of dyn DirectiveConvert in the ConvertOption.
425pub type DirConvertFn =
426    for<'a> fn(&mut Directive<'a>, &Element<'a>, &dyn ErrorHandler) -> CoreDirConvRet<'a>;
427pub type DirectiveConverter = (&'static str, DirConvertFn);
428
429/// stores binding variables exposed by data/prop/setup script.
430/// also stores if the binding is from setup script.
431#[derive(Default)]
432pub struct BindingMetadata<'a>(FxHashMap<&'a str, BindingTypes>, bool);
433impl<'a> BindingMetadata<'a> {
434    pub fn is_setup(&self) -> bool {
435        self.1
436    }
437}
438impl<'a> Deref for BindingMetadata<'a> {
439    type Target = FxHashMap<&'a str, BindingTypes>;
440    fn deref(&self) -> &Self::Target {
441        &self.0
442    }
443}
444
445#[derive(Clone)]
446pub struct ConvertOption {
447    /// For platform developers. Registers platform specific components written in JS.
448    /// e.g. transition, transition-group. Components that require code in Vue runtime.
449    pub get_builtin_component: fn(&str) -> Option<RuntimeHelper>,
450    pub is_dev: bool,
451    pub directive_converters: FxHashMap<&'static str, DirConvertFn>,
452}
453
454impl Default for ConvertOption {
455    fn default() -> Self {
456        Self {
457            get_builtin_component: get_core_component,
458            is_dev: true,
459            directive_converters: FxHashMap::default(),
460        }
461    }
462}
463
464/// SFC info of the current template
465pub struct SFCInfo<'a> {
466    /// Compile the function for inlining inside setup().
467    /// This allows the function to directly access setup() local bindings.
468    pub inline: bool,
469    /// Indicates this SFC template has used :slotted in its styles
470    /// Defaults to `true` for backwards compatibility - SFC tooling should set it
471    /// to `false` if no `:slotted` usage is detected in `<style>`
472    pub slotted: bool,
473    pub scope_id: Option<String>,
474    /// Optional binding metadata analyzed from script - used to optimize
475    /// binding access when `prefixIdentifiers` is enabled.
476    pub binding_metadata: Rc<BindingMetadata<'a>>,
477    /// current SFC filename for self-referencing
478    pub self_name: String,
479}
480
481impl<'a> Default for SFCInfo<'a> {
482    fn default() -> Self {
483        Self {
484            scope_id: None,
485            inline: false,
486            slotted: true,
487            binding_metadata: Rc::new(BindingMetadata::default()),
488            self_name: "".into(),
489        }
490    }
491}
492
493pub struct BaseConverter<'a> {
494    pub err_handle: Box<dyn ErrorHandler>,
495    pub sfc_info: SFCInfo<'a>,
496    pub option: ConvertOption,
497}
498pub type BaseRoot<'a> = IRRoot<BaseConvertInfo<'a>>;
499pub type BaseIR<'a> = IRNode<BaseConvertInfo<'a>>;
500impl<'a> Converter<'a> for BaseConverter<'a> {
501    type IR = BaseRoot<'a>;
502    fn convert_ir(&self, ast: AstRoot<'a>) -> Self::IR {
503        self.convert_core_ir(ast)
504    }
505}
506impl<'a> CoreConverter<'a, BaseConvertInfo<'a>> for BaseConverter<'a> {
507    fn emit_error(&self, error: CompilationError) {
508        self.err_handle.on_error(error)
509    }
510
511    // platform specific methods
512    fn get_builtin_component(&self, tag: &str) -> Option<RuntimeHelper> {
513        (self.option.get_builtin_component)(tag)
514    }
515
516    // core template syntax conversion
517    fn convert_directive(
518        &self,
519        dir: &mut Directive<'a>,
520        e: &mut Element<'a>,
521    ) -> CoreDirConvRet<'a> {
522        if let Some(convert) = self.option.directive_converters.get(dir.name) {
523            convert(dir, e, self.err_handle.as_ref())
524        } else {
525            DirectiveConvertResult::Preserve
526        }
527    }
528    fn convert_if(&self, elems: Vec<Element<'a>>, key: usize) -> BaseIR<'a> {
529        v_if::convert_if(self, elems, key)
530    }
531    fn convert_for(&self, d: Directive<'a>, e: BaseIR<'a>) -> BaseIR<'a> {
532        v_for::convert_for(self, d, e)
533    }
534    // once/memo are noop on SSR/SSR-fallback. They only work in re-render
535    fn convert_memo(&self, _: Directive<'a>, n: BaseIR<'a>) -> BaseIR<'a> {
536        n
537    }
538    fn convert_once(&self, _: Directive<'a>, n: BaseIR<'a>) -> BaseIR<'a> {
539        n
540    }
541    fn convert_slot_outlet(&self, e: Element<'a>) -> BaseIR<'a> {
542        convert_slot_outlet::convert_slot_outlet(self, e)
543    }
544    fn convert_element(&self, e: Element<'a>) -> BaseIR<'a> {
545        convert_element::convert_element(self, e)
546    }
547    fn convert_text(&self, text: TextNode<'a>) -> BaseIR<'a> {
548        // TODO: reduce allocation by push to existing
549        let texts = text.text.into_iter().map(JsExpr::StrLit).collect();
550        IRNode::TextCall(TextIR {
551            fast_path: false,
552            need_patch: false,
553            texts,
554        })
555    }
556    fn convert_interpolation(&self, interp: SourceNode<'a>) -> BaseIR<'a> {
557        let expr = JsExpr::simple(interp.source);
558        let call = JsExpr::Call(RuntimeHelper::ToDisplayString, vec![expr]);
559        IRNode::TextCall(TextIR {
560            fast_path: false,
561            need_patch: false,
562            texts: smallvec![call],
563        })
564    }
565    fn convert_template(&self, e: Element<'a>) -> BaseIR<'a> {
566        convert_element::convert_template(self, e, false)
567    }
568    fn convert_comment(&self, c: SourceNode<'a>) -> BaseIR<'a> {
569        IRNode::CommentCall(c.source)
570    }
571}
572
573impl<'a> BaseConverter<'a> {
574    fn no_slotted(&self) -> bool {
575        self.sfc_info.scope_id.is_some() && !self.sfc_info.slotted
576    }
577}
578
579#[cfg(test)]
580pub mod test {
581    use super::*;
582    use crate::{cast, error::test::TestErrorHandler, parser::test::base_parse};
583    use BaseConverter as BC;
584    use JsExpr as Js;
585
586    pub fn assert_str_lit(expr: &Js, s: &str) {
587        let v = cast!(expr, Js::StrLit);
588        assert_eq!(v.raw, s);
589    }
590
591    #[test]
592    fn test_simplest() {
593        let body = base_convert("<p/>").body;
594        assert_eq!(body.len(), 1);
595        if let IRNode::VNodeCall(VNodeIR { tag, .. }) = &body[0] {
596            assert_str_lit(tag, "p");
597        } else {
598            panic!("wrong parsing");
599        }
600        let body = base_convert("hello world").body;
601        let t = cast!(&body[0], IRNode::TextCall);
602        assert_str_lit(&t.texts[0], "hello world");
603    }
604
605    #[test]
606    fn test_abort() {
607        base_convert("hello <p/> {{world}}");
608    }
609
610    pub fn base_convert(s: &str) -> BaseRoot {
611        let mut convs = FxHashMap::default();
612        for (n, f) in [v_bind::V_BIND, ("on", no_op_directive_convert)] {
613            convs.insert(n, f);
614        }
615        let option = ConvertOption {
616            directive_converters: convs,
617            ..Default::default()
618        };
619        let bc = BC {
620            err_handle: Box::new(TestErrorHandler),
621            sfc_info: Default::default(),
622            option,
623        };
624        let ast = base_parse(s);
625        bc.convert_ir(ast)
626    }
627}