From 242b01549a472d4eeca1404b8f63427e23224253 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 16 May 2022 19:13:39 +0200 Subject: [PATCH] Safe `eval` function --- src/lib.rs | 28 +++++++++++--------- src/library/mod.rs | 1 + src/library/utility/mod.rs | 31 ++++++++++++++++++++++ src/model/property.rs | 10 +++---- src/model/styles.rs | 3 ++- src/source.rs | 14 ++++++++-- src/syntax/mod.rs | 53 ++++++++++++++++++++++++++++++++----- tests/ref/utility/eval.png | Bin 0 -> 5429 bytes tests/typ/utility/eval.typ | 52 ++++++++++++++++++++++++++++++++++++ 9 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 tests/ref/utility/eval.png create mode 100644 tests/typ/utility/eval.typ diff --git a/src/lib.rs b/src/lib.rs index eb6e8f72..06324c11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,7 @@ use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; use std::hash::Hash; +use std::mem; use std::path::PathBuf; use std::sync::Arc; @@ -141,25 +142,26 @@ impl Context { let source = self.sources.get(id); let ast = source.ast()?; - let std = self.std.clone(); - let mut scp = Scopes::new(Some(&std)); + // Save the old context. + let prev_flow = self.flow.take(); + let prev_deps = mem::replace(&mut self.deps, vec![(id, source.rev())]); + self.route.push(id); // Evaluate the module. - let prev = std::mem::replace(&mut self.deps, vec![(id, source.rev())]); - self.route.push(id); - let content = ast.eval(self, &mut scp); + let std = self.std.clone(); + let mut scp = Scopes::new(Some(&std)); + let result = ast.eval(self, &mut scp); + + // Restore the old context and handle control flow. self.route.pop().unwrap(); - let deps = std::mem::replace(&mut self.deps, prev); - let flow = self.flow.take(); - - // Assemble the module. - let module = Module { scope: scp.top, content: content?, deps }; - - // Handle unhandled flow. - if let Some(flow) = flow { + let deps = mem::replace(&mut self.deps, prev_deps); + if let Some(flow) = mem::replace(&mut self.flow, prev_flow) { return Err(flow.forbidden()); } + // Assemble the module. + let module = Module { scope: scp.top, content: result?, deps }; + // Save the evaluated module. self.modules.insert(id, module.clone()); diff --git a/src/library/mod.rs b/src/library/mod.rs index 6a30badf..ac0cbb92 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -73,6 +73,7 @@ pub fn new() -> Scope { // Utility. std.def_fn("type", utility::type_); std.def_fn("assert", utility::assert); + std.def_fn("eval", utility::eval); std.def_fn("int", utility::int); std.def_fn("float", utility::float); std.def_fn("abs", utility::abs); diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 13220242..355315e4 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -8,7 +8,11 @@ pub use color::*; pub use math::*; pub use string::*; +use std::mem; + +use crate::eval::{Eval, Scopes}; use crate::library::prelude::*; +use crate::source::SourceFile; /// The name of a value's type. pub fn type_(_: &mut Context, args: &mut Args) -> TypResult { @@ -23,3 +27,30 @@ pub fn assert(_: &mut Context, args: &mut Args) -> TypResult { } Ok(Value::None) } + +/// Evaluate a string as Typst markup. +pub fn eval(ctx: &mut Context, args: &mut Args) -> TypResult { + let Spanned { v: src, span } = args.expect::>("source")?; + + // Parse the source and set a synthetic span for all nodes. + let mut source = SourceFile::detached(src); + source.synthesize(span); + let ast = source.ast()?; + + // Save the old context, then detach it. + let prev_flow = ctx.flow.take(); + let prev_route = mem::take(&mut ctx.route); + + // Evaluate the source. + let std = ctx.std.clone(); + let mut scp = Scopes::new(Some(&std)); + let result = ast.eval(ctx, &mut scp); + + // Restore the old context and handle control flow. + ctx.route = prev_route; + if let Some(flow) = mem::replace(&mut ctx.flow, prev_flow) { + return Err(flow.forbidden()); + } + + Ok(Value::Content(result?)) +} diff --git a/src/model/property.rs b/src/model/property.rs index b35ffba9..ff686970 100644 --- a/src/model/property.rs +++ b/src/model/property.rs @@ -131,13 +131,13 @@ impl Debug for KeyId { pub trait Key<'a>: Copy + 'static { /// The unfolded type which this property is stored as in a style map. For /// example, this is [`Toggle`](crate::geom::Length) for the - /// [`STRONG`](TextNode::STRONG) property. + /// [`STRONG`](crate::library::text::TextNode::STRONG) property. type Value: Debug + Clone + Hash + Sync + Send + 'static; /// The folded type of value that is returned when reading this property /// from a style chain. For example, this is [`bool`] for the - /// [`STRONG`](TextNode::STRONG) property. For non-copy, non-folding - /// properties this is a reference type. + /// [`STRONG`](crate::library::text::TextNode::STRONG) property. For + /// non-copy, non-folding properties this is a reference type. type Output; /// The name of the property, used for debug printing. @@ -274,8 +274,8 @@ impl Fold for Sides>>> { /// A scoped property barrier. /// -/// Barriers interact with [scoped](StyleMap::scoped) styles: A scoped style -/// can still be read through a single barrier (the one of the node it +/// Barriers interact with [scoped](super::StyleMap::scoped) styles: A scoped +/// style can still be read through a single barrier (the one of the node it /// _should_ apply to), but a second barrier will make it invisible. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Barrier(NodeId); diff --git a/src/model/styles.rs b/src/model/styles.rs index 928133c7..5c36861a 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -103,7 +103,8 @@ impl StyleMap { /// Mark all contained properties as _scoped_. This means that they only /// apply to the first descendant node (of their type) in the hierarchy and - /// not its children, too. This is used by [constructors](Node::construct). + /// not its children, too. This is used by + /// [constructors](crate::eval::Node::construct). pub fn scoped(mut self) -> Self { for entry in &mut self.0 { if let StyleEntry::Property(property) = entry { diff --git a/src/source.rs b/src/source.rs index 780e12a8..deeaef4b 100644 --- a/src/source.rs +++ b/src/source.rs @@ -12,7 +12,7 @@ use crate::diag::TypResult; use crate::loading::{FileHash, Loader}; use crate::parse::{is_newline, parse, reparse}; use crate::syntax::ast::Markup; -use crate::syntax::{self, Category, GreenNode, RedNode}; +use crate::syntax::{self, Category, GreenNode, RedNode, Span}; use crate::util::{PathExt, StrExt}; #[cfg(feature = "codespan-reporting")] @@ -23,6 +23,11 @@ use codespan_reporting::files::{self, Files}; pub struct SourceId(u32); impl SourceId { + /// Create a new source id for a file that is not part of a store. + pub const fn detached() -> Self { + Self(u32::MAX) + } + /// Create a source id from the raw underlying value. /// /// This should only be called with values returned by @@ -165,7 +170,12 @@ impl SourceFile { /// Create a source file without a real id and path, usually for testing. pub fn detached(src: impl Into) -> Self { - Self::new(SourceId(0), Path::new(""), src.into()) + Self::new(SourceId::detached(), Path::new(""), src.into()) + } + + /// Set a synthetic span for all nodes in this file. + pub fn synthesize(&mut self, span: Span) { + Arc::make_mut(&mut self.root).synthesize(Arc::new(span)); } /// The root node of the file's untyped green tree. diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs index abe541b8..29e2718b 100644 --- a/src/syntax/mod.rs +++ b/src/syntax/mod.rs @@ -81,6 +81,14 @@ impl Green { Self::Token(data) => data.kind = kind, } } + + /// Set a synthetic span for the node and all its children. + pub fn synthesize(&mut self, span: Arc) { + match self { + Green::Node(n) => Arc::make_mut(n).synthesize(span), + Green::Token(t) => t.synthesize(span), + } + } } impl Default for Green { @@ -151,6 +159,14 @@ impl GreenNode { self.data().len() } + /// Set a synthetic span for the node and all its children. + pub fn synthesize(&mut self, span: Arc) { + self.data.synthesize(span.clone()); + for child in &mut self.children { + child.synthesize(span.clone()); + } + } + /// The node's children, mutably. pub(crate) fn children_mut(&mut self) -> &mut [Green] { &mut self.children @@ -214,12 +230,14 @@ pub struct GreenData { kind: NodeKind, /// The byte length of the node in the source. len: usize, + /// A synthetic span for the node, usually this is `None`. + span: Option>, } impl GreenData { /// Create new node metadata. pub fn new(kind: NodeKind, len: usize) -> Self { - Self { len, kind } + Self { len, kind, span: None } } /// The type of the node. @@ -231,6 +249,11 @@ impl GreenData { pub fn len(&self) -> usize { self.len } + + /// Set a synthetic span for the node. + pub fn synthesize(&mut self, span: Arc) { + self.span = Some(span) + } } impl From for Green { @@ -270,6 +293,11 @@ impl RedNode { } } + /// The node's metadata. + pub fn data(&self) -> &GreenData { + self.as_ref().data() + } + /// The type of the node. pub fn kind(&self) -> &NodeKind { self.as_ref().kind() @@ -340,6 +368,11 @@ impl<'a> RedRef<'a> { } } + /// The node's metadata. + pub fn data(self) -> &'a GreenData { + self.green.data() + } + /// The type of the node. pub fn kind(self) -> &'a NodeKind { self.green.kind() @@ -352,7 +385,10 @@ impl<'a> RedRef<'a> { /// The span of the node. pub fn span(self) -> Span { - Span::new(self.id, self.offset, self.offset + self.green.len()) + match self.data().span.as_deref() { + Some(&span) => span, + None => Span::new(self.id, self.offset, self.offset + self.len()), + } } /// Whether the node is a leaf node. @@ -368,11 +404,14 @@ impl<'a> RedRef<'a> { match self.kind() { NodeKind::Error(pos, msg) => { - let span = match pos { - ErrorPos::Start => self.span().at_start(), - ErrorPos::Full => self.span(), - ErrorPos::End => self.span().at_end(), - }; + let mut span = self.span(); + if self.data().span.is_none() { + span = match pos { + ErrorPos::Start => span.at_start(), + ErrorPos::Full => span, + ErrorPos::End => span.at_end(), + }; + } vec![Error::new(span, msg.to_string())] } diff --git a/tests/ref/utility/eval.png b/tests/ref/utility/eval.png new file mode 100644 index 0000000000000000000000000000000000000000..38c1d64ebdf1413e57eccfb0558f8f51d1591002 GIT binary patch literal 5429 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 literal 0 HcmV?d00001 diff --git a/tests/typ/utility/eval.typ b/tests/typ/utility/eval.typ new file mode 100644 index 00000000..86b1f0c4 --- /dev/null +++ b/tests/typ/utility/eval.typ @@ -0,0 +1,52 @@ +// Test the `eval` function. + +--- +#eval("_Hello" + " World!_") + +--- +// Error: 7-13 expected identifier +#eval("#let") + +--- +#set raw(around: none) +#show it: raw as text("IBM Plex Sans", eval(it.text)) + +Interacting +``` +#set text(blue) +Blue #move(dy: -0.15em)[🌊] +``` + +--- +// Error: 7-19 cannot continue outside of loop +#eval("{continue}") + +--- +// Error: 7-33 cannot access file system from here +#eval("#include \"../coma.typ\"") + +--- +// Error: 7-35 cannot access file system from here +#eval("#image(\"/res/tiger.jpg\")") + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show strong as image("/res/tiger.jpg") +*No absolute tiger!* +``` + +--- +// Error: 23-30 cannot access file system from here +#show it: raw as eval(it.text) + +``` +#show emph as image("../../res/giraffe.jpg") +_No relative giraffe!_ +``` + +--- +// Error: 7-16 expected comma +#eval("{(1 2)}")