typst_library/foundations/
scope.rs1use std::fmt::{self, Debug, Formatter};
2use std::hash::{Hash, Hasher};
3
4use ecow::{eco_format, EcoString};
5use indexmap::map::Entry;
6use indexmap::IndexMap;
7use typst_syntax::Span;
8
9use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult};
10use crate::foundations::{
11 Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
12 Type, Value,
13};
14use crate::{Category, Library};
15
16#[derive(Debug, Default, Clone)]
18pub struct Scopes<'a> {
19 pub top: Scope,
21 pub scopes: Vec<Scope>,
23 pub base: Option<&'a Library>,
25}
26
27impl<'a> Scopes<'a> {
28 pub fn new(base: Option<&'a Library>) -> Self {
30 Self { top: Scope::new(), scopes: vec![], base }
31 }
32
33 pub fn enter(&mut self) {
35 self.scopes.push(std::mem::take(&mut self.top));
36 }
37
38 pub fn exit(&mut self) {
42 self.top = self.scopes.pop().expect("no pushed scope");
43 }
44
45 pub fn get(&self, var: &str) -> HintedStrResult<&Binding> {
47 std::iter::once(&self.top)
48 .chain(self.scopes.iter().rev())
49 .find_map(|scope| scope.get(var))
50 .or_else(|| {
51 self.base.and_then(|base| match base.global.scope().get(var) {
52 Some(binding) => Some(binding),
53 None if var == "std" => Some(&base.std),
54 None => None,
55 })
56 })
57 .ok_or_else(|| unknown_variable(var))
58 }
59
60 pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> {
62 std::iter::once(&mut self.top)
63 .chain(&mut self.scopes.iter_mut().rev())
64 .find_map(|scope| scope.get_mut(var))
65 .ok_or_else(|| {
66 match self.base.and_then(|base| base.global.scope().get(var)) {
67 Some(_) => cannot_mutate_constant(var),
68 _ if var == "std" => cannot_mutate_constant(var),
69 _ => unknown_variable(var),
70 }
71 })
72 }
73
74 pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> {
76 std::iter::once(&self.top)
77 .chain(self.scopes.iter().rev())
78 .find_map(|scope| scope.get(var))
79 .or_else(|| {
80 self.base.and_then(|base| match base.math.scope().get(var) {
81 Some(binding) => Some(binding),
82 None if var == "std" => Some(&base.std),
83 None => None,
84 })
85 })
86 .ok_or_else(|| {
87 unknown_variable_math(
88 var,
89 self.base.is_some_and(|base| base.global.scope().get(var).is_some()),
90 )
91 })
92 }
93
94 pub fn check_std_shadowed(&self, var: &str) -> bool {
96 self.base.is_some_and(|base| base.global.scope().get(var).is_some())
97 && std::iter::once(&self.top)
98 .chain(self.scopes.iter().rev())
99 .any(|scope| scope.get(var).is_some())
100 }
101}
102
103#[derive(Default, Clone)]
105pub struct Scope {
106 map: IndexMap<EcoString, Binding>,
107 deduplicate: bool,
108 category: Option<Category>,
109}
110
111impl Scope {
113 pub fn new() -> Self {
115 Default::default()
116 }
117
118 pub fn deduplicating() -> Self {
120 Self { deduplicate: true, ..Default::default() }
121 }
122
123 pub fn start_category(&mut self, category: Category) {
125 self.category = Some(category);
126 }
127
128 pub fn reset_category(&mut self) {
130 self.category = None;
131 }
132
133 #[track_caller]
135 pub fn define_func<T: NativeFunc>(&mut self) -> &mut Binding {
136 let data = T::data();
137 self.define(data.name, Func::from(data))
138 }
139
140 #[track_caller]
142 pub fn define_func_with_data(
143 &mut self,
144 data: &'static NativeFuncData,
145 ) -> &mut Binding {
146 self.define(data.name, Func::from(data))
147 }
148
149 #[track_caller]
151 pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
152 let data = T::data();
153 self.define(data.name, Type::from(data))
154 }
155
156 #[track_caller]
158 pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
159 let data = T::data();
160 self.define(data.name, Element::from(data))
161 }
162
163 #[track_caller]
174 pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding {
175 #[cfg(debug_assertions)]
176 if self.deduplicate && self.map.contains_key(name) {
177 panic!("duplicate definition: {name}");
178 }
179
180 let mut binding = Binding::detached(value);
181 binding.category = self.category;
182 self.bind(name.into(), binding)
183 }
184}
185
186impl Scope {
188 pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding {
192 match self.map.entry(name) {
193 Entry::Occupied(mut entry) => {
194 entry.insert(binding);
195 entry.into_mut()
196 }
197 Entry::Vacant(entry) => entry.insert(binding),
198 }
199 }
200
201 pub fn get(&self, var: &str) -> Option<&Binding> {
203 self.map.get(var)
204 }
205
206 pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> {
208 self.map.get_mut(var)
209 }
210
211 pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Binding)> {
213 self.map.iter()
214 }
215}
216
217impl Debug for Scope {
218 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
219 f.write_str("Scope ")?;
220 f.debug_map()
221 .entries(self.map.iter().map(|(k, v)| (k, v.read())))
222 .finish()
223 }
224}
225
226impl Hash for Scope {
227 fn hash<H: Hasher>(&self, state: &mut H) {
228 state.write_usize(self.map.len());
229 for item in &self.map {
230 item.hash(state);
231 }
232 self.deduplicate.hash(state);
233 self.category.hash(state);
234 }
235}
236
237pub trait NativeScope {
239 fn constructor() -> Option<&'static NativeFuncData>;
241
242 fn scope() -> Scope;
244}
245
246#[derive(Debug, Clone, Hash)]
248pub struct Binding {
249 value: Value,
251 kind: BindingKind,
253 span: Span,
255 category: Option<Category>,
257 deprecation: Option<&'static str>,
259}
260
261#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
263enum BindingKind {
264 Normal,
266 Captured(Capturer),
268}
269
270impl Binding {
271 pub fn new(value: impl IntoValue, span: Span) -> Self {
273 Self {
274 value: value.into_value(),
275 span,
276 kind: BindingKind::Normal,
277 category: None,
278 deprecation: None,
279 }
280 }
281
282 pub fn detached(value: impl IntoValue) -> Self {
284 Self::new(value, Span::detached())
285 }
286
287 pub fn deprecated(&mut self, message: &'static str) -> &mut Self {
289 self.deprecation = Some(message);
290 self
291 }
292
293 pub fn read(&self) -> &Value {
295 &self.value
296 }
297
298 pub fn read_checked(&self, mut sink: impl DeprecationSink) -> &Value {
304 if let Some(message) = self.deprecation {
305 sink.emit(message);
306 }
307 &self.value
308 }
309
310 pub fn write(&mut self) -> StrResult<&mut Value> {
314 match self.kind {
315 BindingKind::Normal => Ok(&mut self.value),
316 BindingKind::Captured(capturer) => bail!(
317 "variables from outside the {} are \
318 read-only and cannot be modified",
319 match capturer {
320 Capturer::Function => "function",
321 Capturer::Context => "context expression",
322 }
323 ),
324 }
325 }
326
327 pub fn capture(&self, capturer: Capturer) -> Self {
329 Self {
330 kind: BindingKind::Captured(capturer),
331 ..self.clone()
332 }
333 }
334
335 pub fn span(&self) -> Span {
337 self.span
338 }
339
340 pub fn deprecation(&self) -> Option<&'static str> {
342 self.deprecation
343 }
344
345 pub fn category(&self) -> Option<Category> {
347 self.category
348 }
349}
350
351#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
353pub enum Capturer {
354 Function,
356 Context,
358}
359
360#[cold]
363fn cannot_mutate_constant(var: &str) -> HintedString {
364 eco_format!("cannot mutate a constant: {}", var).into()
365}
366
367#[cold]
369fn unknown_variable(var: &str) -> HintedString {
370 let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
371
372 if var.contains('-') {
373 res.hint(eco_format!(
374 "if you meant to use subtraction, \
375 try adding spaces around the minus sign{}: `{}`",
376 if var.matches('-').count() > 1 { "s" } else { "" },
377 var.replace('-', " - ")
378 ));
379 }
380
381 res
382}
383
384#[cold]
386fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
387 let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
388
389 if matches!(var, "none" | "auto" | "false" | "true") {
390 res.hint(eco_format!(
391 "if you meant to use a literal, \
392 try adding a hash before it: `#{var}`",
393 ));
394 } else if in_global {
395 res.hint(eco_format!(
396 "`{var}` is not available directly in math, \
397 try adding a hash before it: `#{var}`",
398 ));
399 } else {
400 res.hint(eco_format!(
401 "if you meant to display multiple letters as is, \
402 try adding spaces between each letter: `{}`",
403 var.chars().flat_map(|c| [' ', c]).skip(1).collect::<EcoString>()
404 ));
405 res.hint(eco_format!(
406 "or if you meant to display this as text, \
407 try placing it in quotes: `\"{var}\"`"
408 ));
409 }
410
411 res
412}