diff options
Diffstat (limited to 'crates/typst-library/src/foundations/scope.rs')
| -rw-r--r-- | crates/typst-library/src/foundations/scope.rs | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs new file mode 100644 index 00000000..b51f8caa --- /dev/null +++ b/crates/typst-library/src/foundations/scope.rs @@ -0,0 +1,416 @@ +#[doc(inline)] +pub use typst_macros::category; + +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; + +use ecow::{eco_format, EcoString}; +use indexmap::IndexMap; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::Span; +use typst_utils::Static; + +use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::foundations::{ + Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, + NativeType, Type, Value, +}; +use crate::Library; + +/// A stack of scopes. +#[derive(Debug, Default, Clone)] +pub struct Scopes<'a> { + /// The active scope. + pub top: Scope, + /// The stack of lower scopes. + pub scopes: Vec<Scope>, + /// The standard library. + pub base: Option<&'a Library>, +} + +impl<'a> Scopes<'a> { + /// Create a new, empty hierarchy of scopes. + pub fn new(base: Option<&'a Library>) -> Self { + Self { top: Scope::new(), scopes: vec![], base } + } + + /// Enter a new scope. + pub fn enter(&mut self) { + self.scopes.push(std::mem::take(&mut self.top)); + } + + /// Exit the topmost scope. + /// + /// This panics if no scope was entered. + pub fn exit(&mut self) { + self.top = self.scopes.pop().expect("no pushed scope"); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.global.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| unknown_variable(var)) + } + + /// Try to access a variable immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .find_map(|scope| scope.get(var)) + .or_else(|| { + self.base.and_then(|base| match base.math.scope().get(var) { + Some(value) => Some(value), + None if var == "std" => Some(&base.std), + None => None, + }) + }) + .ok_or_else(|| { + unknown_variable_math( + var, + self.base.is_some_and(|base| base.global.scope().get(var).is_some()), + ) + }) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + })? + } + + /// Check if an std variable is shadowed. + pub fn check_std_shadowed(&self, var: &str) -> bool { + self.base.is_some_and(|base| base.global.scope().get(var).is_some()) + && std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .any(|scope| scope.get(var).is_some()) + } +} + +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() +} + +/// The error message when a variable is not found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); + } + + res +} + +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", + var.chars() + .flat_map(|c| [' ', c]) + .skip(1) + .collect::<EcoString>() + )); + res.hint(eco_format!( + "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" + )); + } + + res +} + +/// A map from binding names to values. +#[derive(Default, Clone)] +pub struct Scope { + map: IndexMap<EcoString, Slot>, + deduplicate: bool, + category: Option<Category>, +} + +impl Scope { + /// Create a new empty scope. + pub fn new() -> Self { + Default::default() + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self { deduplicate: true, ..Default::default() } + } + + /// Enter a new category. + pub fn category(&mut self, category: Category) { + self.category = Some(category); + } + + /// Reset the category. + pub fn reset_category(&mut self) { + self.category = None; + } + + /// Bind a value to a name. + #[track_caller] + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { + self.define_spanned(name, value, Span::detached()) + } + + /// Bind a value to a name defined by an identifier. + #[track_caller] + pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { + self.define_spanned(ident.get().clone(), value, ident.span()) + } + + /// Bind a value to a name. + #[track_caller] + pub fn define_spanned( + &mut self, + name: impl Into<EcoString>, + value: impl IntoValue, + span: Span, + ) { + let name = name.into(); + + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Normal, self.category), + ); + } + + /// Define a captured, immutable binding. + pub fn define_captured( + &mut self, + name: EcoString, + value: Value, + capturer: Capturer, + span: Span, + ) { + self.map.insert( + name, + Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), + ); + } + + /// Define a native function through a Rust type that shadows the function. + pub fn define_func<T: NativeFunc>(&mut self) { + let data = T::data(); + self.define(data.name, Func::from(data)); + } + + /// Define a native function with raw function data. + pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { + self.define(data.name, Func::from(data)); + } + + /// Define a native type. + pub fn define_type<T: NativeType>(&mut self) { + let data = T::data(); + self.define(data.name, Type::from(data)); + } + + /// Define a native element. + pub fn define_elem<T: NativeElement>(&mut self) { + let data = T::data(); + self.define(data.name, Element::from(data)); + } + + /// Define a module. + pub fn define_module(&mut self, module: Module) { + self.define(module.name().clone(), module); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.map.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option<HintedStrResult<&mut Value>> { + self.map + .get_mut(var) + .map(Slot::write) + .map(|res| res.map_err(HintedString::from)) + } + + /// Get the span of a definition. + pub fn get_span(&self, var: &str) -> Option<Span> { + Some(self.map.get(var)?.span) + } + + /// Get the category of a definition. + pub fn get_category(&self, var: &str) -> Option<Category> { + self.map.get(var)?.category + } + + /// Iterate over all definitions. + pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> { + self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + } +} + +impl Debug for Scope { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Scope ")?; + f.debug_map() + .entries(self.map.iter().map(|(k, v)| (k, v.read()))) + .finish() + } +} + +impl Hash for Scope { + fn hash<H: Hasher>(&self, state: &mut H) { + state.write_usize(self.map.len()); + for item in &self.map { + item.hash(state); + } + self.deduplicate.hash(state); + self.category.hash(state); + } +} + +/// Defines the associated scope of a Rust type. +pub trait NativeScope { + /// The constructor function for the type, if any. + fn constructor() -> Option<&'static NativeFuncData>; + + /// Get the associated scope for the type. + fn scope() -> Scope; +} + +/// A slot where a value is stored. +#[derive(Clone, Hash)] +struct Slot { + /// The stored value. + value: Value, + /// The kind of slot, determines how the value can be accessed. + kind: Kind, + /// A span associated with the stored value. + span: Span, + /// The category of the slot. + category: Option<Category>, +} + +/// The different kinds of slots. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Kind { + /// A normal, mutable binding. + Normal, + /// A captured copy of another variable. + Captured(Capturer), +} + +/// What the variable was captured by. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Capturer { + /// Captured by a function / closure. + Function, + /// Captured by a context expression. + Context, +} + +impl Slot { + /// Create a new slot. + fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self { + Self { value, span, kind, category } + } + + /// Read the value. + fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + Kind::Normal => Ok(&mut self.value), + Kind::Captured(capturer) => { + bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ) + } + } + } +} + +/// A group of related definitions. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Category(Static<CategoryData>); + +impl Category { + /// Create a new category from raw data. + pub const fn from_data(data: &'static CategoryData) -> Self { + Self(Static(data)) + } + + /// The category's name. + pub fn name(&self) -> &'static str { + self.0.name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the category. + pub fn docs(&self) -> &'static str { + self.0.docs + } +} + +impl Debug for Category { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Category({})", self.name()) + } +} + +/// Defines a category. +#[derive(Debug)] +pub struct CategoryData { + pub name: &'static str, + pub title: &'static str, + pub docs: &'static str, +} |
