feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+631
View File
@@ -0,0 +1,631 @@
use std::ops::Deref;
#[cfg(feature = "internal_debug")]
use std::fmt;
use crate::compiler::tokens::Span;
use crate::value::{ops, value_map_with_capacity, Value};
/// Container for nodes with location info.
///
/// This container fulfills two purposes: it adds location information
/// to nodes, but it also ensures the nodes is heap allocated. The
/// latter is useful to ensure that enum variants do not cause the enum
/// to become too large.
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Spanned<T> {
inner: Box<(T, Span)>,
}
impl<T> Spanned<T> {
/// Creates a new spanned node.
pub fn new(node: T, span: Span) -> Spanned<T> {
Spanned {
inner: Box::new((node, span)),
}
}
/// Accesses the span.
pub fn span(&self) -> Span {
self.inner.1
}
}
impl<T> Deref for Spanned<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner.0
}
}
#[cfg(feature = "internal_debug")]
impl<T: fmt::Debug> fmt::Debug for Spanned<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ok!(fmt::Debug::fmt(&self.inner.0, f));
write!(f, "{:?}", self.inner.1)
}
}
/// A statement node.
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "stmt")
)]
pub enum Stmt<'a> {
Template(Spanned<Template<'a>>),
EmitExpr(Spanned<EmitExpr<'a>>),
EmitRaw(Spanned<EmitRaw<'a>>),
ForLoop(Spanned<ForLoop<'a>>),
IfCond(Spanned<IfCond<'a>>),
WithBlock(Spanned<WithBlock<'a>>),
Set(Spanned<Set<'a>>),
SetBlock(Spanned<SetBlock<'a>>),
AutoEscape(Spanned<AutoEscape<'a>>),
FilterBlock(Spanned<FilterBlock<'a>>),
#[cfg(feature = "multi_template")]
Block(Spanned<Block<'a>>),
#[cfg(feature = "multi_template")]
Import(Spanned<Import<'a>>),
#[cfg(feature = "multi_template")]
FromImport(Spanned<FromImport<'a>>),
#[cfg(feature = "multi_template")]
Extends(Spanned<Extends<'a>>),
#[cfg(feature = "multi_template")]
Include(Spanned<Include<'a>>),
#[cfg(feature = "macros")]
Macro(Spanned<Macro<'a>>),
#[cfg(feature = "macros")]
CallBlock(Spanned<CallBlock<'a>>),
#[cfg(feature = "loop_controls")]
Continue(Spanned<Continue>),
#[cfg(feature = "loop_controls")]
Break(Spanned<Break>),
Do(Spanned<Do<'a>>),
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Stmt<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Stmt::Template(s) => fmt::Debug::fmt(s, f),
Stmt::EmitExpr(s) => fmt::Debug::fmt(s, f),
Stmt::EmitRaw(s) => fmt::Debug::fmt(s, f),
Stmt::ForLoop(s) => fmt::Debug::fmt(s, f),
Stmt::IfCond(s) => fmt::Debug::fmt(s, f),
Stmt::WithBlock(s) => fmt::Debug::fmt(s, f),
Stmt::Set(s) => fmt::Debug::fmt(s, f),
Stmt::SetBlock(s) => fmt::Debug::fmt(s, f),
Stmt::AutoEscape(s) => fmt::Debug::fmt(s, f),
Stmt::FilterBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Block(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Extends(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Include(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::Import(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi_template")]
Stmt::FromImport(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "macros")]
Stmt::Macro(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "macros")]
Stmt::CallBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Continue(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Break(s) => fmt::Debug::fmt(s, f),
Stmt::Do(s) => fmt::Debug::fmt(s, f),
}
}
}
/// An expression node.
#[allow(clippy::enum_variant_names)]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "expr")
)]
pub enum Expr<'a> {
Var(Spanned<Var<'a>>),
Const(Spanned<Const>),
Slice(Spanned<Slice<'a>>),
UnaryOp(Spanned<UnaryOp<'a>>),
BinOp(Spanned<BinOp<'a>>),
IfExpr(Spanned<IfExpr<'a>>),
Filter(Spanned<Filter<'a>>),
Test(Spanned<Test<'a>>),
GetAttr(Spanned<GetAttr<'a>>),
GetItem(Spanned<GetItem<'a>>),
Call(Spanned<Call<'a>>),
List(Spanned<List<'a>>),
Map(Spanned<Map<'a>>),
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Expr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Var(s) => fmt::Debug::fmt(s, f),
Expr::Const(s) => fmt::Debug::fmt(s, f),
Expr::Slice(s) => fmt::Debug::fmt(s, f),
Expr::UnaryOp(s) => fmt::Debug::fmt(s, f),
Expr::BinOp(s) => fmt::Debug::fmt(s, f),
Expr::IfExpr(s) => fmt::Debug::fmt(s, f),
Expr::Filter(s) => fmt::Debug::fmt(s, f),
Expr::Test(s) => fmt::Debug::fmt(s, f),
Expr::GetAttr(s) => fmt::Debug::fmt(s, f),
Expr::GetItem(s) => fmt::Debug::fmt(s, f),
Expr::Call(s) => fmt::Debug::fmt(s, f),
Expr::List(s) => fmt::Debug::fmt(s, f),
Expr::Map(s) => fmt::Debug::fmt(s, f),
}
}
}
impl Expr<'_> {
pub fn description(&self) -> &'static str {
match self {
Expr::Var(_) => "variable",
Expr::Const(_) => "constant",
Expr::Slice(_)
| Expr::UnaryOp(_)
| Expr::BinOp(_)
| Expr::IfExpr(_)
| Expr::GetAttr(_)
| Expr::GetItem(_) => "expression",
Expr::Call(_) => "call",
Expr::List(_) => "list literal",
Expr::Map(_) => "map literal",
Expr::Test(_) => "test expression",
Expr::Filter(_) => "filter expression",
}
}
pub fn span(&self) -> Span {
match self {
Expr::Var(s) => s.span(),
Expr::Const(s) => s.span(),
Expr::Slice(s) => s.span(),
Expr::UnaryOp(s) => s.span(),
Expr::BinOp(s) => s.span(),
Expr::IfExpr(s) => s.span(),
Expr::Filter(s) => s.span(),
Expr::Test(s) => s.span(),
Expr::GetAttr(s) => s.span(),
Expr::GetItem(s) => s.span(),
Expr::Call(s) => s.span(),
Expr::List(s) => s.span(),
Expr::Map(s) => s.span(),
}
}
pub fn as_const(&self) -> Option<Value> {
match self {
Expr::Const(c) => Some(c.value.clone()),
Expr::List(l) => l.as_const(),
Expr::Map(m) => m.as_const(),
Expr::UnaryOp(c) => match c.op {
UnaryOpKind::Not => c.expr.as_const().map(|value| Value::from(!value.is_true())),
UnaryOpKind::Neg => c.expr.as_const().and_then(|v| ops::neg(&v).ok()),
},
Expr::BinOp(c) => {
let (Some(left), Some(right)) = (c.left.as_const(), c.right.as_const()) else {
return None;
};
match c.op {
BinOpKind::Add => ops::add(&left, &right).ok(),
BinOpKind::Sub => ops::sub(&left, &right).ok(),
BinOpKind::Mul => ops::mul(&left, &right).ok(),
BinOpKind::Div => ops::div(&left, &right).ok(),
BinOpKind::FloorDiv => ops::int_div(&left, &right).ok(),
BinOpKind::Rem => ops::rem(&left, &right).ok(),
BinOpKind::Pow => ops::pow(&left, &right).ok(),
BinOpKind::Concat => Some(ops::string_concat(left, &right)),
BinOpKind::Eq => Some(Value::from(left == right)),
BinOpKind::Ne => Some(Value::from(left != right)),
BinOpKind::Lt => Some(Value::from(left < right)),
BinOpKind::Lte => Some(Value::from(left <= right)),
BinOpKind::Gt => Some(Value::from(left > right)),
BinOpKind::Gte => Some(Value::from(left >= right)),
BinOpKind::In => ops::contains(&right, &left).ok(),
BinOpKind::ScAnd => Some(if left.is_true() && right.is_true() {
right
} else {
Value::from(false)
}),
BinOpKind::ScOr => Some(if left.is_true() { left } else { right }),
}
}
_ => None,
}
}
}
/// Root template node.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Template<'a> {
pub children: Vec<Stmt<'a>>,
}
/// A for loop.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct ForLoop<'a> {
pub target: Expr<'a>,
pub iter: Expr<'a>,
pub filter_expr: Option<Expr<'a>>,
pub recursive: bool,
pub body: Vec<Stmt<'a>>,
pub else_body: Vec<Stmt<'a>>,
}
/// An if/else condition.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct IfCond<'a> {
pub expr: Expr<'a>,
pub true_body: Vec<Stmt<'a>>,
pub false_body: Vec<Stmt<'a>>,
}
/// A with block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct WithBlock<'a> {
pub assignments: Vec<(Expr<'a>, Expr<'a>)>,
pub body: Vec<Stmt<'a>>,
}
/// A set statement.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Set<'a> {
pub target: Expr<'a>,
pub expr: Expr<'a>,
}
/// A set capture statement.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct SetBlock<'a> {
pub target: Expr<'a>,
pub filter: Option<Expr<'a>>,
pub body: Vec<Stmt<'a>>,
}
/// A block for inheritance elements.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Block<'a> {
pub name: &'a str,
pub body: Vec<Stmt<'a>>,
}
/// An extends block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Extends<'a> {
pub name: Expr<'a>,
}
/// An include block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Include<'a> {
pub name: Expr<'a>,
pub ignore_missing: bool,
}
/// An auto escape control block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct AutoEscape<'a> {
pub enabled: Expr<'a>,
pub body: Vec<Stmt<'a>>,
}
/// Applies filters to a block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct FilterBlock<'a> {
pub filter: Expr<'a>,
pub body: Vec<Stmt<'a>>,
}
/// Declares a macro.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "macros")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Macro<'a> {
pub name: &'a str,
pub args: Vec<Expr<'a>>,
pub defaults: Vec<Expr<'a>>,
pub body: Vec<Stmt<'a>>,
}
/// A call block
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "macros")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct CallBlock<'a> {
pub call: Spanned<Call<'a>>,
pub macro_decl: Spanned<Macro<'a>>,
}
/// Continue
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Continue;
/// Break
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Break;
/// A call block
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Do<'a> {
pub call: Spanned<Call<'a>>,
}
/// A "from" import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct FromImport<'a> {
pub expr: Expr<'a>,
pub names: Vec<(Expr<'a>, Option<Expr<'a>>)>,
}
/// A full module import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi_template")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Import<'a> {
pub expr: Expr<'a>,
pub name: Expr<'a>,
}
/// Outputs the expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct EmitExpr<'a> {
pub expr: Expr<'a>,
}
/// Outputs raw template code.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct EmitRaw<'a> {
pub raw: &'a str,
}
/// Looks up a variable.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Var<'a> {
pub id: &'a str,
}
/// Loads a constant
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Const {
pub value: Value,
}
/// Represents a slice.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Slice<'a> {
pub expr: Expr<'a>,
pub start: Option<Expr<'a>>,
pub stop: Option<Expr<'a>>,
pub step: Option<Expr<'a>>,
}
/// A kind of unary operator.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum UnaryOpKind {
Not,
Neg,
}
/// An unary operator expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct UnaryOp<'a> {
pub op: UnaryOpKind,
pub expr: Expr<'a>,
}
/// A kind of binary operator.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum BinOpKind {
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
ScAnd,
ScOr,
Add,
Sub,
Mul,
Div,
FloorDiv,
Rem,
Pow,
Concat,
In,
}
/// A binary operator expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct BinOp<'a> {
pub op: BinOpKind,
pub left: Expr<'a>,
pub right: Expr<'a>,
}
/// An if expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct IfExpr<'a> {
pub test_expr: Expr<'a>,
pub true_expr: Expr<'a>,
pub false_expr: Option<Expr<'a>>,
}
/// A filter expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Filter<'a> {
pub name: &'a str,
pub expr: Option<Expr<'a>>,
pub args: Vec<CallArg<'a>>,
}
/// A test expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Test<'a> {
pub name: &'a str,
pub expr: Expr<'a>,
pub args: Vec<CallArg<'a>>,
}
/// An attribute lookup expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct GetAttr<'a> {
pub expr: Expr<'a>,
pub name: &'a str,
}
/// An item lookup expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct GetItem<'a> {
pub expr: Expr<'a>,
pub subscript_expr: Expr<'a>,
}
/// Calls something.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Call<'a> {
pub expr: Expr<'a>,
pub args: Vec<CallArg<'a>>,
}
/// A call argument helper
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum CallArg<'a> {
Pos(Expr<'a>),
Kwarg(&'a str, Expr<'a>),
PosSplat(Expr<'a>),
KwargSplat(Expr<'a>),
}
/// Creates a list of values.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct List<'a> {
pub items: Vec<Expr<'a>>,
}
impl List<'_> {
pub fn as_const(&self) -> Option<Value> {
if !self.items.iter().all(|x| matches!(x, Expr::Const(_))) {
return None;
}
let items = self.items.iter();
let sequence = items.filter_map(|expr| match expr {
Expr::Const(v) => Some(v.value.clone()),
_ => None,
});
Some(Value::from(sequence.collect::<Vec<_>>()))
}
}
/// Creates a map of values.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Map<'a> {
pub keys: Vec<Expr<'a>>,
pub values: Vec<Expr<'a>>,
}
impl Map<'_> {
pub fn as_const(&self) -> Option<Value> {
if !self.keys.iter().all(|x| matches!(x, Expr::Const(_)))
|| !self.values.iter().all(|x| matches!(x, Expr::Const(_)))
{
return None;
}
let mut rv = value_map_with_capacity(self.keys.len());
for (key, value) in self.keys.iter().zip(self.values.iter()) {
if let (Expr::Const(maybe_key), Expr::Const(value)) = (key, value) {
rv.insert(maybe_key.value.clone(), value.value.clone());
}
}
Some(Value::from_object(rv))
}
}
/// Defines the specific type of call.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub enum CallType<'ast, 'source> {
Function(&'source str),
Method(&'ast Expr<'source>, &'source str),
#[cfg(feature = "multi_template")]
Block(&'source str),
Object(&'ast Expr<'source>),
}
impl<'a> Call<'a> {
/// Try to isolate a method call.
///
/// name + call and attribute lookup + call are really method
/// calls which are easier to handle for the compiler as a separate
/// thing.
pub fn identify_call(&self) -> CallType<'_, 'a> {
match self.expr {
Expr::Var(ref var) => CallType::Function(var.id),
Expr::GetAttr(ref attr) => {
#[cfg(feature = "multi_template")]
{
if let Expr::Var(ref var) = attr.expr {
if var.id == "self" {
return CallType::Block(attr.name);
}
}
}
CallType::Method(&attr.expr, attr.name)
}
_ => CallType::Object(&self.expr),
}
}
}
+982
View File
@@ -0,0 +1,982 @@
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::mem;
use crate::compiler::ast;
use crate::compiler::instructions::{
Instruction, Instructions, LocalId, LOOP_FLAG_RECURSIVE, LOOP_FLAG_WITH_LOOP_VAR, MAX_LOCALS,
};
use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::ops::neg;
use crate::value::{Kwargs, UndefinedType, Value, ValueMap, ValueRepr};
#[cfg(test)]
use similar_asserts::assert_eq;
#[cfg(feature = "macros")]
type Caller<'source> = ast::Spanned<ast::Macro<'source>>;
#[cfg(not(feature = "macros"))]
type Caller<'source> = std::marker::PhantomData<&'source ()>;
/// For the first `MAX_LOCALS` filters/tests, an ID is returned for faster lookups from the stack.
fn get_local_id<'source>(ids: &mut BTreeMap<&'source str, LocalId>, name: &'source str) -> LocalId {
if let Some(id) = ids.get(name) {
*id
} else if ids.len() >= MAX_LOCALS {
!0
} else {
let next_id = ids.len() as LocalId;
ids.insert(name, next_id);
next_id
}
}
/// Represents an open block of code that does not yet have updated
/// jump targets.
enum PendingBlock {
Branch {
jump_instr: u32,
},
Loop {
iter_instr: u32,
jump_instrs: Vec<u32>,
},
ScBool {
jump_instrs: Vec<u32>,
},
}
const CODEGEN_POOL_MAX_ITEMS: usize = 64;
const CODEGEN_POOLED_MAX_CAPACITY: usize = 64;
thread_local! {
static PENDING_BLOCK_POOL: RefCell<Vec<Vec<PendingBlock>>> = const { RefCell::new(Vec::new()) };
static SPAN_STACK_POOL: RefCell<Vec<Vec<Span>>> = const { RefCell::new(Vec::new()) };
}
#[inline(always)]
fn take_pending_block_buffer() -> Vec<PendingBlock> {
let mut buf = PENDING_BLOCK_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_else(|| Vec::with_capacity(8));
buf.clear();
buf
}
#[inline(always)]
fn take_span_stack_buffer() -> Vec<Span> {
let mut buf = SPAN_STACK_POOL
.with(|pool| pool.borrow_mut().pop())
.unwrap_or_else(|| Vec::with_capacity(8));
buf.clear();
buf
}
#[inline(always)]
fn recycle_pending_block_buffer(mut buf: Vec<PendingBlock>) {
if buf.capacity() <= CODEGEN_POOLED_MAX_CAPACITY {
buf.clear();
PENDING_BLOCK_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < CODEGEN_POOL_MAX_ITEMS {
pool.push(buf);
}
});
}
}
#[inline(always)]
fn recycle_span_stack_buffer(mut buf: Vec<Span>) {
if buf.capacity() <= CODEGEN_POOLED_MAX_CAPACITY {
buf.clear();
SPAN_STACK_POOL.with(|pool| {
let mut pool = pool.borrow_mut();
if pool.len() < CODEGEN_POOL_MAX_ITEMS {
pool.push(buf);
}
});
}
}
/// Provides a convenient interface to creating instructions for the VM.
pub struct CodeGenerator<'source> {
instructions: Instructions<'source>,
blocks: BTreeMap<&'source str, Instructions<'source>>,
pending_block: Vec<PendingBlock>,
current_line: u16,
span_stack: Vec<Span>,
filter_local_ids: BTreeMap<&'source str, LocalId>,
test_local_ids: BTreeMap<&'source str, LocalId>,
raw_template_bytes: usize,
}
impl<'source> CodeGenerator<'source> {
/// Creates a new code generator.
pub fn new(file: &'source str, source: &'source str) -> CodeGenerator<'source> {
CodeGenerator {
instructions: Instructions::new(file, source),
blocks: BTreeMap::new(),
pending_block: take_pending_block_buffer(),
current_line: 0,
span_stack: take_span_stack_buffer(),
filter_local_ids: BTreeMap::new(),
test_local_ids: BTreeMap::new(),
raw_template_bytes: 0,
}
}
/// Sets the current location's line.
pub fn set_line(&mut self, lineno: u16) {
self.current_line = lineno;
}
/// Sets line from span.
pub fn set_line_from_span(&mut self, span: Span) {
self.set_line(span.start_line);
}
/// Pushes a span to the stack
pub fn push_span(&mut self, span: Span) {
self.span_stack.push(span);
self.set_line_from_span(span);
}
/// Pops a span from the stack.
pub fn pop_span(&mut self) {
self.span_stack.pop();
}
/// Add a simple instruction with the current location.
pub fn add(&mut self, instr: Instruction<'source>) -> u32 {
if let Some(span) = self.span_stack.last() {
if span.start_line == self.current_line {
return self.instructions.add_with_span(instr, *span);
}
}
self.instructions.add_with_line(instr, self.current_line)
}
/// Add a simple instruction with other location.
pub fn add_with_span(&mut self, instr: Instruction<'source>, span: Span) -> u32 {
self.instructions.add_with_span(instr, span)
}
/// Returns the next instruction index.
pub fn next_instruction(&self) -> u32 {
self.instructions.len() as u32
}
/// Creates a sub generator.
#[cfg(feature = "multi_template")]
fn new_subgenerator(&self) -> CodeGenerator<'source> {
let mut sub = CodeGenerator::new(self.instructions.name(), self.instructions.source());
sub.current_line = self.current_line;
sub.span_stack = self.span_stack.last().copied().into_iter().collect();
sub
}
/// Finishes a sub generator and syncs it back.
#[cfg(feature = "multi_template")]
fn finish_subgenerator(&mut self, sub: CodeGenerator<'source>) -> Instructions<'source> {
self.current_line = sub.current_line;
let (instructions, blocks) = sub.finish();
self.blocks.extend(blocks);
instructions
}
/// Starts a for loop
pub fn start_for_loop(&mut self, with_loop_var: bool, recursive: bool) {
let mut flags = 0;
if with_loop_var {
flags |= LOOP_FLAG_WITH_LOOP_VAR;
}
if recursive {
flags |= LOOP_FLAG_RECURSIVE;
}
self.add(Instruction::PushLoop(flags));
let instr = self.add(Instruction::Iterate(!0));
self.pending_block.push(PendingBlock::Loop {
iter_instr: instr,
jump_instrs: Vec::new(),
});
}
/// Ends the open for loop
pub fn end_for_loop(&mut self, push_did_not_iterate: bool) {
if let Some(PendingBlock::Loop {
iter_instr,
jump_instrs,
}) = self.pending_block.pop()
{
self.add(Instruction::Jump(iter_instr));
let loop_end = self.next_instruction();
if push_did_not_iterate {
self.add(Instruction::PushDidNotIterate);
};
self.add(Instruction::PopLoopFrame);
for instr in jump_instrs.into_iter().chain(Some(iter_instr)) {
match self.instructions.get_mut(instr) {
Some(&mut Instruction::Iterate(ref mut jump_target))
| Some(&mut Instruction::Jump(ref mut jump_target)) => {
*jump_target = loop_end;
}
_ => unreachable!(),
}
}
} else {
unreachable!()
}
}
/// Begins an if conditional
pub fn start_if(&mut self) {
let jump_instr = self.add(Instruction::JumpIfFalse(!0));
self.pending_block.push(PendingBlock::Branch { jump_instr });
}
/// Begins an else conditional
pub fn start_else(&mut self) {
let jump_instr = self.add(Instruction::Jump(!0));
self.end_condition(jump_instr + 1);
self.pending_block.push(PendingBlock::Branch { jump_instr });
}
/// Closes the current if block.
pub fn end_if(&mut self) {
self.end_condition(self.next_instruction());
}
/// Starts a short-circuited bool block.
pub fn start_sc_bool(&mut self) {
self.pending_block.push(PendingBlock::ScBool {
jump_instrs: Vec::new(),
});
}
/// Emits a short-circuited bool operator.
pub fn sc_bool(&mut self, and: bool) {
if let Some(&mut PendingBlock::ScBool {
ref mut jump_instrs,
}) = self.pending_block.last_mut()
{
jump_instrs.push(self.instructions.add(if and {
Instruction::JumpIfFalseOrPop(!0)
} else {
Instruction::JumpIfTrueOrPop(!0)
}));
} else {
unreachable!();
}
}
/// Ends a short-circuited bool block.
pub fn end_sc_bool(&mut self) {
let end = self.next_instruction();
if let Some(PendingBlock::ScBool { jump_instrs }) = self.pending_block.pop() {
for instr in jump_instrs {
match self.instructions.get_mut(instr) {
Some(&mut Instruction::JumpIfFalseOrPop(ref mut target))
| Some(&mut Instruction::JumpIfTrueOrPop(ref mut target)) => {
*target = end;
}
_ => unreachable!(),
}
}
}
}
fn end_condition(&mut self, new_jump_instr: u32) {
match self.pending_block.pop() {
Some(PendingBlock::Branch { jump_instr }) => {
match self.instructions.get_mut(jump_instr) {
Some(&mut Instruction::JumpIfFalse(ref mut target))
| Some(&mut Instruction::Jump(ref mut target)) => {
*target = new_jump_instr;
}
_ => {}
}
}
_ => unreachable!(),
}
}
/// Compiles a statement.
pub fn compile_stmt(&mut self, stmt: &ast::Stmt<'source>) {
match stmt {
ast::Stmt::Template(t) => {
self.set_line_from_span(t.span());
for node in &t.children {
self.compile_stmt(node);
}
}
ast::Stmt::EmitExpr(expr) => {
self.compile_emit_expr(expr);
}
ast::Stmt::EmitRaw(raw) => {
self.set_line_from_span(raw.span());
self.add(Instruction::EmitRaw(raw.raw));
self.raw_template_bytes += raw.raw.len();
}
ast::Stmt::ForLoop(for_loop) => {
self.compile_for_loop(for_loop);
}
ast::Stmt::IfCond(if_cond) => {
self.compile_if_stmt(if_cond);
}
ast::Stmt::WithBlock(with_block) => {
self.set_line_from_span(with_block.span());
self.add(Instruction::PushWith);
for (target, expr) in &with_block.assignments {
self.compile_expr(expr);
self.compile_assignment(target);
}
for node in &with_block.body {
self.compile_stmt(node);
}
self.add(Instruction::PopFrame);
}
ast::Stmt::Set(set) => {
self.set_line_from_span(set.span());
self.compile_expr(&set.expr);
self.compile_assignment(&set.target);
}
ast::Stmt::SetBlock(set_block) => {
self.set_line_from_span(set_block.span());
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &set_block.body {
self.compile_stmt(node);
}
self.add(Instruction::EndCapture);
if let Some(ref filter) = set_block.filter {
self.compile_expr(filter);
}
self.compile_assignment(&set_block.target);
}
ast::Stmt::AutoEscape(auto_escape) => {
self.set_line_from_span(auto_escape.span());
self.compile_expr(&auto_escape.enabled);
self.add(Instruction::PushAutoEscape);
for node in &auto_escape.body {
self.compile_stmt(node);
}
self.add(Instruction::PopAutoEscape);
}
ast::Stmt::FilterBlock(filter_block) => {
self.set_line_from_span(filter_block.span());
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &filter_block.body {
self.compile_stmt(node);
}
self.add(Instruction::EndCapture);
self.compile_expr(&filter_block.filter);
self.add(Instruction::Emit);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Block(block) => {
self.compile_block(block);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Import(import) => {
self.add(Instruction::BeginCapture(CaptureMode::Capture));
self.add(Instruction::PushWith);
self.compile_expr(&import.expr);
self.add_with_span(Instruction::Include(false), import.span());
self.add(Instruction::EndCapture);
self.add(Instruction::ExportLocals);
self.add(Instruction::PopFrame);
self.compile_assignment(&import.name);
}
#[cfg(feature = "multi_template")]
ast::Stmt::FromImport(from_import) => {
self.add(Instruction::BeginCapture(CaptureMode::Discard));
self.add(Instruction::PushWith);
self.compile_expr(&from_import.expr);
self.add_with_span(Instruction::Include(false), from_import.span());
for (name, _) in &from_import.names {
self.compile_expr(name);
}
self.add(Instruction::PopFrame);
for (name, alias) in from_import.names.iter().rev() {
self.compile_assignment(alias.as_ref().unwrap_or(name));
}
self.add(Instruction::EndCapture);
}
#[cfg(feature = "multi_template")]
ast::Stmt::Extends(extends) => {
self.set_line_from_span(extends.span());
self.compile_expr(&extends.name);
self.add_with_span(Instruction::LoadBlocks, extends.span());
}
#[cfg(feature = "multi_template")]
ast::Stmt::Include(include) => {
self.set_line_from_span(include.span());
self.compile_expr(&include.name);
self.add_with_span(Instruction::Include(include.ignore_missing), include.span());
}
#[cfg(feature = "macros")]
ast::Stmt::Macro(macro_decl) => {
self.compile_macro(macro_decl);
}
#[cfg(feature = "macros")]
ast::Stmt::CallBlock(call_block) => {
self.compile_call_block(call_block);
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(cont) => {
self.set_line_from_span(cont.span());
for pending_block in self.pending_block.iter().rev() {
if let PendingBlock::Loop { iter_instr, .. } = pending_block {
self.add(Instruction::Jump(*iter_instr));
break;
}
}
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Break(brk) => {
self.set_line_from_span(brk.span());
let instr = self.add(Instruction::Jump(0));
for pending_block in self.pending_block.iter_mut().rev() {
if let &mut PendingBlock::Loop {
ref mut jump_instrs,
..
} = pending_block
{
jump_instrs.push(instr);
break;
}
}
}
ast::Stmt::Do(do_tag) => {
self.compile_do(do_tag);
}
}
}
#[cfg(feature = "multi_template")]
fn compile_block(&mut self, block: &ast::Spanned<ast::Block<'source>>) {
self.set_line_from_span(block.span());
let mut sub = self.new_subgenerator();
for node in &block.body {
sub.compile_stmt(node);
}
let instructions = self.finish_subgenerator(sub);
self.blocks.insert(block.name, instructions);
self.add(Instruction::CallBlock(block.name));
}
#[cfg(feature = "macros")]
fn compile_macro_expression(&mut self, macro_decl: &ast::Spanned<ast::Macro<'source>>) {
use crate::compiler::instructions::MACRO_CALLER;
self.set_line_from_span(macro_decl.span());
let instr = self.add(Instruction::Jump(!0));
let mut defaults_iter = macro_decl.defaults.iter().rev();
for arg in macro_decl.args.iter().rev() {
if let Some(default) = defaults_iter.next() {
self.add(Instruction::DupTop);
self.add(Instruction::IsUndefined);
self.start_if();
self.add(Instruction::DiscardTop);
self.compile_expr(default);
self.end_if();
}
self.compile_assignment(arg);
}
for node in &macro_decl.body {
self.compile_stmt(node);
}
self.add(Instruction::Return);
let mut undeclared = crate::compiler::meta::find_macro_closure(macro_decl);
let caller_reference = undeclared.remove("caller");
let macro_instr = self.next_instruction();
for name in &undeclared {
self.add(Instruction::Enclose(name));
}
self.add(Instruction::GetClosure);
self.add(Instruction::LoadConst(Value::from_object(
macro_decl
.args
.iter()
.map(|x| match x {
ast::Expr::Var(var) => Value::from(var.id),
_ => unreachable!(),
})
.collect::<Vec<Value>>(),
)));
let mut flags = 0;
if caller_reference {
flags |= MACRO_CALLER;
}
self.add(Instruction::BuildMacro(macro_decl.name, instr + 1, flags));
if let Some(&mut Instruction::Jump(ref mut target)) = self.instructions.get_mut(instr) {
*target = macro_instr;
} else {
unreachable!();
}
}
#[cfg(feature = "macros")]
fn compile_macro(&mut self, macro_decl: &ast::Spanned<ast::Macro<'source>>) {
self.compile_macro_expression(macro_decl);
self.add(Instruction::StoreLocal(macro_decl.name));
}
#[cfg(feature = "macros")]
fn compile_call_block(&mut self, call_block: &ast::Spanned<ast::CallBlock<'source>>) {
self.compile_call(&call_block.call, Some(&call_block.macro_decl));
self.add(Instruction::Emit);
}
fn compile_do(&mut self, do_tag: &ast::Spanned<ast::Do<'source>>) {
self.compile_call(&do_tag.call, None);
}
fn compile_if_stmt(&mut self, if_cond: &ast::Spanned<ast::IfCond<'source>>) {
self.set_line_from_span(if_cond.span());
self.push_span(if_cond.expr.span());
self.compile_expr(&if_cond.expr);
self.start_if();
self.pop_span();
for node in &if_cond.true_body {
self.compile_stmt(node);
}
if !if_cond.false_body.is_empty() {
self.start_else();
for node in &if_cond.false_body {
self.compile_stmt(node);
}
}
self.end_if();
}
fn compile_emit_expr(&mut self, expr: &ast::Spanned<ast::EmitExpr<'source>>) {
if let ast::Expr::Call(call) = &expr.expr {
self.set_line_from_span(expr.expr.span());
match call.identify_call() {
ast::CallType::Function(name) => {
if name == "super" && call.args.is_empty() {
self.add_with_span(Instruction::FastSuper, call.span());
return;
} else if name == "loop" && call.args.len() == 1 {
self.compile_call_args(std::slice::from_ref(&call.args[0]), 0, None);
self.add_with_span(Instruction::FastRecurse, call.span());
return;
}
}
#[cfg(feature = "multi_template")]
ast::CallType::Block(name) => {
self.add(Instruction::CallBlock(name));
return;
}
_ => {}
}
}
self.push_span(expr.expr.span());
self.compile_expr(&expr.expr);
self.add(Instruction::Emit);
self.pop_span();
}
fn compile_for_loop(&mut self, for_loop: &ast::Spanned<ast::ForLoop<'source>>) {
self.set_line_from_span(for_loop.span());
// filter expressions work like a nested for loop without
// the special loop variable. in one loop, the condition is checked and
// passing items accumulated into a list. in the second, that list is
// iterated over normally
if let Some(ref filter_expr) = for_loop.filter_expr {
self.add(Instruction::LoadConst(Value::from(0usize)));
self.push_span(filter_expr.span());
self.compile_expr(&for_loop.iter);
self.start_for_loop(false, false);
self.add(Instruction::DupTop);
self.compile_assignment(&for_loop.target);
self.compile_expr(filter_expr);
self.start_if();
self.add(Instruction::Swap);
self.add(Instruction::LoadConst(Value::from(1usize)));
self.add(Instruction::Add);
self.start_else();
self.add(Instruction::DiscardTop);
self.end_if();
self.pop_span();
self.end_for_loop(false);
self.add(Instruction::BuildList(None));
self.start_for_loop(true, for_loop.recursive);
} else {
self.push_span(for_loop.iter.span());
self.compile_expr(&for_loop.iter);
self.start_for_loop(true, for_loop.recursive);
self.pop_span();
}
self.compile_assignment(&for_loop.target);
for node in &for_loop.body {
self.compile_stmt(node);
}
self.end_for_loop(!for_loop.else_body.is_empty());
if !for_loop.else_body.is_empty() {
self.start_if();
for node in &for_loop.else_body {
self.compile_stmt(node);
}
self.end_if();
};
}
/// Compiles an assignment expression.
pub fn compile_assignment(&mut self, expr: &ast::Expr<'source>) {
match expr {
ast::Expr::Var(var) => {
self.add(Instruction::StoreLocal(var.id));
}
ast::Expr::List(list) => {
self.push_span(list.span());
self.add(Instruction::UnpackList(list.items.len()));
for expr in &list.items {
self.compile_assignment(expr);
}
self.pop_span();
}
ast::Expr::GetAttr(attr) => {
self.push_span(attr.span());
self.compile_expr(&attr.expr);
self.add(Instruction::SetAttr(attr.name));
}
_ => unreachable!(),
}
}
/// Compiles an expression.
pub fn compile_expr(&mut self, expr: &ast::Expr<'source>) {
// try to do constant folding
if let Some(v) = expr.as_const() {
self.set_line_from_span(expr.span());
self.add(Instruction::LoadConst(v.clone()));
return;
}
match expr {
ast::Expr::Var(v) => {
self.set_line_from_span(v.span());
self.add(Instruction::Lookup(v.id));
}
ast::Expr::Const(_) => unreachable!(), // handled by constant folding
ast::Expr::Slice(s) => {
self.push_span(s.span());
self.compile_expr(&s.expr);
if let Some(ref start) = s.start {
self.compile_expr(start);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
if let Some(ref stop) = s.stop {
self.compile_expr(stop);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
if let Some(ref step) = s.step {
self.compile_expr(step);
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
self.add(Instruction::Slice);
self.pop_span();
}
ast::Expr::UnaryOp(c) => {
self.set_line_from_span(c.span());
match c.op {
ast::UnaryOpKind::Not => {
self.compile_expr(&c.expr);
self.add(Instruction::Not);
}
ast::UnaryOpKind::Neg => {
// common case: negative numbers. In that case we
// directly negate them if this is possible without
// an error.
if let ast::Expr::Const(ref c) = c.expr {
if let Ok(negated) = neg(&c.value) {
self.add(Instruction::LoadConst(negated));
return;
}
}
self.compile_expr(&c.expr);
self.add_with_span(Instruction::Neg, c.span());
}
}
}
ast::Expr::BinOp(c) => {
self.compile_bin_op(c);
}
ast::Expr::IfExpr(i) => {
self.set_line_from_span(i.span());
self.compile_expr(&i.test_expr);
self.start_if();
self.compile_expr(&i.true_expr);
self.start_else();
if let Some(ref false_expr) = i.false_expr {
self.compile_expr(false_expr);
} else {
// special behavior: missing false block have a silent undefined
// to permit special casing. This is for compatibility also with
// what Jinja2 does.
self.add(Instruction::LoadConst(
ValueRepr::Undefined(UndefinedType::Silent).into(),
));
}
self.end_if();
}
ast::Expr::Filter(f) => {
self.push_span(f.span());
if let Some(ref expr) = f.expr {
self.compile_expr(expr);
}
let arg_count = self.compile_call_args(&f.args, 1, None);
let local_id = get_local_id(&mut self.filter_local_ids, f.name);
self.add(Instruction::ApplyFilter(f.name, arg_count, local_id));
self.pop_span();
}
ast::Expr::Test(f) => {
self.push_span(f.span());
self.compile_expr(&f.expr);
let arg_count = self.compile_call_args(&f.args, 1, None);
let local_id = get_local_id(&mut self.test_local_ids, f.name);
self.add(Instruction::PerformTest(f.name, arg_count, local_id));
self.pop_span();
}
ast::Expr::GetAttr(g) => {
self.push_span(g.span());
self.compile_expr(&g.expr);
self.add(Instruction::GetAttr(g.name));
self.pop_span();
}
ast::Expr::GetItem(g) => {
self.push_span(g.span());
self.compile_expr(&g.expr);
self.compile_expr(&g.subscript_expr);
self.add(Instruction::GetItem);
self.pop_span();
}
ast::Expr::Call(c) => {
self.compile_call(c, None);
}
ast::Expr::List(l) => {
self.set_line_from_span(l.span());
for item in &l.items {
self.compile_expr(item);
}
self.add(Instruction::BuildList(Some(l.items.len())));
}
ast::Expr::Map(m) => {
self.set_line_from_span(m.span());
assert_eq!(m.keys.len(), m.values.len());
for (key, value) in m.keys.iter().zip(m.values.iter()) {
self.compile_expr(key);
self.compile_expr(value);
}
self.add(Instruction::BuildMap(m.keys.len()));
}
}
}
fn compile_call(
&mut self,
c: &ast::Spanned<ast::Call<'source>>,
caller: Option<&Caller<'source>>,
) {
self.push_span(c.span());
match c.identify_call() {
ast::CallType::Function(name) => {
let arg_count = self.compile_call_args(&c.args, 0, caller);
self.add(Instruction::CallFunction(name, arg_count));
}
#[cfg(feature = "multi_template")]
ast::CallType::Block(name) => {
self.add(Instruction::BeginCapture(CaptureMode::Capture));
self.add(Instruction::CallBlock(name));
self.add(Instruction::EndCapture);
}
ast::CallType::Method(expr, name) => {
self.compile_expr(expr);
let arg_count = self.compile_call_args(&c.args, 1, caller);
self.add(Instruction::CallMethod(name, arg_count));
}
ast::CallType::Object(expr) => {
self.compile_expr(expr);
let arg_count = self.compile_call_args(&c.args, 1, caller);
self.add(Instruction::CallObject(arg_count));
}
};
self.pop_span();
}
fn compile_call_args(
&mut self,
args: &[ast::CallArg<'source>],
extra_args: usize,
caller: Option<&Caller<'source>>,
) -> Option<u16> {
let mut pending_args = extra_args;
let mut num_args_batches = 0;
let mut has_kwargs = caller.is_some();
let mut static_kwargs = caller.is_none();
for arg in args {
match arg {
ast::CallArg::Pos(expr) => {
self.compile_expr(expr);
pending_args += 1;
}
ast::CallArg::PosSplat(expr) => {
if pending_args > 0 {
self.add(Instruction::BuildList(Some(pending_args)));
pending_args = 0;
num_args_batches += 1;
}
self.compile_expr(expr);
num_args_batches += 1;
}
ast::CallArg::Kwarg(_, expr) => {
if !matches!(expr, ast::Expr::Const(_)) {
static_kwargs = false;
}
has_kwargs = true;
}
ast::CallArg::KwargSplat(_) => {
static_kwargs = false;
has_kwargs = true;
}
}
}
if has_kwargs {
let mut pending_kwargs = 0;
let mut num_kwargs_batches = 0;
let mut collected_kwargs = ValueMap::new();
for arg in args {
match arg {
ast::CallArg::Kwarg(key, value) => {
if static_kwargs {
if let ast::Expr::Const(c) = value {
collected_kwargs.insert(Value::from(*key), c.value.clone());
} else {
unreachable!();
}
} else {
self.add(Instruction::LoadConst(Value::from(*key)));
self.compile_expr(value);
pending_kwargs += 1;
}
}
ast::CallArg::KwargSplat(expr) => {
if pending_kwargs > 0 {
self.add(Instruction::BuildKwargs(pending_kwargs));
num_kwargs_batches += 1;
pending_kwargs = 0;
}
self.compile_expr(expr);
num_kwargs_batches += 1;
}
ast::CallArg::Pos(_) | ast::CallArg::PosSplat(_) => {}
}
}
if !collected_kwargs.is_empty() {
self.add(Instruction::LoadConst(Kwargs::wrap(collected_kwargs)));
} else {
// The conditions above guarantee that if we collect static kwargs
// we cannot enter this block (single kwargs batch, no caller).
#[cfg(feature = "macros")]
{
if let Some(caller) = caller {
self.add(Instruction::LoadConst(Value::from("caller")));
self.compile_macro_expression(caller);
pending_kwargs += 1
}
}
if num_kwargs_batches > 0 {
if pending_kwargs > 0 {
self.add(Instruction::BuildKwargs(pending_kwargs));
num_kwargs_batches += 1;
}
self.add(Instruction::MergeKwargs(num_kwargs_batches));
} else {
self.add(Instruction::BuildKwargs(pending_kwargs));
}
}
pending_args += 1;
}
if num_args_batches > 0 {
if pending_args > 0 {
self.add(Instruction::BuildList(Some(pending_args)));
num_args_batches += 1;
}
self.add(Instruction::UnpackLists(num_args_batches));
None
} else {
assert!(pending_args as u16 as usize == pending_args);
Some(pending_args as u16)
}
}
fn compile_bin_op(&mut self, c: &ast::Spanned<ast::BinOp<'source>>) {
self.push_span(c.span());
let instr = match c.op {
ast::BinOpKind::Eq => Instruction::Eq,
ast::BinOpKind::Ne => Instruction::Ne,
ast::BinOpKind::Lt => Instruction::Lt,
ast::BinOpKind::Lte => Instruction::Lte,
ast::BinOpKind::Gt => Instruction::Gt,
ast::BinOpKind::Gte => Instruction::Gte,
ast::BinOpKind::ScAnd | ast::BinOpKind::ScOr => {
self.start_sc_bool();
self.compile_expr(&c.left);
self.sc_bool(matches!(c.op, ast::BinOpKind::ScAnd));
self.compile_expr(&c.right);
self.end_sc_bool();
self.pop_span();
return;
}
ast::BinOpKind::Add => Instruction::Add,
ast::BinOpKind::Sub => Instruction::Sub,
ast::BinOpKind::Mul => Instruction::Mul,
ast::BinOpKind::Div => Instruction::Div,
ast::BinOpKind::FloorDiv => Instruction::IntDiv,
ast::BinOpKind::Rem => Instruction::Rem,
ast::BinOpKind::Pow => Instruction::Pow,
ast::BinOpKind::Concat => Instruction::StringConcat,
ast::BinOpKind::In => Instruction::In,
};
self.compile_expr(&c.left);
self.compile_expr(&c.right);
self.add(instr);
self.pop_span();
}
/// Returns the size hint for buffers.
///
/// This is a proposal for the initial buffer size when rendering directly to a string.
pub fn buffer_size_hint(&self) -> usize {
// for now the assumption is made that twice the bytes of template code without
// control structures, rounded up to the next power of two is a good default. The
// round to the next power of two is chosen because the underlying vector backing
// strings prefers powers of two.
(self.raw_template_bytes * 2).next_power_of_two()
}
/// Converts the compiler into the instructions.
pub fn finish(
mut self,
) -> (
Instructions<'source>,
BTreeMap<&'source str, Instructions<'source>>,
) {
assert!(self.pending_block.is_empty());
recycle_pending_block_buffer(mem::take(&mut self.pending_block));
recycle_span_stack_buffer(mem::take(&mut self.span_stack));
(self.instructions, self.blocks)
}
}
+481
View File
@@ -0,0 +1,481 @@
#[cfg(feature = "internal_debug")]
use std::fmt;
use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::Value;
/// This loop has the loop var.
pub const LOOP_FLAG_WITH_LOOP_VAR: u8 = 1;
/// This loop is recursive.
pub const LOOP_FLAG_RECURSIVE: u8 = 2;
/// This macro uses the caller var.
#[cfg(feature = "macros")]
pub const MACRO_CALLER: u8 = 2;
/// Rust type to represent locals.
pub type LocalId = u8;
/// The maximum number of filters/tests that can be cached.
pub const MAX_LOCALS: usize = 50;
/// Represents an instruction for the VM.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "op", content = "arg")
)]
#[derive(Clone)]
pub enum Instruction<'source> {
/// Emits raw source
EmitRaw(&'source str),
/// Stores a variable (only possible in for loops)
StoreLocal(&'source str),
/// Load a variable,
Lookup(&'source str),
/// Looks up an attribute.
GetAttr(&'source str),
/// Sets an attribute.
SetAttr(&'source str),
/// Looks up an item.
GetItem,
/// Performs a slice operation.
Slice,
/// Loads a constant value.
LoadConst(Value),
/// Builds a map of the last n pairs on the stack.
BuildMap(usize),
/// Builds a kwargs map of the last n pairs on the stack.
BuildKwargs(usize),
/// Merges N kwargs maps on the list into one.
MergeKwargs(usize),
/// Builds a list of the last n pairs on the stack.
BuildList(Option<usize>),
/// Unpacks a list into N stack items.
UnpackList(usize),
/// Unpacks N lists onto the stack and pushes the number of items there were unpacked.
UnpackLists(usize),
/// Add the top two values
Add,
/// Subtract the top two values
Sub,
/// Multiply the top two values
Mul,
/// Divide the top two values
Div,
/// Integer divide the top two values as "integer".
///
/// Note that in MiniJinja this currently uses an euclidean
/// division to match the rem implementation. In Python this
/// instead uses a flooring division and a flooring remainder.
IntDiv,
/// Calculate the remainder the top two values
Rem,
/// x to the power of y.
Pow,
/// Negates the value.
Neg,
/// `=` operator
Eq,
/// `!=` operator
Ne,
/// `>` operator
Gt,
/// `>=` operator
Gte,
/// `<` operator
Lt,
/// `<=` operator
Lte,
/// Unary not
Not,
/// String concatenation operator
StringConcat,
/// Performs a containment check
In,
/// Apply a filter.
ApplyFilter(&'source str, Option<u16>, LocalId),
/// Perform a filter.
PerformTest(&'source str, Option<u16>, LocalId),
/// Emit the stack top as output
Emit,
/// Starts a loop
///
/// The argument are loop flags.
PushLoop(u8),
/// Starts a with block.
PushWith,
/// Does a single loop iteration
///
/// The argument is the jump target for when the loop
/// ends and must point to a `PopFrame` instruction.
Iterate(u32),
/// Push a bool that indicates that the loop iterated.
PushDidNotIterate,
/// Pops the topmost frame
PopFrame,
/// Pops the topmost frame and runs loop logic
PopLoopFrame,
/// Jump to a specific instruction
Jump(u32),
/// Jump if the stack top evaluates to false
JumpIfFalse(u32),
/// Jump if the stack top evaluates to false or pops the value
JumpIfFalseOrPop(u32),
/// Jump if the stack top evaluates to true or pops the value
JumpIfTrueOrPop(u32),
/// Sets the auto escape flag to the current value.
PushAutoEscape,
/// Resets the auto escape flag to the previous value.
PopAutoEscape,
/// Begins capturing of output (false) or discard (true).
BeginCapture(CaptureMode),
/// Ends capturing of output.
EndCapture,
/// Calls a global function
CallFunction(&'source str, Option<u16>),
/// Calls a method
CallMethod(&'source str, Option<u16>),
/// Calls an object
CallObject(Option<u16>),
/// Duplicates the top item
DupTop,
/// Discards the top item
DiscardTop,
/// A fast super instruction without intermediate capturing.
FastSuper,
/// A fast loop recurse instruction without intermediate capturing.
FastRecurse,
/// Swaps the top two items in the stack.
Swap,
/// Call into a block.
#[cfg(feature = "multi_template")]
CallBlock(&'source str),
/// Loads block from a template with name on stack ("extends")
#[cfg(feature = "multi_template")]
LoadBlocks,
/// Includes another template.
#[cfg(feature = "multi_template")]
Include(bool),
/// Builds a module
#[cfg(feature = "multi_template")]
ExportLocals,
/// Builds a macro on the stack.
#[cfg(feature = "macros")]
BuildMacro(&'source str, u32, u8),
/// Breaks from the interpreter loop (exists a function)
#[cfg(feature = "macros")]
Return,
/// True if the value is undefined
#[cfg(feature = "macros")]
IsUndefined,
/// Encloses a variable.
#[cfg(feature = "macros")]
Enclose(&'source str),
/// Returns the closure of this context level.
#[cfg(feature = "macros")]
GetClosure,
}
#[derive(Copy, Clone)]
struct LineInfo {
first_instruction: u32,
line: u16,
}
#[cfg(feature = "debug")]
#[derive(Copy, Clone)]
struct SpanInfo {
first_instruction: u32,
span: Span,
}
/// Wrapper around instructions to help with location management.
pub struct Instructions<'source> {
pub(crate) instructions: Vec<Instruction<'source>>,
line_infos: Vec<LineInfo>,
#[cfg(feature = "debug")]
span_infos: Vec<SpanInfo>,
name: &'source str,
source: &'source str,
}
pub(crate) static EMPTY_INSTRUCTIONS: Instructions<'static> = Instructions {
instructions: Vec::new(),
line_infos: Vec::new(),
#[cfg(feature = "debug")]
span_infos: Vec::new(),
name: "<unknown>",
source: "",
};
impl<'source> Instructions<'source> {
/// Creates a new instructions object.
pub fn new(name: &'source str, source: &'source str) -> Instructions<'source> {
Instructions {
instructions: Vec::with_capacity(256),
line_infos: Vec::with_capacity(128),
#[cfg(feature = "debug")]
span_infos: Vec::with_capacity(128),
name,
source,
}
}
/// Returns the name of the template.
pub fn name(&self) -> &'source str {
self.name
}
/// Returns the source reference.
pub fn source(&self) -> &'source str {
self.source
}
/// Returns an instruction by index
#[inline(always)]
pub fn get(&self, idx: u32) -> Option<&Instruction<'source>> {
self.instructions.get(idx as usize)
}
/// Returns an instruction by index mutably
pub fn get_mut(&mut self, idx: u32) -> Option<&mut Instruction<'source>> {
self.instructions.get_mut(idx as usize)
}
/// Adds a new instruction
pub fn add(&mut self, instr: Instruction<'source>) -> u32 {
let rv = self.instructions.len();
self.instructions.push(instr);
rv as u32
}
fn add_line_record(&mut self, instr: u32, line: u16) {
let same_loc = self
.line_infos
.last()
.is_some_and(|last_loc| last_loc.line == line);
if !same_loc {
self.line_infos.push(LineInfo {
first_instruction: instr,
line,
});
}
}
/// Adds a new instruction with line number.
pub fn add_with_line(&mut self, instr: Instruction<'source>, line: u16) -> u32 {
let rv = self.add(instr);
self.add_line_record(rv, line);
// if we follow up to a valid span with no more span, clear it out
#[cfg(feature = "debug")]
{
if self
.span_infos
.last()
.is_some_and(|x| x.span != Span::default())
{
self.span_infos.push(SpanInfo {
first_instruction: rv,
span: Span::default(),
});
}
}
rv
}
/// Adds a new instruction with span.
pub fn add_with_span(&mut self, instr: Instruction<'source>, span: Span) -> u32 {
let rv = self.add(instr);
#[cfg(feature = "debug")]
{
let same_loc = self
.span_infos
.last()
.is_some_and(|last_loc| last_loc.span == span);
if !same_loc {
self.span_infos.push(SpanInfo {
first_instruction: rv,
span,
});
}
}
self.add_line_record(rv, span.start_line);
rv
}
/// Looks up the line for an instruction
pub fn get_line(&self, idx: u32) -> Option<usize> {
let loc = match self
.line_infos
.binary_search_by_key(&idx, |x| x.first_instruction)
{
Ok(idx) => &self.line_infos[idx],
Err(0) => return None,
Err(idx) => &self.line_infos[idx - 1],
};
Some(loc.line as usize)
}
/// Looks up a span for an instruction.
pub fn get_span(&self, idx: u32) -> Option<Span> {
#[cfg(feature = "debug")]
{
let loc = match self
.span_infos
.binary_search_by_key(&idx, |x| x.first_instruction)
{
Ok(idx) => &self.span_infos[idx],
Err(0) => return None,
Err(idx) => &self.span_infos[idx - 1],
};
(loc.span != Span::default()).then_some(loc.span)
}
#[cfg(not(feature = "debug"))]
{
let _ = idx;
None
}
}
/// Returns a list of all names referenced in the current block backwards
/// from the given pc.
#[cfg(feature = "debug")]
pub fn get_referenced_names(&self, idx: u32) -> Vec<&'source str> {
let mut rv = Vec::new();
// make sure we don't crash on empty instructions
if self.instructions.is_empty() {
return rv;
}
let idx = (idx as usize).min(self.instructions.len() - 1);
for instr in self.instructions[..=idx].iter().rev() {
let name = match instr {
Instruction::Lookup(name)
| Instruction::StoreLocal(name)
| Instruction::CallFunction(name, _) => *name,
Instruction::PushLoop(flags) if flags & LOOP_FLAG_WITH_LOOP_VAR != 0 => "loop",
Instruction::PushLoop(_) | Instruction::PushWith => break,
_ => continue,
};
if !rv.contains(&name) {
rv.push(name);
}
}
rv
}
/// Returns the number of instructions
pub fn len(&self) -> usize {
self.instructions.len()
}
/// Do we have any instructions?
#[allow(unused)]
pub fn is_empty(&self) -> bool {
self.instructions.is_empty()
}
}
#[cfg(feature = "internal_debug")]
impl fmt::Debug for Instructions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct InstructionWrapper<'a>(usize, &'a Instruction<'a>, Option<usize>);
impl fmt::Debug for InstructionWrapper<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
ok!(write!(f, "{:>05} | {:?}", self.0, self.1,));
if let Some(line) = self.2 {
ok!(write!(f, " [line {line}]"));
}
Ok(())
}
}
let mut list = f.debug_list();
let mut last_line = None;
for (idx, instr) in self.instructions.iter().enumerate() {
let line = self.get_line(idx as u32);
list.entry(&InstructionWrapper(
idx,
instr,
if line != last_line { line } else { None },
));
last_line = line;
}
list.finish()
}
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_sizes() {
assert_eq!(std::mem::size_of::<Instruction>(), 32);
}
File diff suppressed because it is too large. Load diff
+307
View File
@@ -0,0 +1,307 @@
use std::collections::HashSet;
use std::fmt::Write;
use crate::compiler::ast;
struct AssignmentTracker<'a> {
out: HashSet<&'a str>,
nested_out: Option<HashSet<String>>,
assigned: Vec<HashSet<&'a str>>,
}
impl<'a> AssignmentTracker<'a> {
fn is_assigned(&self, name: &str) -> bool {
self.assigned.iter().any(|x| x.contains(name))
}
fn assign(&mut self, name: &'a str) {
self.assigned.last_mut().unwrap().insert(name);
}
fn assign_nested(&mut self, name: String) {
if let Some(ref mut nested_out) = self.nested_out {
if !nested_out.contains(&name) {
nested_out.insert(name);
}
}
}
fn push(&mut self) {
self.assigned.push(Default::default());
}
fn pop(&mut self) {
self.assigned.pop();
}
}
/// Finds all variables that need to be captured as closure for a macro.
#[cfg(feature = "macros")]
pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
let mut state = AssignmentTracker {
out: HashSet::new(),
nested_out: None,
assigned: vec![Default::default()],
};
tracker_visit_macro(m, &mut state, false);
state.out
}
/// Finds all variables that are undeclared in a template.
pub fn find_undeclared(t: &ast::Stmt<'_>, track_nested: bool) -> HashSet<String> {
let mut state = AssignmentTracker {
out: HashSet::new(),
nested_out: if track_nested {
Some(HashSet::new())
} else {
None
},
assigned: vec![Default::default()],
};
track_walk(t, &mut state);
if let Some(nested) = state.nested_out {
nested
} else {
state.out.into_iter().map(|x| x.to_string()).collect()
}
}
fn tracker_visit_expr_opt<'a>(expr: &Option<ast::Expr<'a>>, state: &mut AssignmentTracker<'a>) {
if let Some(expr) = expr {
tracker_visit_expr(expr, state);
}
}
#[cfg(feature = "macros")]
fn tracker_visit_macro<'a>(
m: &ast::Macro<'a>,
state: &mut AssignmentTracker<'a>,
declare_caller: bool,
) {
if declare_caller {
// this is not completely correct as caller is actually only defined
// if the macro was used in the context of a call block. However it
// is impossible to determine this at compile time so we err on the
// side of assuming caller is there.
state.assign("caller");
}
m.args.iter().for_each(|arg| track_assign(arg, state));
m.defaults
.iter()
.for_each(|expr| tracker_visit_expr(expr, state));
m.body.iter().for_each(|node| track_walk(node, state));
}
fn tracker_visit_callarg<'a>(callarg: &ast::CallArg<'a>, state: &mut AssignmentTracker<'a>) {
match callarg {
ast::CallArg::Pos(expr)
| ast::CallArg::Kwarg(_, expr)
| ast::CallArg::PosSplat(expr)
| ast::CallArg::KwargSplat(expr) => tracker_visit_expr(expr, state),
}
}
fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
match expr {
ast::Expr::Var(var) => {
if !state.is_assigned(var.id) {
state.out.insert(var.id);
// if we are not tracking nested assignments, we can consider a variable
// to be assigned the first time we perform a lookup.
if state.nested_out.is_none() {
state.assign(var.id);
} else {
state.assign_nested(var.id.to_string());
}
}
}
ast::Expr::Const(_) => {}
ast::Expr::UnaryOp(expr) => tracker_visit_expr(&expr.expr, state),
ast::Expr::BinOp(expr) => {
tracker_visit_expr(&expr.left, state);
tracker_visit_expr(&expr.right, state);
}
ast::Expr::IfExpr(expr) => {
tracker_visit_expr(&expr.test_expr, state);
tracker_visit_expr(&expr.true_expr, state);
tracker_visit_expr_opt(&expr.false_expr, state);
}
ast::Expr::Filter(expr) => {
tracker_visit_expr_opt(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::Test(expr) => {
tracker_visit_expr(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::GetAttr(expr) => {
// if we are tracking nested, we check if we have a chain of attribute
// lookups that terminate in a variable lookup. In that case we can
// assign the nested lookup.
if state.nested_out.is_some() {
let mut attrs = vec![expr.name];
let mut ptr = &expr.expr;
loop {
match ptr {
ast::Expr::Var(var) => {
if !state.is_assigned(var.id) {
let mut rv = var.id.to_string();
for attr in attrs.iter().rev() {
write!(rv, ".{attr}").ok();
}
state.assign_nested(rv);
return;
} else {
break;
}
}
ast::Expr::GetAttr(expr) => {
attrs.push(expr.name);
ptr = &expr.expr;
continue;
}
_ => break,
}
}
}
tracker_visit_expr(&expr.expr, state)
}
ast::Expr::GetItem(expr) => {
tracker_visit_expr(&expr.expr, state);
tracker_visit_expr(&expr.subscript_expr, state);
}
ast::Expr::Slice(slice) => {
tracker_visit_expr_opt(&slice.start, state);
tracker_visit_expr_opt(&slice.stop, state);
tracker_visit_expr_opt(&slice.step, state);
}
ast::Expr::Call(expr) => {
tracker_visit_expr(&expr.expr, state);
expr.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
ast::Expr::List(expr) => expr.items.iter().for_each(|x| tracker_visit_expr(x, state)),
ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
tracker_visit_expr(k, state);
tracker_visit_expr(v, state);
}),
}
}
fn track_assign<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
match expr {
ast::Expr::Var(var) => state.assign(var.id),
ast::Expr::List(list) => list.items.iter().for_each(|x| track_assign(x, state)),
_ => {}
}
}
fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
match node {
ast::Stmt::Template(stmt) => {
state.assign("self");
stmt.children.iter().for_each(|x| track_walk(x, state));
}
ast::Stmt::EmitExpr(expr) => tracker_visit_expr(&expr.expr, state),
ast::Stmt::EmitRaw(_) => {}
ast::Stmt::ForLoop(stmt) => {
state.push();
state.assign("loop");
tracker_visit_expr(&stmt.iter, state);
track_assign(&stmt.target, state);
tracker_visit_expr_opt(&stmt.filter_expr, state);
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
state.push();
stmt.else_body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::IfCond(stmt) => {
tracker_visit_expr(&stmt.expr, state);
state.push();
stmt.true_body.iter().for_each(|x| track_walk(x, state));
state.pop();
state.push();
stmt.false_body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::WithBlock(stmt) => {
state.push();
for (target, expr) in &stmt.assignments {
track_assign(target, state);
tracker_visit_expr(expr, state);
}
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::Set(stmt) => {
track_assign(&stmt.target, state);
tracker_visit_expr(&stmt.expr, state);
}
ast::Stmt::AutoEscape(stmt) => {
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::FilterBlock(stmt) => {
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
ast::Stmt::SetBlock(stmt) => {
track_assign(&stmt.target, state);
state.push();
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
#[cfg(feature = "multi_template")]
ast::Stmt::Block(stmt) => {
state.push();
state.assign("super");
stmt.body.iter().for_each(|x| track_walk(x, state));
state.pop();
}
#[cfg(feature = "multi_template")]
ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
#[cfg(feature = "multi_template")]
ast::Stmt::Import(stmt) => {
track_assign(&stmt.name, state);
}
#[cfg(feature = "multi_template")]
ast::Stmt::FromImport(stmt) => stmt.names.iter().for_each(|(arg, alias)| {
track_assign(alias.as_ref().unwrap_or(arg), state);
}),
#[cfg(feature = "macros")]
ast::Stmt::Macro(stmt) => {
state.assign(stmt.name);
state.push();
tracker_visit_macro(stmt, state, true);
state.pop();
}
#[cfg(feature = "macros")]
ast::Stmt::CallBlock(stmt) => {
tracker_visit_expr(&stmt.call.expr, state);
stmt.call
.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
state.push();
tracker_visit_macro(&stmt.macro_decl, state, true);
state.pop();
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(_) | ast::Stmt::Break(_) => {}
ast::Stmt::Do(stmt) => {
tracker_visit_expr(&stmt.call.expr, state);
stmt.call
.args
.iter()
.for_each(|x| tracker_visit_callarg(x, state));
}
}
}
+9
View File
@@ -0,0 +1,9 @@
#![allow(missing_docs)]
/// This module contains the internals of the compiler.
pub mod ast;
pub mod codegen;
pub mod instructions;
pub mod lexer;
pub mod meta;
pub mod parser;
pub mod tokens;
File diff suppressed because it is too large. Load diff
+157
View File
@@ -0,0 +1,157 @@
use std::fmt;
/// Represents a token in the stream.
#[derive(Debug)]
#[cfg_attr(
feature = "unstable_machinery_serde",
derive(serde::Serialize),
serde(tag = "name", content = "payload")
)]
pub enum Token<'a> {
/// Raw template data.
TemplateData(&'a str),
/// Variable block start.
VariableStart,
/// Variable block end
VariableEnd,
/// Statement block start
BlockStart,
/// Statement block end
BlockEnd,
/// An identifier.
Ident(&'a str),
/// A borrowed string.
Str(&'a str),
/// An allocated string.
String(Box<str>),
/// An integer (limited to i64)
Int(u64),
/// A large integer
Int128(Box<u128>),
/// A float
Float(f64),
/// A plus (`+`) operator.
Plus,
/// A plus (`-`) operator.
Minus,
/// A mul (`*`) operator.
Mul,
/// A div (`/`) operator.
Div,
/// A floor division (`//`) operator.
FloorDiv,
/// Power operator (`**`).
Pow,
/// A mod (`%`) operator.
Mod,
/// A dot operator (`.`)
Dot,
/// The comma operator (`,`)
Comma,
/// The colon operator (`:`)
Colon,
/// The tilde operator (`~`)
Tilde,
/// The assignment operator (`=`)
Assign,
/// The pipe symbol.
Pipe,
/// `==` operator
Eq,
/// `!=` operator
Ne,
/// `>` operator
Gt,
/// `>=` operator
Gte,
/// `<` operator
Lt,
/// `<=` operator
Lte,
/// Open Bracket
BracketOpen,
/// Close Bracket
BracketClose,
/// Open Parenthesis
ParenOpen,
/// Close Parenthesis
ParenClose,
/// Open Brace
BraceOpen,
/// Close Brace
BraceClose,
}
impl fmt::Display for Token<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::TemplateData(_) => f.write_str("template-data"),
Token::VariableStart => f.write_str("start of variable block"),
Token::VariableEnd => f.write_str("end of variable block"),
Token::BlockStart => f.write_str("start of block"),
Token::BlockEnd => f.write_str("end of block"),
Token::Ident(_) => f.write_str("identifier"),
Token::Str(_) | Token::String(_) => f.write_str("string"),
Token::Int(_) | Token::Int128(_) => f.write_str("integer"),
Token::Float(_) => f.write_str("float"),
Token::Plus => f.write_str("`+`"),
Token::Minus => f.write_str("`-`"),
Token::Mul => f.write_str("`*`"),
Token::Div => f.write_str("`/`"),
Token::FloorDiv => f.write_str("`//`"),
Token::Pow => f.write_str("`**`"),
Token::Mod => f.write_str("`%`"),
Token::Dot => f.write_str("`.`"),
Token::Comma => f.write_str("`,`"),
Token::Colon => f.write_str("`:`"),
Token::Tilde => f.write_str("`~`"),
Token::Assign => f.write_str("`=`"),
Token::Pipe => f.write_str("`|`"),
Token::Eq => f.write_str("`==`"),
Token::Ne => f.write_str("`!=`"),
Token::Gt => f.write_str("`>`"),
Token::Gte => f.write_str("`>=`"),
Token::Lt => f.write_str("`<`"),
Token::Lte => f.write_str("`<=`"),
Token::BracketOpen => f.write_str("`[`"),
Token::BracketClose => f.write_str("`]`"),
Token::ParenOpen => f.write_str("`(`"),
Token::ParenClose => f.write_str("`)`"),
Token::BraceOpen => f.write_str("`{`"),
Token::BraceClose => f.write_str("`}`"),
}
}
}
/// Token span information
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Span {
pub start_line: u16,
pub start_col: u16,
pub start_offset: u32,
pub end_line: u16,
pub end_col: u16,
pub end_offset: u32,
}
impl fmt::Debug for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("Span")
.field("start_line", &self.start_line)
.field("start_col", &self.start_col)
.field("start_offset", &self.start_offset)
.field("end_line", &self.end_line)
.field("end_col", &self.end_col)
.field("end_offset", &self.end_offset)
.finish()
} else {
write!(
f,
" @ {}:{}-{}:{}",
self.start_line, self.start_col, self.end_line, self.end_col
)
}
}
}