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, Value};
9use typst_syntax::ast::{self, AstNode, BareImportError};
10use typst_syntax::package::{PackageManifest, PackageSpec};
11use typst_syntax::{FileId, Span, VirtualPath};
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 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                && ident.as_str() == new_name.as_str()
51            {
52                // Warn on `import x as x`
53                vm.engine.sink.warn(warning!(
54                    new_name.span(),
55                    "unnecessary import rename to same name",
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                                && 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                            vm.bind(item.bound_name(), binding.clone());
156                        }
157                    }
158                }
159                if !errors.is_empty() {
160                    return Err(errors);
161                }
162            }
163        }
164
165        Ok(Value::None)
166    }
167}
168
169impl Eval for ast::ModuleInclude<'_> {
170    type Output = Content;
171
172    fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
173        let span = self.source().span();
174        let source = self.source().eval(vm)?;
175        let module = match source {
176            Value::Str(path) => import(&mut vm.engine, &path, span)?,
177            Value::Module(module) => module,
178            v => bail!(span, "expected path or module, found {}", v.ty()),
179        };
180        Ok(module.content())
181    }
182}
183
184/// Process an import of a package or file relative to the current location.
185pub fn import(engine: &mut Engine, from: &str, span: Span) -> SourceResult<Module> {
186    if from.starts_with('@') {
187        let spec = from.parse::<PackageSpec>().at(span)?;
188        import_package(engine, spec, span)
189    } else {
190        let id = span.resolve_path(from).at(span)?;
191        import_file(engine, id, span)
192    }
193}
194
195/// Import a file from a path. The path is resolved relative to the given
196/// `span`.
197fn import_file(engine: &mut Engine, id: FileId, span: Span) -> SourceResult<Module> {
198    // Load the source file.
199    let source = engine.world.source(id).at(span)?;
200
201    // Prevent cyclic importing.
202    if engine.route.contains(source.id()) {
203        bail!(span, "cyclic import");
204    }
205
206    // Evaluate the file.
207    let point = || Tracepoint::Import;
208    eval(
209        engine.routines,
210        engine.world,
211        engine.traced,
212        TrackedMut::reborrow_mut(&mut engine.sink),
213        engine.route.track(),
214        &source,
215    )
216    .trace(engine.world, point, span)
217}
218
219/// Import an external package.
220fn import_package(
221    engine: &mut Engine,
222    spec: PackageSpec,
223    span: Span,
224) -> SourceResult<Module> {
225    let (name, id) = resolve_package(engine, spec, span)?;
226    import_file(engine, id, span).map(|module| module.with_name(name))
227}
228
229/// Resolve the name and entrypoint of a package.
230fn resolve_package(
231    engine: &mut Engine,
232    spec: PackageSpec,
233    span: Span,
234) -> SourceResult<(EcoString, FileId)> {
235    // Evaluate the manifest.
236    let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
237    let bytes = engine.world.file(manifest_id).at(span)?;
238    let string = bytes.as_str().map_err(FileError::from).at(span)?;
239    let manifest: PackageManifest = toml::from_str(string)
240        .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
241        .at(span)?;
242    manifest.validate(&spec).at(span)?;
243
244    // Evaluate the entry point.
245    Ok((manifest.package.name, manifest_id.join(&manifest.package.entrypoint)))
246}