typst_eval/
import.rs

1use comemo::TrackedMut;
2use ecow::{eco_format, eco_vec, EcoString};
3use typst_library::diag::{
4    bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
5};
6use typst_library::engine::Engine;
7use typst_library::foundations::{Binding, Content, Module, Value};
8use typst_library::World;
9use typst_syntax::ast::{self, AstNode, BareImportError};
10use typst_syntax::package::{PackageManifest, PackageSpec};
11use typst_syntax::{FileId, Span, VirtualPath};
12
13use crate::{eval, Eval, Vm};
14
15impl Eval for ast::ModuleImport<'_> {
16    type Output = Value;
17
18    fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
19        let source_expr = self.source();
20        let source_span = source_expr.span();
21
22        let mut source = source_expr.eval(vm)?;
23        let mut is_str = false;
24
25        match &source {
26            Value::Func(func) => {
27                if func.scope().is_none() {
28                    bail!(source_span, "cannot import from user-defined functions");
29                }
30            }
31            Value::Type(_) => {}
32            Value::Module(_) => {}
33            Value::Str(path) => {
34                source = Value::Module(import(&mut vm.engine, path, source_span)?);
35                is_str = true;
36            }
37            v => {
38                bail!(
39                    source_span,
40                    "expected path, module, function, or type, found {}",
41                    v.ty()
42                )
43            }
44        }
45
46        // If there is a rename, import the source itself under that name.
47        let new_name = self.new_name();
48        if let Some(new_name) = new_name {
49            if let ast::Expr::Ident(ident) = self.source() {
50                if ident.as_str() == new_name.as_str() {
51                    // Warn on `import x as x`
52                    vm.engine.sink.warn(warning!(
53                        new_name.span(),
54                        "unnecessary import rename to same name",
55                    ));
56                }
57            }
58
59            // Define renamed module on the scope.
60            vm.define(new_name, source.clone());
61        }
62
63        let scope = source.scope().unwrap();
64        match self.imports() {
65            None => {
66                if new_name.is_none() {
67                    match self.bare_name() {
68                        // Bare dynamic string imports are not allowed.
69                        Ok(name)
70                            if !is_str || matches!(source_expr, ast::Expr::Str(_)) =>
71                        {
72                            if matches!(source_expr, ast::Expr::Ident(_)) {
73                                vm.engine.sink.warn(warning!(
74                                    source_expr.span(),
75                                    "this import has no effect",
76                                ));
77                            }
78                            vm.scopes.top.bind(name, Binding::new(source, source_span));
79                        }
80                        Ok(_) | Err(BareImportError::Dynamic) => bail!(
81                            source_span, "dynamic import requires an explicit name";
82                            hint: "you can name the import with `as`"
83                        ),
84                        Err(BareImportError::PathInvalid) => bail!(
85                            source_span, "module name would not be a valid identifier";
86                            hint: "you can rename the import with `as`",
87                        ),
88                        // Bad package spec would have failed the import already.
89                        Err(BareImportError::PackageInvalid) => unreachable!(),
90                    }
91                }
92            }
93            Some(ast::Imports::Wildcard) => {
94                for (var, binding) in scope.iter() {
95                    vm.scopes.top.bind(var.clone(), binding.clone());
96                }
97            }
98            Some(ast::Imports::Items(items)) => {
99                let mut errors = eco_vec![];
100                for item in items.iter() {
101                    let mut path = item.path().iter().peekable();
102                    let mut scope = scope;
103
104                    while let Some(component) = &path.next() {
105                        let Some(binding) = scope.get(component) else {
106                            errors.push(error!(component.span(), "unresolved import"));
107                            break;
108                        };
109
110                        if path.peek().is_some() {
111                            // Nested import, as this is not the last component.
112                            // This must be a submodule.
113                            let value = binding.read();
114                            let Some(submodule) = value.scope() else {
115                                let error = if matches!(value, Value::Func(function) if function.scope().is_none())
116                                {
117                                    error!(
118                                        component.span(),
119                                        "cannot import from user-defined functions"
120                                    )
121                                } else if !matches!(
122                                    value,
123                                    Value::Func(_) | Value::Module(_) | Value::Type(_)
124                                ) {
125                                    error!(
126                                        component.span(),
127                                        "expected module, function, or type, found {}",
128                                        value.ty()
129                                    )
130                                } else {
131                                    panic!("unexpected nested import failure")
132                                };
133                                errors.push(error);
134                                break;
135                            };
136
137                            // Walk into the submodule.
138                            scope = submodule;
139                        } else {
140                            // Now that we have the scope of the innermost submodule
141                            // in the import path, we may extract the desired item from
142                            // it.
143
144                            // Warn on `import ...: x as x`
145                            if let ast::ImportItem::Renamed(renamed_item) = &item {
146                                if renamed_item.original_name().as_str()
147                                    == renamed_item.new_name().as_str()
148                                {
149                                    vm.engine.sink.warn(warning!(
150                                        renamed_item.new_name().span(),
151                                        "unnecessary import rename to same name",
152                                    ));
153                                }
154                            }
155
156                            vm.bind(item.bound_name(), binding.clone());
157                        }
158                    }
159                }
160                if !errors.is_empty() {
161                    return Err(errors);
162                }
163            }
164        }
165
166        Ok(Value::None)
167    }
168}
169
170impl Eval for ast::ModuleInclude<'_> {
171    type Output = Content;
172
173    fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
174        let span = self.source().span();
175        let source = self.source().eval(vm)?;
176        let module = match source {
177            Value::Str(path) => import(&mut vm.engine, &path, span)?,
178            Value::Module(module) => module,
179            v => bail!(span, "expected path or module, found {}", v.ty()),
180        };
181        Ok(module.content())
182    }
183}
184
185/// Process an import of a package or file relative to the current location.
186pub fn import(engine: &mut Engine, from: &str, span: Span) -> SourceResult<Module> {
187    if from.starts_with('@') {
188        let spec = from.parse::<PackageSpec>().at(span)?;
189        import_package(engine, spec, span)
190    } else {
191        let id = span.resolve_path(from).at(span)?;
192        import_file(engine, id, span)
193    }
194}
195
196/// Import a file from a path. The path is resolved relative to the given
197/// `span`.
198fn import_file(engine: &mut Engine, id: FileId, span: Span) -> SourceResult<Module> {
199    // Load the source file.
200    let source = engine.world.source(id).at(span)?;
201
202    // Prevent cyclic importing.
203    if engine.route.contains(source.id()) {
204        bail!(span, "cyclic import");
205    }
206
207    // Evaluate the file.
208    let point = || Tracepoint::Import;
209    eval(
210        engine.routines,
211        engine.world,
212        engine.traced,
213        TrackedMut::reborrow_mut(&mut engine.sink),
214        engine.route.track(),
215        &source,
216    )
217    .trace(engine.world, point, span)
218}
219
220/// Import an external package.
221fn import_package(
222    engine: &mut Engine,
223    spec: PackageSpec,
224    span: Span,
225) -> SourceResult<Module> {
226    let (name, id) = resolve_package(engine, spec, span)?;
227    import_file(engine, id, span).map(|module| module.with_name(name))
228}
229
230/// Resolve the name and entrypoint of a package.
231fn resolve_package(
232    engine: &mut Engine,
233    spec: PackageSpec,
234    span: Span,
235) -> SourceResult<(EcoString, FileId)> {
236    // Evaluate the manifest.
237    let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
238    let bytes = engine.world.file(manifest_id).at(span)?;
239    let string = bytes.as_str().map_err(FileError::from).at(span)?;
240    let manifest: PackageManifest = toml::from_str(string)
241        .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
242        .at(span)?;
243    manifest.validate(&spec).at(span)?;
244
245    // Evaluate the entry point.
246    Ok((manifest.package.name, manifest_id.join(&manifest.package.entrypoint)))
247}