mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Capture variables in templates 🔍
This commit is contained in:
parent
2641c2d20e
commit
2036663ed2
@ -40,7 +40,7 @@ impl<'a> EvalContext<'a> {
|
||||
pub fn new(env: &'a mut Env, scope: &'a Scope, state: State) -> Self {
|
||||
Self {
|
||||
env,
|
||||
scopes: Scopes::new(scope),
|
||||
scopes: Scopes::new(Some(scope)),
|
||||
state,
|
||||
groups: vec![],
|
||||
inner: vec![],
|
||||
|
@ -7,6 +7,7 @@ mod context;
|
||||
mod ops;
|
||||
mod scope;
|
||||
mod state;
|
||||
mod template;
|
||||
|
||||
pub use call::*;
|
||||
pub use context::*;
|
||||
@ -174,7 +175,7 @@ impl Eval for Spanned<&Expr> {
|
||||
Expr::Str(v) => Value::Str(v.clone()),
|
||||
Expr::Array(v) => Value::Array(v.with_span(self.span).eval(ctx)),
|
||||
Expr::Dict(v) => Value::Dict(v.with_span(self.span).eval(ctx)),
|
||||
Expr::Template(v) => Value::Template(v.clone()),
|
||||
Expr::Template(v) => v.with_span(self.span).eval(ctx),
|
||||
Expr::Group(v) => v.eval(ctx),
|
||||
Expr::Block(v) => v.with_span(self.span).eval(ctx),
|
||||
Expr::Call(v) => v.with_span(self.span).eval(ctx),
|
||||
@ -183,6 +184,7 @@ impl Eval for Spanned<&Expr> {
|
||||
Expr::Let(v) => v.with_span(self.span).eval(ctx),
|
||||
Expr::If(v) => v.with_span(self.span).eval(ctx),
|
||||
Expr::For(v) => v.with_span(self.span).eval(ctx),
|
||||
Expr::CapturedValue(v) => v.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -327,18 +329,28 @@ impl Spanned<&ExprBinary> {
|
||||
let rhs = self.v.rhs.eval(ctx);
|
||||
let span = self.v.lhs.span;
|
||||
|
||||
if let Expr::Ident(id) = &self.v.lhs.v {
|
||||
if let Some(slot) = ctx.scopes.get_mut(id) {
|
||||
let lhs = std::mem::replace(slot, Value::None);
|
||||
*slot = op(lhs, rhs);
|
||||
return Value::None;
|
||||
} else if ctx.scopes.is_const(id) {
|
||||
ctx.diag(error!(span, "cannot assign to constant"));
|
||||
} else {
|
||||
ctx.diag(error!(span, "unknown variable"));
|
||||
match &self.v.lhs.v {
|
||||
Expr::Ident(id) => {
|
||||
if let Some(slot) = ctx.scopes.get_mut(id) {
|
||||
*slot = op(std::mem::take(slot), rhs);
|
||||
return Value::None;
|
||||
} else if ctx.scopes.is_const(id) {
|
||||
ctx.diag(error!(span, "cannot assign to a constant"));
|
||||
} else {
|
||||
ctx.diag(error!(span, "unknown variable"));
|
||||
}
|
||||
}
|
||||
|
||||
Expr::CapturedValue(_) => {
|
||||
ctx.diag(error!(
|
||||
span,
|
||||
"cannot assign to captured expression in a template",
|
||||
));
|
||||
}
|
||||
|
||||
_ => {
|
||||
ctx.diag(error!(span, "cannot assign to this expression"));
|
||||
}
|
||||
} else {
|
||||
ctx.diag(error!(span, "cannot assign to this expression"));
|
||||
}
|
||||
|
||||
Value::Error
|
||||
@ -421,7 +433,7 @@ impl Eval for Spanned<&ExprFor> {
|
||||
|
||||
(ForPattern::KeyValue(..), Value::Str(_))
|
||||
| (ForPattern::KeyValue(..), Value::Array(_)) => {
|
||||
ctx.diag(error!(self.v.pat.span, "mismatched pattern",));
|
||||
ctx.diag(error!(self.v.pat.span, "mismatched pattern"));
|
||||
}
|
||||
|
||||
(_, Value::Error) => {}
|
||||
|
@ -5,19 +5,19 @@ use std::iter;
|
||||
use super::Value;
|
||||
|
||||
/// A stack of scopes.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Scopes<'a> {
|
||||
/// The active scope.
|
||||
top: Scope,
|
||||
/// The stack of lower scopes.
|
||||
scopes: Vec<Scope>,
|
||||
/// The base scope.
|
||||
base: &'a Scope,
|
||||
base: Option<&'a Scope>,
|
||||
}
|
||||
|
||||
impl<'a> Scopes<'a> {
|
||||
/// Create a new hierarchy of scopes.
|
||||
pub fn new(base: &'a Scope) -> Self {
|
||||
pub fn new(base: Option<&'a Scope>) -> Self {
|
||||
Self { top: Scope::new(), scopes: vec![], base }
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ impl<'a> Scopes<'a> {
|
||||
pub fn get(&self, var: &str) -> Option<&Value> {
|
||||
iter::once(&self.top)
|
||||
.chain(self.scopes.iter().rev())
|
||||
.chain(iter::once(self.base))
|
||||
.chain(self.base.into_iter())
|
||||
.find_map(|scope| scope.get(var))
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ impl<'a> Scopes<'a> {
|
||||
///
|
||||
/// Defaults to `false` if the variable does not exist.
|
||||
pub fn is_const(&self, var: &str) -> bool {
|
||||
self.base.get(var).is_some()
|
||||
self.base.map_or(false, |base| base.get(var).is_some())
|
||||
}
|
||||
}
|
||||
|
||||
|
56
src/eval/template.rs
Normal file
56
src/eval/template.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use super::*;
|
||||
use crate::syntax::visit::*;
|
||||
|
||||
impl Eval for Spanned<&ExprTemplate> {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(self, ctx: &mut EvalContext) -> Self::Output {
|
||||
let mut template = self.v.clone();
|
||||
let mut visitor = CapturesVisitor::new(ctx);
|
||||
visitor.visit_template(&mut template);
|
||||
Value::Template(template)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visitor that replaces all captured variables with their values.
|
||||
struct CapturesVisitor<'a> {
|
||||
external: &'a Scopes<'a>,
|
||||
internal: Scopes<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CapturesVisitor<'a> {
|
||||
fn new(ctx: &'a EvalContext) -> Self {
|
||||
Self {
|
||||
external: &ctx.scopes,
|
||||
internal: Scopes::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for CapturesVisitor<'a> {
|
||||
fn visit_scope_pre(&mut self) {
|
||||
self.internal.push();
|
||||
}
|
||||
|
||||
fn visit_scope_post(&mut self) {
|
||||
self.internal.pop();
|
||||
}
|
||||
|
||||
fn visit_def(&mut self, id: &mut Ident) {
|
||||
self.internal.define(id.as_str(), Value::None);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'a mut Expr) {
|
||||
if let Expr::Ident(ident) = expr {
|
||||
// Find out whether the identifier is not locally defined, but
|
||||
// captured, and if so, replace it with it's value.
|
||||
if self.internal.get(ident).is_none() {
|
||||
if let Some(value) = self.external.get(ident) {
|
||||
*expr = Expr::CapturedValue(value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
@ -87,7 +87,14 @@ impl Eval for &Value {
|
||||
ctx.push(ctx.make_text_node(match self {
|
||||
Value::None => return,
|
||||
Value::Str(s) => s.clone(),
|
||||
Value::Template(tree) => return tree.eval(ctx),
|
||||
Value::Template(tree) => {
|
||||
// We do not want to allow the template access to the current
|
||||
// scopes.
|
||||
let prev = std::mem::take(&mut ctx.scopes);
|
||||
tree.eval(ctx);
|
||||
ctx.scopes = prev;
|
||||
return;
|
||||
}
|
||||
other => pretty(other),
|
||||
}));
|
||||
}
|
||||
@ -195,7 +202,7 @@ impl Deref for ValueFunc {
|
||||
|
||||
impl Pretty for ValueFunc {
|
||||
fn pretty(&self, p: &mut Printer) {
|
||||
write!(p, "(function {})", self.name).unwrap();
|
||||
p.push_str(&self.name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,7 +522,7 @@ mod tests {
|
||||
test_pretty(Color::Rgba(RgbaColor::new(1, 1, 1, 0xff)), "#010101");
|
||||
test_pretty("hello", r#""hello""#);
|
||||
test_pretty(vec![Spanned::zero(Node::Strong)], "[*]");
|
||||
test_pretty(ValueFunc::new("nil", |_, _| Value::None), "(function nil)");
|
||||
test_pretty(ValueFunc::new("nil", |_, _| Value::None), "nil");
|
||||
test_pretty(ValueAny::new(1), "1");
|
||||
test_pretty(Value::Error, "(error)");
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::color::RgbaColor;
|
||||
use crate::eval::Value;
|
||||
use crate::geom::{AngularUnit, LengthUnit};
|
||||
|
||||
/// An expression.
|
||||
@ -50,6 +51,11 @@ pub enum Expr {
|
||||
If(ExprIf),
|
||||
/// A for expression: `#for x #in y { z }`.
|
||||
For(ExprFor),
|
||||
/// A captured value.
|
||||
///
|
||||
/// This node is never created by parsing. It only results from an in-place
|
||||
/// transformation of an identifier to a captured value.
|
||||
CapturedValue(Value),
|
||||
}
|
||||
|
||||
impl Pretty for Expr {
|
||||
@ -86,6 +92,7 @@ impl Pretty for Expr {
|
||||
Self::Let(v) => v.pretty(p),
|
||||
Self::If(v) => v.pretty(p),
|
||||
Self::For(v) => v.pretty(p),
|
||||
Self::CapturedValue(v) => v.pretty(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ mod ident;
|
||||
mod node;
|
||||
mod span;
|
||||
mod token;
|
||||
pub mod visit;
|
||||
|
||||
pub use expr::*;
|
||||
pub use ident::*;
|
||||
|
185
src/syntax/visit.rs
Normal file
185
src/syntax/visit.rs
Normal file
@ -0,0 +1,185 @@
|
||||
//! Syntax tree traversal.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Visits syntax tree nodes in a depth-first manner.
|
||||
pub trait Visitor<'a>: Sized {
|
||||
/// Visit a variable definition.
|
||||
fn visit_def(&mut self, _ident: &'a mut Ident) {}
|
||||
|
||||
/// Visit the start of a scope.
|
||||
fn visit_scope_pre(&mut self) {}
|
||||
|
||||
/// Visit the end of a scope.
|
||||
fn visit_scope_post(&mut self) {}
|
||||
|
||||
fn visit_node(&mut self, node: &'a mut Node) {
|
||||
walk_node(self, node)
|
||||
}
|
||||
fn visit_expr(&mut self, expr: &'a mut Expr) {
|
||||
walk_expr(self, expr)
|
||||
}
|
||||
fn visit_array(&mut self, array: &'a mut ExprArray) {
|
||||
walk_array(self, array)
|
||||
}
|
||||
fn visit_dict(&mut self, dict: &'a mut ExprDict) {
|
||||
walk_dict(self, dict)
|
||||
}
|
||||
fn visit_template(&mut self, template: &'a mut ExprTemplate) {
|
||||
walk_template(self, template)
|
||||
}
|
||||
fn visit_group(&mut self, group: &'a mut ExprGroup) {
|
||||
walk_group(self, group)
|
||||
}
|
||||
fn visit_block(&mut self, block: &'a mut ExprBlock) {
|
||||
walk_block(self, block)
|
||||
}
|
||||
fn visit_binary(&mut self, binary: &'a mut ExprBinary) {
|
||||
walk_binary(self, binary)
|
||||
}
|
||||
fn visit_unary(&mut self, unary: &'a mut ExprUnary) {
|
||||
walk_unary(self, unary)
|
||||
}
|
||||
fn visit_call(&mut self, call: &'a mut ExprCall) {
|
||||
walk_call(self, call)
|
||||
}
|
||||
fn visit_arg(&mut self, arg: &'a mut Argument) {
|
||||
walk_arg(self, arg)
|
||||
}
|
||||
fn visit_let(&mut self, expr_let: &'a mut ExprLet) {
|
||||
walk_let(self, expr_let)
|
||||
}
|
||||
fn visit_if(&mut self, expr_if: &'a mut ExprIf) {
|
||||
walk_if(self, expr_if)
|
||||
}
|
||||
fn visit_for(&mut self, expr_for: &'a mut ExprFor) {
|
||||
walk_for(self, expr_for)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_node<'a, V: Visitor<'a>>(v: &mut V, node: &'a mut Node) {
|
||||
match node {
|
||||
Node::Strong => {}
|
||||
Node::Emph => {}
|
||||
Node::Space => {}
|
||||
Node::Linebreak => {}
|
||||
Node::Parbreak => {}
|
||||
Node::Text(_) => {}
|
||||
Node::Heading(_) => {}
|
||||
Node::Raw(_) => {}
|
||||
Node::Expr(expr) => v.visit_expr(expr),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_expr<'a, V: Visitor<'a>>(v: &mut V, expr: &'a mut Expr) {
|
||||
match expr {
|
||||
Expr::None => {}
|
||||
Expr::Ident(_) => {}
|
||||
Expr::Bool(_) => {}
|
||||
Expr::Int(_) => {}
|
||||
Expr::Float(_) => {}
|
||||
Expr::Length(_, _) => {}
|
||||
Expr::Angle(_, _) => {}
|
||||
Expr::Percent(_) => {}
|
||||
Expr::Color(_) => {}
|
||||
Expr::Str(_) => {}
|
||||
Expr::Array(e) => v.visit_array(e),
|
||||
Expr::Dict(e) => v.visit_dict(e),
|
||||
Expr::Template(e) => v.visit_template(e),
|
||||
Expr::Group(e) => v.visit_group(e),
|
||||
Expr::Block(e) => v.visit_block(e),
|
||||
Expr::Unary(e) => v.visit_unary(e),
|
||||
Expr::Binary(e) => v.visit_binary(e),
|
||||
Expr::Call(e) => v.visit_call(e),
|
||||
Expr::Let(e) => v.visit_let(e),
|
||||
Expr::If(e) => v.visit_if(e),
|
||||
Expr::For(e) => v.visit_for(e),
|
||||
Expr::CapturedValue(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_array<'a, V: Visitor<'a>>(v: &mut V, array: &'a mut ExprArray) {
|
||||
for expr in array {
|
||||
v.visit_expr(&mut expr.v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_dict<'a, V: Visitor<'a>>(v: &mut V, dict: &'a mut ExprDict) {
|
||||
for named in dict {
|
||||
v.visit_expr(&mut named.expr.v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_template<'a, V: Visitor<'a>>(v: &mut V, template: &'a mut ExprTemplate) {
|
||||
v.visit_scope_pre();
|
||||
for node in template {
|
||||
v.visit_node(&mut node.v);
|
||||
}
|
||||
v.visit_scope_post();
|
||||
}
|
||||
|
||||
pub fn walk_group<'a, V: Visitor<'a>>(v: &mut V, group: &'a mut ExprGroup) {
|
||||
v.visit_expr(&mut group.v);
|
||||
}
|
||||
|
||||
pub fn walk_block<'a, V: Visitor<'a>>(v: &mut V, block: &'a mut ExprBlock) {
|
||||
if block.scopes {
|
||||
v.visit_scope_pre();
|
||||
}
|
||||
for expr in &mut block.exprs {
|
||||
v.visit_expr(&mut expr.v);
|
||||
}
|
||||
if block.scopes {
|
||||
v.visit_scope_post();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_binary<'a, V: Visitor<'a>>(v: &mut V, binary: &'a mut ExprBinary) {
|
||||
v.visit_expr(&mut binary.lhs.v);
|
||||
v.visit_expr(&mut binary.rhs.v);
|
||||
}
|
||||
|
||||
pub fn walk_unary<'a, V: Visitor<'a>>(v: &mut V, unary: &'a mut ExprUnary) {
|
||||
v.visit_expr(&mut unary.expr.v);
|
||||
}
|
||||
|
||||
pub fn walk_call<'a, V: Visitor<'a>>(v: &mut V, call: &'a mut ExprCall) {
|
||||
v.visit_expr(&mut call.callee.v);
|
||||
for arg in &mut call.args.v {
|
||||
v.visit_arg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_arg<'a, V: Visitor<'a>>(v: &mut V, arg: &'a mut Argument) {
|
||||
match arg {
|
||||
Argument::Pos(expr) => v.visit_expr(&mut expr.v),
|
||||
Argument::Named(named) => v.visit_expr(&mut named.expr.v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_let<'a, V: Visitor<'a>>(v: &mut V, expr_let: &'a mut ExprLet) {
|
||||
v.visit_def(&mut expr_let.pat.v);
|
||||
if let Some(init) = &mut expr_let.init {
|
||||
v.visit_expr(&mut init.v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_if<'a, V: Visitor<'a>>(v: &mut V, expr_if: &'a mut ExprIf) {
|
||||
v.visit_expr(&mut expr_if.condition.v);
|
||||
v.visit_expr(&mut expr_if.if_body.v);
|
||||
if let Some(body) = &mut expr_if.else_body {
|
||||
v.visit_expr(&mut body.v);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_for<'a, V: Visitor<'a>>(v: &mut V, expr_for: &'a mut ExprFor) {
|
||||
match &mut expr_for.pat.v {
|
||||
ForPattern::Value(value) => v.visit_def(value),
|
||||
ForPattern::KeyValue(key, value) => {
|
||||
v.visit_def(key);
|
||||
v.visit_def(value);
|
||||
}
|
||||
}
|
||||
v.visit_expr(&mut expr_for.iter.v);
|
||||
v.visit_expr(&mut expr_for.body.v);
|
||||
}
|
@ -86,7 +86,7 @@
|
||||
// Error: 1:3-1:8 cannot assign to this expression
|
||||
{ 1 + 2 = 3}
|
||||
|
||||
// Error: 1:3-1:6 cannot assign to constant
|
||||
// Error: 1:3-1:6 cannot assign to a constant
|
||||
{ box = "hi" }
|
||||
|
||||
// Works if we define box before (since then it doesn't resolve to the standard
|
||||
|
Loading…
x
Reference in New Issue
Block a user