Skip to main content

typst_eval/
import.rs

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