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:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+631
@@ -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
@@ -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 ¯o_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
@@ -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);
|
||||
}
|
||||
+1005
File diff suppressed because it is too large.
Load diff
+307
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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;
|
||||
+1365
File diff suppressed because it is too large.
Load diff
+157
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user