minijinja/compiler/
instructions.rs

1#[cfg(feature = "internal_debug")]
2use std::fmt;
3
4use crate::compiler::tokens::Span;
5use crate::output::CaptureMode;
6use crate::value::Value;
7
8/// This loop has the loop var.
9pub const LOOP_FLAG_WITH_LOOP_VAR: u8 = 1;
10
11/// This loop is recursive.
12pub const LOOP_FLAG_RECURSIVE: u8 = 2;
13
14/// This macro uses the caller var.
15#[cfg(feature = "macros")]
16pub const MACRO_CALLER: u8 = 2;
17
18/// Rust type to represent locals.
19pub type LocalId = u8;
20
21/// The maximum number of filters/tests that can be cached.
22pub const MAX_LOCALS: usize = 50;
23
24/// Represents an instruction for the VM.
25#[cfg_attr(feature = "internal_debug", derive(Debug))]
26#[cfg_attr(
27    feature = "unstable_machinery_serde",
28    derive(serde::Serialize),
29    serde(tag = "op", content = "arg")
30)]
31#[derive(Clone)]
32pub enum Instruction<'source> {
33    /// Emits raw source
34    EmitRaw(&'source str),
35
36    /// Stores a variable (only possible in for loops)
37    StoreLocal(&'source str),
38
39    /// Load a variable,
40    Lookup(&'source str),
41
42    /// Looks up an attribute.
43    GetAttr(&'source str),
44
45    /// Sets an attribute.
46    SetAttr(&'source str),
47
48    /// Looks up an item.
49    GetItem,
50
51    /// Performs a slice operation.
52    Slice,
53
54    /// Loads a constant value.
55    LoadConst(Value),
56
57    /// Builds a map of the last n pairs on the stack.
58    BuildMap(usize),
59
60    /// Builds a kwargs map of the last n pairs on the stack.
61    BuildKwargs(usize),
62
63    /// Merges N kwargs maps on the list into one.
64    MergeKwargs(usize),
65
66    /// Builds a list of the last n pairs on the stack.
67    BuildList(Option<usize>),
68
69    /// Unpacks a list into N stack items.
70    UnpackList(usize),
71
72    /// Unpacks N lists onto the stack and pushes the number of items there were unpacked.
73    UnpackLists(usize),
74
75    /// Add the top two values
76    Add,
77
78    /// Subtract the top two values
79    Sub,
80
81    /// Multiply the top two values
82    Mul,
83
84    /// Divide the top two values
85    Div,
86
87    /// Integer divide the top two values as "integer".
88    ///
89    /// Note that in MiniJinja this currently uses an euclidean
90    /// division to match the rem implementation.  In Python this
91    /// instead uses a flooring division and a flooring remainder.
92    IntDiv,
93
94    /// Calculate the remainder the top two values
95    Rem,
96
97    /// x to the power of y.
98    Pow,
99
100    /// Negates the value.
101    Neg,
102
103    /// `=` operator
104    Eq,
105
106    /// `!=` operator
107    Ne,
108
109    /// `>` operator
110    Gt,
111
112    /// `>=` operator
113    Gte,
114
115    /// `<` operator
116    Lt,
117
118    /// `<=` operator
119    Lte,
120
121    /// Unary not
122    Not,
123
124    /// String concatenation operator
125    StringConcat,
126
127    /// Performs a containment check
128    In,
129
130    /// Apply a filter.
131    ApplyFilter(&'source str, Option<u16>, LocalId),
132
133    /// Perform a filter.
134    PerformTest(&'source str, Option<u16>, LocalId),
135
136    /// Emit the stack top as output
137    Emit,
138
139    /// Starts a loop
140    ///
141    /// The argument are loop flags.
142    PushLoop(u8),
143
144    /// Starts a with block.
145    PushWith,
146
147    /// Does a single loop iteration
148    ///
149    /// The argument is the jump target for when the loop
150    /// ends and must point to a `PopFrame` instruction.
151    Iterate(u32),
152
153    /// Push a bool that indicates that the loop iterated.
154    PushDidNotIterate,
155
156    /// Pops the topmost frame
157    PopFrame,
158
159    /// Pops the topmost frame and runs loop logic
160    PopLoopFrame,
161
162    /// Jump to a specific instruction
163    Jump(u32),
164
165    /// Jump if the stack top evaluates to false
166    JumpIfFalse(u32),
167
168    /// Jump if the stack top evaluates to false or pops the value
169    JumpIfFalseOrPop(u32),
170
171    /// Jump if the stack top evaluates to true or pops the value
172    JumpIfTrueOrPop(u32),
173
174    /// Sets the auto escape flag to the current value.
175    PushAutoEscape,
176
177    /// Resets the auto escape flag to the previous value.
178    PopAutoEscape,
179
180    /// Begins capturing of output (false) or discard (true).
181    BeginCapture(CaptureMode),
182
183    /// Ends capturing of output.
184    EndCapture,
185
186    /// Calls a global function
187    CallFunction(&'source str, Option<u16>),
188
189    /// Calls a method
190    CallMethod(&'source str, Option<u16>),
191
192    /// Calls an object
193    CallObject(Option<u16>),
194
195    /// Duplicates the top item
196    DupTop,
197
198    /// Discards the top item
199    DiscardTop,
200
201    /// A fast super instruction without intermediate capturing.
202    FastSuper,
203
204    /// A fast loop recurse instruction without intermediate capturing.
205    FastRecurse,
206
207    /// Swaps the top two items in the stack.
208    Swap,
209
210    /// Call into a block.
211    #[cfg(feature = "multi_template")]
212    CallBlock(&'source str),
213
214    /// Loads block from a template with name on stack ("extends")
215    #[cfg(feature = "multi_template")]
216    LoadBlocks,
217
218    /// Includes another template.
219    #[cfg(feature = "multi_template")]
220    Include(bool),
221
222    /// Builds a module
223    #[cfg(feature = "multi_template")]
224    ExportLocals,
225
226    /// Builds a macro on the stack.
227    #[cfg(feature = "macros")]
228    BuildMacro(&'source str, u32, u8),
229
230    /// Breaks from the interpreter loop (exists a function)
231    #[cfg(feature = "macros")]
232    Return,
233
234    /// True if the value is undefined
235    #[cfg(feature = "macros")]
236    IsUndefined,
237
238    /// Encloses a variable.
239    #[cfg(feature = "macros")]
240    Enclose(&'source str),
241
242    /// Returns the closure of this context level.
243    #[cfg(feature = "macros")]
244    GetClosure,
245}
246
247#[derive(Copy, Clone)]
248struct LineInfo {
249    first_instruction: u32,
250    line: u16,
251}
252
253#[cfg(feature = "debug")]
254#[derive(Copy, Clone)]
255struct SpanInfo {
256    first_instruction: u32,
257    span: Span,
258}
259
260/// Wrapper around instructions to help with location management.
261pub struct Instructions<'source> {
262    pub(crate) instructions: Vec<Instruction<'source>>,
263    line_infos: Vec<LineInfo>,
264    #[cfg(feature = "debug")]
265    span_infos: Vec<SpanInfo>,
266    name: &'source str,
267    source: &'source str,
268}
269
270pub(crate) static EMPTY_INSTRUCTIONS: Instructions<'static> = Instructions {
271    instructions: Vec::new(),
272    line_infos: Vec::new(),
273    #[cfg(feature = "debug")]
274    span_infos: Vec::new(),
275    name: "<unknown>",
276    source: "",
277};
278
279impl<'source> Instructions<'source> {
280    /// Creates a new instructions object.
281    pub fn new(name: &'source str, source: &'source str) -> Instructions<'source> {
282        Instructions {
283            instructions: Vec::with_capacity(128),
284            line_infos: Vec::with_capacity(128),
285            #[cfg(feature = "debug")]
286            span_infos: Vec::with_capacity(128),
287            name,
288            source,
289        }
290    }
291
292    /// Returns the name of the template.
293    pub fn name(&self) -> &'source str {
294        self.name
295    }
296
297    /// Returns the source reference.
298    pub fn source(&self) -> &'source str {
299        self.source
300    }
301
302    /// Returns an instruction by index
303    #[inline(always)]
304    pub fn get(&self, idx: u32) -> Option<&Instruction<'source>> {
305        self.instructions.get(idx as usize)
306    }
307
308    /// Returns an instruction by index mutably
309    pub fn get_mut(&mut self, idx: u32) -> Option<&mut Instruction<'source>> {
310        self.instructions.get_mut(idx as usize)
311    }
312
313    /// Adds a new instruction
314    pub fn add(&mut self, instr: Instruction<'source>) -> u32 {
315        let rv = self.instructions.len();
316        self.instructions.push(instr);
317        rv as u32
318    }
319
320    fn add_line_record(&mut self, instr: u32, line: u16) {
321        let same_loc = self
322            .line_infos
323            .last()
324            .is_some_and(|last_loc| last_loc.line == line);
325        if !same_loc {
326            self.line_infos.push(LineInfo {
327                first_instruction: instr,
328                line,
329            });
330        }
331    }
332
333    /// Adds a new instruction with line number.
334    pub fn add_with_line(&mut self, instr: Instruction<'source>, line: u16) -> u32 {
335        let rv = self.add(instr);
336        self.add_line_record(rv, line);
337
338        // if we follow up to a valid span with no more span, clear it out
339        #[cfg(feature = "debug")]
340        {
341            if self
342                .span_infos
343                .last()
344                .is_some_and(|x| x.span != Span::default())
345            {
346                self.span_infos.push(SpanInfo {
347                    first_instruction: rv,
348                    span: Span::default(),
349                });
350            }
351        }
352        rv
353    }
354
355    /// Adds a new instruction with span.
356    pub fn add_with_span(&mut self, instr: Instruction<'source>, span: Span) -> u32 {
357        let rv = self.add(instr);
358        #[cfg(feature = "debug")]
359        {
360            let same_loc = self
361                .span_infos
362                .last()
363                .is_some_and(|last_loc| last_loc.span == span);
364            if !same_loc {
365                self.span_infos.push(SpanInfo {
366                    first_instruction: rv,
367                    span,
368                });
369            }
370        }
371        self.add_line_record(rv, span.start_line);
372        rv
373    }
374
375    /// Looks up the line for an instruction
376    pub fn get_line(&self, idx: u32) -> Option<usize> {
377        let loc = match self
378            .line_infos
379            .binary_search_by_key(&idx, |x| x.first_instruction)
380        {
381            Ok(idx) => &self.line_infos[idx],
382            Err(0) => return None,
383            Err(idx) => &self.line_infos[idx - 1],
384        };
385        Some(loc.line as usize)
386    }
387
388    /// Looks up a span for an instruction.
389    pub fn get_span(&self, idx: u32) -> Option<Span> {
390        #[cfg(feature = "debug")]
391        {
392            let loc = match self
393                .span_infos
394                .binary_search_by_key(&idx, |x| x.first_instruction)
395            {
396                Ok(idx) => &self.span_infos[idx],
397                Err(0) => return None,
398                Err(idx) => &self.span_infos[idx - 1],
399            };
400            (loc.span != Span::default()).then_some(loc.span)
401        }
402        #[cfg(not(feature = "debug"))]
403        {
404            let _ = idx;
405            None
406        }
407    }
408
409    /// Returns a list of all names referenced in the current block backwards
410    /// from the given pc.
411    #[cfg(feature = "debug")]
412    pub fn get_referenced_names(&self, idx: u32) -> Vec<&'source str> {
413        let mut rv = Vec::new();
414        // make sure we don't crash on empty instructions
415        if self.instructions.is_empty() {
416            return rv;
417        }
418        let idx = (idx as usize).min(self.instructions.len() - 1);
419        for instr in self.instructions[..=idx].iter().rev() {
420            let name = match instr {
421                Instruction::Lookup(name)
422                | Instruction::StoreLocal(name)
423                | Instruction::CallFunction(name, _) => *name,
424                Instruction::PushLoop(flags) if flags & LOOP_FLAG_WITH_LOOP_VAR != 0 => "loop",
425                Instruction::PushLoop(_) | Instruction::PushWith => break,
426                _ => continue,
427            };
428            if !rv.contains(&name) {
429                rv.push(name);
430            }
431        }
432        rv
433    }
434
435    /// Returns the number of instructions
436    pub fn len(&self) -> usize {
437        self.instructions.len()
438    }
439
440    /// Do we have any instructions?
441    #[allow(unused)]
442    pub fn is_empty(&self) -> bool {
443        self.instructions.is_empty()
444    }
445}
446
447#[cfg(feature = "internal_debug")]
448impl fmt::Debug for Instructions<'_> {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        struct InstructionWrapper<'a>(usize, &'a Instruction<'a>, Option<usize>);
451
452        impl fmt::Debug for InstructionWrapper<'_> {
453            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454                ok!(write!(f, "{:>05} | {:?}", self.0, self.1,));
455                if let Some(line) = self.2 {
456                    ok!(write!(f, "  [line {line}]"));
457                }
458                Ok(())
459            }
460        }
461
462        let mut list = f.debug_list();
463        let mut last_line = None;
464        for (idx, instr) in self.instructions.iter().enumerate() {
465            let line = self.get_line(idx as u32);
466            list.entry(&InstructionWrapper(
467                idx,
468                instr,
469                if line != last_line { line } else { None },
470            ));
471            last_line = line;
472        }
473        list.finish()
474    }
475}
476
477#[test]
478#[cfg(target_pointer_width = "64")]
479fn test_sizes() {
480    assert_eq!(std::mem::size_of::<Instruction>(), 32);
481}