use comemo::TrackedMut; use ecow::{eco_format, eco_vec, EcoString}; use typst_library::diag::{ bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, }; use typst_library::engine::Engine; use typst_library::foundations::{Binding, Content, Module, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; use typst_syntax::{FileId, Span, VirtualPath}; use crate::{eval, Eval, Vm}; impl Eval for ast::ModuleImport<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { let source_expr = self.source(); let source_span = source_expr.span(); let mut source = source_expr.eval(vm)?; let mut is_str = false; match &source { Value::Func(func) => { if func.scope().is_none() { bail!(source_span, "cannot import from user-defined functions"); } } Value::Type(_) => {} Value::Module(_) => {} Value::Str(path) => { source = Value::Module(import(&mut vm.engine, path, source_span)?); is_str = true; } v => { bail!( source_span, "expected path, module, function, or type, found {}", v.ty() ) } } // If there is a rename, import the source itself under that name. let new_name = self.new_name(); if let Some(new_name) = new_name { if let ast::Expr::Ident(ident) = self.source() { if ident.as_str() == new_name.as_str() { // Warn on `import x as x` vm.engine.sink.warn(warning!( new_name.span(), "unnecessary import rename to same name", )); } } // Define renamed module on the scope. vm.define(new_name, source.clone()); } let scope = source.scope().unwrap(); match self.imports() { None => { if new_name.is_none() { match self.bare_name() { // Bare dynamic string imports are not allowed. Ok(name) if !is_str || matches!(source_expr, ast::Expr::Str(_)) => { if matches!(source_expr, ast::Expr::Ident(_)) { vm.engine.sink.warn(warning!( source_expr.span(), "this import has no effect", )); } vm.scopes.top.bind(name, Binding::new(source, source_span)); } Ok(_) | Err(BareImportError::Dynamic) => bail!( source_span, "dynamic import requires an explicit name"; hint: "you can name the import with `as`" ), Err(BareImportError::PathInvalid) => bail!( source_span, "module name would not be a valid identifier"; hint: "you can rename the import with `as`", ), // Bad package spec would have failed the import already. Err(BareImportError::PackageInvalid) => unreachable!(), } } } Some(ast::Imports::Wildcard) => { for (var, binding) in scope.iter() { vm.scopes.top.bind(var.clone(), binding.clone()); } } Some(ast::Imports::Items(items)) => { let mut errors = eco_vec![]; for item in items.iter() { let mut path = item.path().iter().peekable(); let mut scope = scope; while let Some(component) = &path.next() { let Some(binding) = scope.get(component) else { errors.push(error!(component.span(), "unresolved import")); break; }; if path.peek().is_some() { // Nested import, as this is not the last component. // This must be a submodule. let value = binding.read(); let Some(submodule) = value.scope() else { let error = if matches!(value, Value::Func(function) if function.scope().is_none()) { error!( component.span(), "cannot import from user-defined functions" ) } else if !matches!( value, Value::Func(_) | Value::Module(_) | Value::Type(_) ) { error!( component.span(), "expected module, function, or type, found {}", value.ty() ) } else { panic!("unexpected nested import failure") }; errors.push(error); break; }; // Walk into the submodule. scope = submodule; } else { // Now that we have the scope of the innermost submodule // in the import path, we may extract the desired item from // it. // Warn on `import ...: x as x` if let ast::ImportItem::Renamed(renamed_item) = &item { if renamed_item.original_name().as_str() == renamed_item.new_name().as_str() { vm.engine.sink.warn(warning!( renamed_item.new_name().span(), "unnecessary import rename to same name", )); } } vm.bind(item.bound_name(), binding.clone()); } } } if !errors.is_empty() { return Err(errors); } } } Ok(Value::None) } } impl Eval for ast::ModuleInclude<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { let span = self.source().span(); let source = self.source().eval(vm)?; let module = match source { Value::Str(path) => import(&mut vm.engine, &path, span)?, Value::Module(module) => module, v => bail!(span, "expected path or module, found {}", v.ty()), }; Ok(module.content()) } } /// Process an import of a package or file relative to the current location. pub fn import(engine: &mut Engine, from: &str, span: Span) -> SourceResult { if from.starts_with('@') { let spec = from.parse::().at(span)?; import_package(engine, spec, span) } else { let id = span.resolve_path(from).at(span)?; import_file(engine, id, span) } } /// Import a file from a path. The path is resolved relative to the given /// `span`. fn import_file(engine: &mut Engine, id: FileId, span: Span) -> SourceResult { // Load the source file. let source = engine.world.source(id).at(span)?; // Prevent cyclic importing. if engine.route.contains(source.id()) { bail!(span, "cyclic import"); } // Evaluate the file. let point = || Tracepoint::Import; eval( engine.routines, engine.world, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), &source, ) .trace(engine.world, point, span) } /// Import an external package. fn import_package( engine: &mut Engine, spec: PackageSpec, span: Span, ) -> SourceResult { let (name, id) = resolve_package(engine, spec, span)?; import_file(engine, id, span).map(|module| module.with_name(name)) } /// Resolve the name and entrypoint of a package. fn resolve_package( engine: &mut Engine, spec: PackageSpec, span: Span, ) -> SourceResult<(EcoString, FileId)> { // Evaluate the manifest. let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let bytes = engine.world.file(manifest_id).at(span)?; let string = bytes.as_str().map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .at(span)?; manifest.validate(&spec).at(span)?; // Evaluate the entry point. Ok((manifest.package.name, manifest_id.join(&manifest.package.entrypoint))) }