1use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::fmt::{self, Write as _};
6use std::ops::Range;
7use std::path::PathBuf;
8
9use ecow::eco_format;
10#[cfg(feature = "local-registry")]
11use ecow::{EcoVec, eco_vec};
12use rustc_hash::{FxHashMap, FxHashSet};
14use serde::{Deserialize, Serialize};
15use tinymist_world::package::registry::PackageIndexEntry;
16use tinymist_world::package::{PackageSpec, PackageSpecExt};
17use typst::World;
18use typst::diag::{EcoString, StrResult};
19use typst::syntax::package::PackageManifest;
20use typst::syntax::{
21 FileId, LinkedNode, RootedPath, Source, Span, SyntaxKind, VirtualPath, VirtualRoot, ast,
22};
23use typst_shim::syntax::{resolve_path_from_id, source_range};
24
25use crate::LocalContext;
26use crate::analysis::{SharedContext, TypeInfo};
27use crate::syntax::{DeclExpr, Expr, LexicalScope, Pattern, PatternSig};
28use crate::ty::Ty;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PackageInfo {
33 pub path: PathBuf,
35 pub namespace: EcoString,
37 pub name: EcoString,
39 pub version: String,
41}
42
43impl From<PackageIndexEntry> for PackageInfo {
44 fn from(entry: PackageIndexEntry) -> Self {
45 let spec = entry.spec();
46 Self {
47 path: entry.path.unwrap_or_default(),
48 namespace: spec.namespace,
49 name: spec.name,
50 version: spec.version.to_string(),
51 }
52 }
53}
54
55pub fn parse_package_import(node: &LinkedNode) -> Option<PackageSpec> {
58 if !matches!(node.kind(), SyntaxKind::Str) {
59 return None;
60 }
61
62 let import_node = node.parent()?.cast::<ast::ModuleImport>()?;
63
64 let ast::Expr::Str(str_node) = import_node.source() else {
65 return None;
66 };
67 let import_str = str_node.get();
68 if import_str.starts_with('@') {
69 import_str.parse().ok()
70 } else {
71 None
72 }
73}
74
75pub fn find_package_and_latest<'a>(
78 ctx: &'a SharedContext,
79 package_spec: &PackageSpec,
80) -> (
81 Option<Cow<'a, PackageIndexEntry>>,
82 Option<Cow<'a, PackageIndexEntry>>,
83) {
84 let versionless_spec = package_spec.versionless();
85
86 if package_spec.is_preview() {
87 let packages = ctx.world().packages();
88
89 let current = packages.iter().find(|it| it.matches(package_spec));
90 let latest = packages
91 .iter()
92 .filter(|it| it.matches_versionless(&versionless_spec))
93 .max_by_key(|entry| entry.package.version);
94
95 (current.map(Cow::Borrowed), latest.map(Cow::Borrowed))
96 } else if cfg!(feature = "local-registry") {
97 let local_packages = ctx.non_preview_packages();
98
99 let current = local_packages.iter().find(|it| it.matches(package_spec));
100 let latest = local_packages
101 .iter()
102 .filter(|it| it.matches_versionless(&versionless_spec))
103 .max_by_key(|entry| entry.package.version);
104
105 (
106 current.map(|p| Cow::Owned(p.clone())),
107 latest.map(|p| Cow::Owned(p.clone())),
108 )
109 } else {
110 (None, None)
111 }
112}
113
114pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
116 Ok(FileId::new(RootedPath::new(
117 VirtualRoot::Package(PackageSpec {
118 namespace: spec.namespace.clone(),
119 name: spec.name.clone(),
120 version: spec.version.parse()?,
121 }),
122 VirtualPath::new("typst.toml").expect("valid manifest path"),
123 )))
124}
125
126pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
128 let toml_data = world
129 .file(toml_id)
130 .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
131
132 let string = std::str::from_utf8(&toml_data)
133 .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
134
135 toml::from_str(string)
136 .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
137}
138
139pub(crate) fn package_entrypoint_id(manifest_id: FileId, entrypoint: &str) -> FileId {
140 resolve_path_from_id(manifest_id, entrypoint)
141 .expect("valid package entrypoint")
142 .intern()
143}
144
145pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
147 let toml_id = get_manifest_id(spec)?;
148 let manifest = ctx.get_manifest(toml_id)?;
149
150 let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
151
152 ctx.preload_package(entry_point);
153 Ok(())
154}
155
156pub fn package_tyck_scope(
158 ctx: &mut LocalContext,
159 spec: &PackageInfo,
160 options: PackageTyckDumpOptions,
161) -> StrResult<PackageTyckDump> {
162 let toml_id = get_manifest_id(spec)?;
163 let manifest = ctx.get_manifest(toml_id)?;
164 let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
165 let files = collect_package_tyck_files(ctx, entry_point, options)?;
166
167 Ok(PackageTyckDump {
168 schema: 1,
169 package: DumpPackageInfo {
170 namespace: spec.namespace.to_string(),
171 name: spec.name.to_string(),
172 version: spec.version.clone(),
173 spec: format!("@{}/{}:{}", spec.namespace, spec.name, spec.version),
174 path: spec.path.to_string_lossy().into_owned(),
175 entrypoint: manifest.package.entrypoint.to_string(),
176 },
177 entrypoint: dump_file_id(entry_point),
178 files,
179 })
180}
181
182fn collect_package_tyck_files(
183 ctx: &mut LocalContext,
184 entry_point: FileId,
185 options: PackageTyckDumpOptions,
186) -> StrResult<Vec<DumpFile>> {
187 let package_root = entry_point.root().clone();
188 let mut files = vec![];
189 let mut seen = FxHashSet::default();
190 let mut queue = VecDeque::from([entry_point]);
191
192 while let Some(fid) = queue.pop_front() {
193 if !seen.insert(fid) {
194 continue;
195 }
196
197 let source = match ctx.source_by_id(fid) {
198 Ok(source) => source,
199 Err(err) if fid == entry_point => {
200 return Err(eco_format!(
201 "failed to read package entrypoint {fid:?}: {err}"
202 ));
203 }
204 Err(err) => {
205 log::warn!("skipping unreadable package source {fid:?}: {err}");
206 continue;
207 }
208 };
209 let expr_info = ctx.expr_stage(&source);
210 let type_info = ctx.type_check(&source);
211
212 let mut imported_files = expr_info
213 .imports
214 .keys()
215 .copied()
216 .map(dump_file_id)
217 .collect::<Vec<_>>();
218 imported_files.sort_by(|left, right| left.file_id.cmp(&right.file_id));
219
220 for imported in expr_info.imports.keys().copied() {
221 if imported.root() == &package_root && !seen.contains(&imported) {
222 queue.push_back(imported);
223 }
224 }
225
226 files.push(dump_file_scope(
227 &source,
228 &expr_info.exports,
229 &expr_info.root,
230 &type_info,
231 imported_files,
232 options,
233 ));
234 }
235
236 files.sort_by(|left, right| left.file_id.cmp(&right.file_id));
237 Ok(files)
238}
239
240#[derive(Debug, Clone, Copy, Default)]
242pub struct PackageTyckDumpOptions {
243 pub max_type_chars: Option<usize>,
248}
249
250#[derive(Debug, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct PackageTyckDump {
254 schema: u32,
255 package: DumpPackageInfo,
256 entrypoint: DumpFileId,
257 files: Vec<DumpFile>,
258}
259
260#[derive(Debug, Serialize)]
261#[serde(rename_all = "camelCase")]
262struct DumpPackageInfo {
263 namespace: String,
264 name: String,
265 version: String,
266 spec: String,
267 path: String,
268 entrypoint: String,
269}
270
271#[derive(Debug, Clone, Serialize)]
272#[serde(rename_all = "camelCase")]
273struct DumpFileId {
274 file_id: String,
275 root: String,
276 path: String,
277}
278
279#[derive(Debug, Serialize)]
280#[serde(rename_all = "camelCase")]
281struct DumpFile {
282 file_id: String,
283 root: String,
284 path: String,
285 imports: Vec<DumpFileId>,
286 scopes: Vec<DumpScope>,
287 type_mappings: Vec<DumpTypeMapping>,
288}
289
290#[derive(Debug, Serialize)]
291#[serde(rename_all = "camelCase")]
292struct DumpScope {
293 kind: &'static str,
294 name: String,
295 declaration: Option<DumpDecl>,
296 variables: Vec<DumpVariable>,
297}
298
299#[derive(Debug, Serialize)]
300#[serde(rename_all = "camelCase")]
301struct DumpVariable {
302 name: String,
303 kind: String,
304 source: &'static str,
305 exported: bool,
306 declaration: DumpDecl,
307 expression: Option<String>,
308 ty: Option<DumpType>,
309}
310
311#[derive(Debug, Clone, Copy)]
312struct DumpVariableOrigin {
313 source: &'static str,
314 exported: bool,
315}
316
317#[derive(Debug, Serialize)]
318#[serde(rename_all = "camelCase")]
319struct DumpDecl {
320 debug: String,
321 kind: String,
322 file_id: Option<String>,
323 range: Option<DumpRange>,
324}
325
326#[derive(Clone, Debug, Serialize)]
327#[serde(rename_all = "camelCase")]
328struct DumpType {
329 debug: EcoString,
330 describe: Option<EcoString>,
331 repr: Option<EcoString>,
332}
333
334struct TypeDumper<'a> {
335 type_info: &'a TypeInfo,
336 options: PackageTyckDumpOptions,
337 cache: FxHashMap<Ty, DumpType>,
338}
339
340impl<'a> TypeDumper<'a> {
341 fn new(type_info: &'a TypeInfo, options: PackageTyckDumpOptions) -> Self {
342 Self {
343 type_info,
344 options,
345 cache: FxHashMap::default(),
346 }
347 }
348
349 fn dump(&mut self, source: Ty) -> DumpType {
350 if let Some(dump) = self.cache.get(&source) {
351 return dump.clone();
352 }
353
354 let ty = self.type_info.simplify(source.clone(), true);
355 let display_ty = if contains_signature_binders(&ty) {
356 self.type_info.simplify(source.clone(), false)
357 } else {
358 ty.clone()
359 };
360 let dump = DumpType {
361 debug: format_debug_dump(&ty, self.options.max_type_chars).into(),
362 describe: display_ty.describe().map(|text| {
363 truncate_dump_string(text.to_string(), self.options.max_type_chars).into()
364 }),
365 repr: display_ty.repr().map(|text| {
366 truncate_dump_string(text.to_string(), self.options.max_type_chars).into()
367 }),
368 };
369 self.cache.insert(source, dump.clone());
370 dump
371 }
372}
373
374#[derive(Debug, Serialize)]
375#[serde(rename_all = "camelCase")]
376struct DumpTypeMapping {
377 range: DumpRange,
378 ty: DumpType,
379}
380
381#[derive(Debug, Serialize)]
382#[serde(rename_all = "camelCase")]
383struct DumpRange {
384 start: usize,
385 end: usize,
386}
387
388fn dump_file_scope(
389 source: &Source,
390 exports: &LexicalScope,
391 root_expr: &Expr,
392 type_info: &TypeInfo,
393 imports: Vec<DumpFileId>,
394 options: PackageTyckDumpOptions,
395) -> DumpFile {
396 let fid = source.id();
397 let file = dump_file_id(fid);
398 let mut types = TypeDumper::new(type_info, options);
399
400 let file_scope = DumpScope {
401 kind: "file",
402 name: file.path.clone(),
403 declaration: None,
404 variables: dump_scope_variables(source, &mut types, exports, "export", true),
405 };
406
407 let mut scopes = vec![file_scope];
408 collect_function_scopes(source, &mut types, root_expr, &mut scopes);
409
410 DumpFile {
411 file_id: file.file_id,
412 root: file.root,
413 path: file.path,
414 imports,
415 scopes,
416 type_mappings: dump_type_mappings(source, &mut types),
417 }
418}
419
420fn dump_scope_variables(
421 source: &Source,
422 types: &mut TypeDumper,
423 scope: &LexicalScope,
424 var_source: &'static str,
425 exported: bool,
426) -> Vec<DumpVariable> {
427 let origin = DumpVariableOrigin {
428 source: var_source,
429 exported,
430 };
431 let mut vars = scope
432 .iter()
433 .filter_map(|(name, expr)| {
434 let decl = expr_decl(expr)?;
435 Some(dump_variable(
436 source,
437 types,
438 name.as_ref(),
439 decl,
440 Some(expr),
441 origin,
442 ))
443 })
444 .collect::<Vec<_>>();
445
446 vars.sort_by(variable_cmp);
447 vars.dedup_by(|left, right| {
448 left.name == right.name && left.declaration.debug == right.declaration.debug
449 });
450 vars
451}
452
453fn collect_function_scopes(
454 source: &Source,
455 types: &mut TypeDumper,
456 expr: &Expr,
457 scopes: &mut Vec<DumpScope>,
458) {
459 if let Expr::Func(func) = expr {
460 let mut variables = vec![];
461 collect_pattern_sig_variables(source, types, &func.params, "parameter", &mut variables);
462 collect_local_variables(source, types, &func.body, &mut variables);
463 variables.sort_by(variable_cmp);
464 variables.dedup_by(|left, right| {
465 left.name == right.name && left.declaration.debug == right.declaration.debug
466 });
467
468 scopes.push(DumpScope {
469 kind: "function",
470 name: scope_name(&func.decl),
471 declaration: Some(dump_decl(source, &func.decl)),
472 variables,
473 });
474
475 collect_function_scopes(source, types, &func.body, scopes);
476 return;
477 }
478
479 walk_expr_children(expr, &mut |child| {
480 collect_function_scopes(source, types, child, scopes);
481 });
482}
483
484fn collect_local_variables(
485 source: &Source,
486 types: &mut TypeDumper,
487 expr: &Expr,
488 variables: &mut Vec<DumpVariable>,
489) {
490 match expr {
491 Expr::Func(_) => {}
492 Expr::Let(let_expr) => {
493 collect_pattern_variables(source, types, &let_expr.pattern, "local", variables);
494 if let Some(body) = &let_expr.body {
495 collect_local_variables(source, types, body, variables);
496 }
497 }
498 Expr::ForLoop(for_loop) => {
499 collect_pattern_variables(source, types, &for_loop.pattern, "local", variables);
500 collect_local_variables(source, types, &for_loop.iter, variables);
501 collect_local_variables(source, types, &for_loop.body, variables);
502 }
503 _ => {
504 walk_expr_children(expr, &mut |child| {
505 collect_local_variables(source, types, child, variables);
506 });
507 }
508 }
509}
510
511fn collect_pattern_sig_variables(
512 source: &Source,
513 types: &mut TypeDumper,
514 sig: &PatternSig,
515 var_source: &'static str,
516 variables: &mut Vec<DumpVariable>,
517) {
518 for pattern in &sig.pos {
519 collect_pattern_variables(source, types, pattern, var_source, variables);
520 }
521 for (decl, pattern) in &sig.named {
522 variables.push(dump_variable(
523 source,
524 types,
525 decl.name().as_ref(),
526 decl,
527 None,
528 DumpVariableOrigin {
529 source: var_source,
530 exported: false,
531 },
532 ));
533 collect_pattern_variables(source, types, pattern, var_source, variables);
534 }
535 for (decl, pattern) in sig.spread_left.iter().chain(sig.spread_right.iter()) {
536 variables.push(dump_variable(
537 source,
538 types,
539 decl.name().as_ref(),
540 decl,
541 None,
542 DumpVariableOrigin {
543 source: var_source,
544 exported: false,
545 },
546 ));
547 collect_pattern_variables(source, types, pattern, var_source, variables);
548 }
549}
550
551fn collect_pattern_variables(
552 source: &Source,
553 types: &mut TypeDumper,
554 pattern: &Pattern,
555 var_source: &'static str,
556 variables: &mut Vec<DumpVariable>,
557) {
558 match pattern {
559 Pattern::Expr(expr) => collect_local_variables(source, types, expr, variables),
560 Pattern::Simple(decl) => {
561 variables.push(dump_variable(
562 source,
563 types,
564 decl.name().as_ref(),
565 decl,
566 None,
567 DumpVariableOrigin {
568 source: var_source,
569 exported: false,
570 },
571 ));
572 }
573 Pattern::Sig(sig) => {
574 collect_pattern_sig_variables(source, types, sig, var_source, variables);
575 }
576 }
577}
578
579fn walk_expr_children(expr: &Expr, f: &mut impl FnMut(&Expr)) {
580 match expr {
581 Expr::Block(exprs) => exprs.iter().for_each(f),
582 Expr::Array(args) | Expr::Dict(args) | Expr::Args(args) => {
583 walk_args(args.args.iter(), f);
584 }
585 Expr::Pattern(pattern) => walk_pattern(pattern, f),
586 Expr::Element(elem) => elem.content.iter().for_each(f),
587 Expr::Unary(unary) => f(&unary.lhs),
588 Expr::Binary(binary) => {
589 f(&binary.operands.0);
590 f(&binary.operands.1);
591 }
592 Expr::Apply(apply) => {
593 f(&apply.callee);
594 f(&apply.args);
595 }
596 Expr::Func(func) => {
597 walk_pattern_sig(&func.params, f);
598 f(&func.body);
599 }
600 Expr::Let(let_expr) => {
601 walk_pattern(&let_expr.pattern, f);
602 if let Some(body) = &let_expr.body {
603 f(body);
604 }
605 }
606 Expr::Show(show) => {
607 if let Some(selector) = &show.selector {
608 f(selector);
609 }
610 f(&show.edit);
611 }
612 Expr::Set(set) => {
613 f(&set.target);
614 f(&set.args);
615 if let Some(cond) = &set.cond {
616 f(cond);
617 }
618 }
619 Expr::Ref(ref_expr) => {
620 if let Some(step) = &ref_expr.step {
621 f(step);
622 }
623 if let Some(root) = &ref_expr.root {
624 f(root);
625 }
626 }
627 Expr::ContentRef(content_ref) => {
628 if let Some(body) = &content_ref.body {
629 f(body);
630 }
631 }
632 Expr::Select(select) => f(&select.lhs),
633 Expr::Import(import) => {
634 f(&import.source);
635 }
636 Expr::Include(include) => {
637 f(&include.source);
638 }
639 Expr::Contextual(inner) => f(inner),
640 Expr::Conditional(cond) => {
641 f(&cond.cond);
642 f(&cond.then);
643 f(&cond.else_);
644 }
645 Expr::WhileLoop(while_loop) => {
646 f(&while_loop.cond);
647 f(&while_loop.body);
648 }
649 Expr::ForLoop(for_loop) => {
650 walk_pattern(&for_loop.pattern, f);
651 f(&for_loop.iter);
652 f(&for_loop.body);
653 }
654 Expr::Type(_) | Expr::Decl(_) | Expr::Star => {}
655 }
656}
657
658fn walk_args<'a>(
659 args: impl Iterator<Item = &'a crate::syntax::ArgExpr>,
660 f: &mut impl FnMut(&Expr),
661) {
662 for arg in args {
663 match arg {
664 crate::syntax::ArgExpr::Pos(expr) | crate::syntax::ArgExpr::Spread(expr) => f(expr),
665 crate::syntax::ArgExpr::Named(pair) => f(&pair.1),
666 crate::syntax::ArgExpr::NamedRt(pair) => {
667 f(&pair.0);
668 f(&pair.1);
669 }
670 }
671 }
672}
673
674fn walk_pattern(pattern: &Pattern, f: &mut impl FnMut(&Expr)) {
675 match pattern {
676 Pattern::Expr(expr) => f(expr),
677 Pattern::Simple(_) => {}
678 Pattern::Sig(sig) => walk_pattern_sig(sig, f),
679 }
680}
681
682fn walk_pattern_sig(sig: &PatternSig, f: &mut impl FnMut(&Expr)) {
683 for pattern in &sig.pos {
684 walk_pattern(pattern, f);
685 }
686 for (_, pattern) in &sig.named {
687 walk_pattern(pattern, f);
688 }
689 for (_, pattern) in sig.spread_left.iter().chain(sig.spread_right.iter()) {
690 walk_pattern(pattern, f);
691 }
692}
693
694fn expr_decl(expr: &Expr) -> Option<&DeclExpr> {
695 match expr {
696 Expr::Decl(decl) => Some(decl),
697 Expr::Ref(ref_expr) => ref_expr
698 .root
699 .as_ref()
700 .and_then(expr_decl)
701 .or(Some(&ref_expr.decl)),
702 _ => None,
703 }
704}
705
706fn dump_variable(
707 source: &Source,
708 types: &mut TypeDumper,
709 name: &str,
710 decl: &DeclExpr,
711 expr: Option<&Expr>,
712 origin: DumpVariableOrigin,
713) -> DumpVariable {
714 let ty = types
715 .type_info
716 .vars
717 .get(decl)
718 .map(|bounds| bounds.as_type())
719 .map(|ty| types.dump(ty));
720
721 DumpVariable {
722 name: name.to_owned(),
723 kind: decl.kind().to_string(),
724 source: origin.source,
725 exported: origin.exported,
726 declaration: dump_decl(source, decl),
727 expression: expr.map(ToString::to_string),
728 ty,
729 }
730}
731
732fn dump_decl(source: &Source, decl: &DeclExpr) -> DumpDecl {
733 DumpDecl {
734 debug: format!("{decl:?}"),
735 kind: decl.kind().to_string(),
736 file_id: decl.file_id().map(|fid| dump_file_id(fid).file_id),
737 range: dump_span_range(source, decl.span()),
738 }
739}
740
741fn contains_signature_binders(ty: &Ty) -> bool {
742 contains_signature_binders_inner(ty, &mut FxHashSet::default(), &mut FxHashSet::default())
743}
744
745#[allow(clippy::mutable_key_type)]
746fn contains_signature_binders_inner(
747 ty: &Ty,
748 traversed: &mut FxHashSet<Ty>,
749 type_var_traversed: &mut FxHashSet<Ty>,
750) -> bool {
751 if !traversed.insert(ty.clone()) {
752 return false;
753 }
754
755 match ty {
756 Ty::Func(sig) | Ty::Pattern(sig) => {
757 sig.inputs()
758 .any(|ty| contains_type_var(ty, type_var_traversed))
759 || sig
760 .inputs()
761 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
762 || sig.body.as_ref().is_some_and(|ty| {
763 contains_signature_binders_inner(ty, traversed, type_var_traversed)
764 })
765 }
766 Ty::Args(sig) => {
767 sig.inputs()
768 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
769 || sig.body.as_ref().is_some_and(|ty| {
770 contains_signature_binders_inner(ty, traversed, type_var_traversed)
771 })
772 }
773 Ty::With(with) => {
774 contains_signature_binders_inner(&with.sig, traversed, type_var_traversed)
775 || with
776 .with
777 .inputs()
778 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
779 || with.with.body.as_ref().is_some_and(|ty| {
780 contains_signature_binders_inner(ty, traversed, type_var_traversed)
781 })
782 }
783 Ty::Param(param) => {
784 contains_signature_binders_inner(¶m.ty, traversed, type_var_traversed)
785 }
786 Ty::Union(types) | Ty::Tuple(types) => types
787 .iter()
788 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
789 Ty::Let(bounds) => bounds
790 .lbs
791 .iter()
792 .chain(&bounds.ubs)
793 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
794 Ty::Dict(record) => record
795 .types
796 .iter()
797 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
798 Ty::Array(elem) => contains_signature_binders_inner(elem, traversed, type_var_traversed),
799 Ty::Select(select) => {
800 contains_signature_binders_inner(&select.ty, traversed, type_var_traversed)
801 }
802 Ty::Unary(unary) => {
803 contains_signature_binders_inner(&unary.lhs, traversed, type_var_traversed)
804 }
805 Ty::Binary(binary) => binary
806 .operands()
807 .iter()
808 .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
809 Ty::If(if_ty) => {
810 contains_signature_binders_inner(&if_ty.cond, traversed, type_var_traversed)
811 || contains_signature_binders_inner(&if_ty.then, traversed, type_var_traversed)
812 || contains_signature_binders_inner(&if_ty.else_, traversed, type_var_traversed)
813 }
814 Ty::Var(_) | Ty::Any | Ty::Boolean(_) | Ty::Builtin(_) | Ty::Value(_) => false,
815 }
816}
817
818#[allow(clippy::mutable_key_type)]
819fn contains_type_var(ty: &Ty, traversed: &mut FxHashSet<Ty>) -> bool {
820 if !traversed.insert(ty.clone()) {
821 return false;
822 }
823
824 match ty {
825 Ty::Var(_) => true,
826 Ty::Func(sig) | Ty::Args(sig) | Ty::Pattern(sig) => {
827 sig.inputs().any(|ty| contains_type_var(ty, traversed))
828 || sig
829 .body
830 .as_ref()
831 .is_some_and(|ty| contains_type_var(ty, traversed))
832 }
833 Ty::With(with) => {
834 contains_type_var(&with.sig, traversed)
835 || with
836 .with
837 .inputs()
838 .any(|ty| contains_type_var(ty, traversed))
839 || with
840 .with
841 .body
842 .as_ref()
843 .is_some_and(|ty| contains_type_var(ty, traversed))
844 }
845 Ty::Param(param) => contains_type_var(¶m.ty, traversed),
846 Ty::Union(types) | Ty::Tuple(types) => {
847 types.iter().any(|ty| contains_type_var(ty, traversed))
848 }
849 Ty::Let(bounds) => bounds
850 .lbs
851 .iter()
852 .chain(&bounds.ubs)
853 .any(|ty| contains_type_var(ty, traversed)),
854 Ty::Dict(record) => record
855 .types
856 .iter()
857 .any(|ty| contains_type_var(ty, traversed)),
858 Ty::Array(elem) => contains_type_var(elem, traversed),
859 Ty::Select(select) => contains_type_var(&select.ty, traversed),
860 Ty::Unary(unary) => contains_type_var(&unary.lhs, traversed),
861 Ty::Binary(binary) => binary
862 .operands()
863 .iter()
864 .any(|ty| contains_type_var(ty, traversed)),
865 Ty::If(if_ty) => {
866 contains_type_var(&if_ty.cond, traversed)
867 || contains_type_var(&if_ty.then, traversed)
868 || contains_type_var(&if_ty.else_, traversed)
869 }
870 Ty::Any | Ty::Boolean(_) | Ty::Builtin(_) | Ty::Value(_) => false,
871 }
872}
873
874fn dump_type_mappings(source: &Source, types: &mut TypeDumper) -> Vec<DumpTypeMapping> {
875 let type_info = types.type_info;
876 let mut mappings = type_info
877 .mapping
878 .iter()
879 .filter_map(|(span, mapped_types)| {
880 let range = dump_span_range(source, *span)?;
881 let ty = Ty::from_types(mapped_types.clone().into_iter());
882 Some(DumpTypeMapping {
883 range,
884 ty: types.dump(ty),
885 })
886 })
887 .collect::<Vec<_>>();
888 mappings.sort_by(|left, right| {
889 left.range
890 .start
891 .cmp(&right.range.start)
892 .then_with(|| left.range.end.cmp(&right.range.end))
893 });
894 mappings
895}
896
897struct TruncatingString {
898 text: String,
899 remaining_chars: usize,
900 truncated: bool,
901}
902
903impl fmt::Write for TruncatingString {
904 fn write_str(&mut self, text: &str) -> fmt::Result {
905 if self.remaining_chars == 0 {
906 self.truncated = true;
907 return Err(fmt::Error);
908 }
909
910 if let Some((byte_idx, _)) = text.char_indices().nth(self.remaining_chars) {
911 self.text.push_str(&text[..byte_idx]);
912 self.remaining_chars = 0;
913 self.truncated = true;
914 Err(fmt::Error)
915 } else {
916 self.remaining_chars -= text.chars().count();
917 self.text.push_str(text);
918 Ok(())
919 }
920 }
921}
922
923fn format_debug_dump<T: fmt::Debug>(value: &T, max_chars: Option<usize>) -> String {
924 let Some(max_chars) = max_chars else {
925 return format!("{value:#?}");
926 };
927
928 let mut out = TruncatingString {
929 text: String::new(),
930 remaining_chars: max_chars,
931 truncated: false,
932 };
933 let _ = write!(&mut out, "{value:#?}");
934 if out.truncated {
935 out.text.push_str(" ... truncated ...");
936 out.text.shrink_to_fit();
937 }
938 out.text
939}
940
941fn truncate_dump_string(mut text: String, max_chars: Option<usize>) -> String {
942 let Some(max_chars) = max_chars else {
943 return text;
944 };
945 if let Some((byte_idx, _)) = text.char_indices().nth(max_chars) {
946 text.truncate(byte_idx);
947 text.push_str(" ... truncated ...");
948 text.shrink_to_fit();
949 }
950 text
951}
952
953fn dump_span_range(source: &Source, span: Span) -> Option<DumpRange> {
954 if span.id()? != source.id() {
955 return None;
956 }
957
958 let Range { start, end } = source_range(source, span)?;
959 Some(DumpRange { start, end })
960}
961
962fn dump_file_id(fid: FileId) -> DumpFileId {
963 let root = match fid.root() {
964 VirtualRoot::Project => "$project".to_owned(),
965 VirtualRoot::Package(spec) => spec.to_string(),
966 };
967 let path = fid.vpath().get_without_slash().to_owned();
968 let file_id = if matches!(fid.root(), VirtualRoot::Package(_)) {
969 format!("{root}/{}", fid.vpath().get_without_slash())
970 } else {
971 fid.vpath().get_with_slash().to_owned()
972 };
973
974 DumpFileId {
975 file_id,
976 root,
977 path,
978 }
979}
980
981fn scope_name(decl: &DeclExpr) -> String {
982 let name = decl.name().as_ref();
983 if name.is_empty() {
984 format!("{decl:?}")
985 } else {
986 name.to_owned()
987 }
988}
989
990fn variable_cmp(left: &DumpVariable, right: &DumpVariable) -> std::cmp::Ordering {
991 left.declaration
992 .range
993 .as_ref()
994 .map(|range| (range.start, range.end))
995 .cmp(
996 &right
997 .declaration
998 .range
999 .as_ref()
1000 .map(|range| (range.start, range.end)),
1001 )
1002 .then_with(|| left.name.cmp(&right.name))
1003 .then_with(|| left.kind.cmp(&right.kind))
1004}
1005
1006#[cfg(feature = "local-registry")]
1008pub enum PackageFilter {
1009 For(EcoString),
1011 ExceptFor(EcoString),
1013 All,
1015}
1016
1017#[cfg(feature = "local-registry")]
1018pub fn list_package(
1020 world: &tinymist_project::LspWorld,
1021 filter: PackageFilter,
1022) -> EcoVec<PackageIndexEntry> {
1023 trait IsDirFollowLinks {
1024 fn is_dir_follow_links(&self) -> bool;
1025 }
1026
1027 impl IsDirFollowLinks for PathBuf {
1028 fn is_dir_follow_links(&self) -> bool {
1029 self.canonicalize()
1032 .map(|meta| meta.is_dir())
1033 .unwrap_or(false)
1034 }
1035 }
1036
1037 let registry = &world.registry;
1038
1039 let mut packages = eco_vec![];
1043
1044 let paths = registry.paths();
1045 log::info!("searching for packages in paths {paths:?}");
1046
1047 let mut search_in_dir = |local_path: PathBuf, ns: EcoString| {
1048 if !local_path.exists() || !local_path.is_dir_follow_links() {
1049 return;
1050 }
1051 let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
1054 else {
1055 return;
1056 };
1057 for package in package_names {
1058 let Some(package) = once_log(package, "read package name") else {
1059 continue;
1060 };
1061 let package_name = EcoString::from(package.file_name().to_string_lossy());
1062 if package_name.starts_with('.') {
1063 continue;
1064 }
1065
1066 let package_path = package.path();
1067 if !package_path.is_dir_follow_links() {
1068 continue;
1069 }
1070 let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
1072 else {
1073 continue;
1074 };
1075 for version in versions {
1076 let Some(version_entry) = once_log(version, "read package version") else {
1077 continue;
1078 };
1079 if version_entry.file_name().to_string_lossy().starts_with('.') {
1080 continue;
1081 }
1082 let package_version_path = version_entry.path();
1083 if !package_version_path.is_dir_follow_links() {
1084 continue;
1085 }
1086 let Some(version) = once_log(
1087 version_entry.file_name().to_string_lossy().parse(),
1088 "parse package version",
1089 ) else {
1090 continue;
1091 };
1092 let spec = PackageSpec {
1093 namespace: ns.clone(),
1094 name: package_name.clone(),
1095 version,
1096 };
1097 let manifest_id = typst::syntax::FileId::new(typst::syntax::RootedPath::new(
1098 typst::syntax::VirtualRoot::Package(spec.clone()),
1099 typst::syntax::VirtualPath::new("typst.toml").expect("valid manifest path"),
1100 ));
1101 let Some(manifest) =
1102 once_log(get_manifest(world, manifest_id), "read package manifest")
1103 else {
1104 continue;
1105 };
1106 packages.push(PackageIndexEntry {
1107 namespace: ns.clone(),
1108 package: manifest.package,
1109 template: manifest.template,
1110 updated_at: None,
1111 path: Some(package_version_path),
1112 });
1113 }
1114 }
1115 };
1116
1117 for dir in paths {
1118 let matching_ns = match &filter {
1119 PackageFilter::For(ns) => {
1120 let local_path = dir.join(ns.as_str());
1121 search_in_dir(local_path, ns.clone());
1122
1123 continue;
1124 }
1125 PackageFilter::ExceptFor(ns) => Some(ns),
1126 PackageFilter::All => None,
1127 };
1128
1129 let Some(namespaces) = once_log(std::fs::read_dir(dir), "read package directory") else {
1130 continue;
1131 };
1132 for dir in namespaces {
1133 let Some(dir) = once_log(dir, "read ns directory") else {
1134 continue;
1135 };
1136 let ns = dir.file_name();
1137 let ns = ns.to_string_lossy();
1138 if let Some(matching_ns) = &matching_ns
1139 && matching_ns.as_str() == ns.as_ref()
1140 {
1141 continue;
1142 }
1143 let local_path = dir.path();
1144 search_in_dir(local_path, ns.into());
1145 }
1146 }
1147
1148 packages
1149}
1150
1151#[cfg(feature = "local-registry")]
1152fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
1153 use std::collections::HashSet;
1154 use std::sync::OnceLock;
1155
1156 use parking_lot::Mutex;
1157
1158 let err = match result {
1159 Ok(value) => return Some(value),
1160 Err(err) => err,
1161 };
1162
1163 static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
1164 let mut once = ONCE.get_or_init(Default::default).lock();
1165 if once.insert(site) {
1166 log::error!("failed to perform {site}: {err}");
1167 }
1168
1169 None
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use std::str::FromStr;
1175
1176 use typst::syntax::package::PackageSpec;
1177
1178 use super::*;
1179 use crate::syntax::Decl;
1180 use crate::tests::{run_with_ctx, run_with_sources};
1181 use crate::ty::{SigTy, TypeVar};
1182
1183 fn manifest_id() -> FileId {
1184 FileId::new(RootedPath::new(
1185 VirtualRoot::Package(
1186 PackageSpec::from_str("@preview/example:0.1.0").expect("valid package spec"),
1187 ),
1188 VirtualPath::new("typst.toml").expect("valid manifest path"),
1189 ))
1190 }
1191
1192 #[test]
1193 fn package_entrypoint_id_resolves_relative_to_manifest_parent() {
1194 let manifest_id = manifest_id();
1195 let entrypoint = package_entrypoint_id(manifest_id, "src/lib.typ");
1196
1197 assert_eq!(entrypoint.root(), manifest_id.root());
1198 assert_eq!(entrypoint.vpath().get_with_slash(), "/src/lib.typ");
1199 }
1200
1201 #[test]
1202 fn package_entrypoint_id_resolves_absolute_path_in_package_root() {
1203 let manifest_id = manifest_id();
1204 let entrypoint = package_entrypoint_id(manifest_id, "/lib.typ");
1205
1206 assert_eq!(entrypoint.root(), manifest_id.root());
1207 assert_eq!(entrypoint.vpath().get_with_slash(), "/lib.typ");
1208 }
1209
1210 #[test]
1211 #[allow(clippy::mutable_key_type)]
1212 fn signature_binder_detection_visits_shared_type_once() {
1213 const DEPTH: usize = 16;
1214
1215 let mut shared = Ty::Any;
1216 for _ in 0..DEPTH {
1217 shared = Ty::Tuple(vec![shared.clone(), shared].into());
1218 }
1219 let mut traversed = FxHashSet::default();
1220 assert!(!contains_signature_binders_inner(
1221 &shared,
1222 &mut traversed,
1223 &mut FxHashSet::default(),
1224 ));
1225 assert_eq!(traversed.len(), DEPTH + 1);
1226
1227 let binder = TypeVar::new("input".into(), Decl::lit("input").into());
1228 let binder_ty = Ty::Var(binder);
1229 assert!(contains_signature_binders(&Ty::Func(SigTy::unary(
1230 binder_ty,
1231 Ty::Any,
1232 ))));
1233 }
1234
1235 #[test]
1236 fn type_dumper_reuses_dumped_types() {
1237 let info = TypeInfo::default();
1238 let mut dumper = TypeDumper::new(
1239 &info,
1240 PackageTyckDumpOptions {
1241 max_type_chars: Some(128),
1242 },
1243 );
1244
1245 let first = dumper.dump(Ty::Any);
1246 let second = dumper.dump(Ty::Any);
1247
1248 assert_eq!(dumper.cache.len(), 1);
1249 assert_eq!(first.debug, second.debug);
1250 assert_eq!(first.describe, second.describe);
1251 assert_eq!(first.repr, second.repr);
1252 }
1253
1254 #[test]
1255 #[allow(clippy::mutable_key_type)]
1256 fn principal_dump_preserves_type_guard_branches() {
1257 run_with_sources(
1258 r#"
1259#let auto-cast(pat) = {
1260 if type(pat) == dictionary {
1261 pat
1262 } else if type(pat) == array {
1263 pat.map(auto-cast)
1264 }
1265}
1266"#,
1267 |verse, path| {
1268 run_with_ctx(verse, path, &|ctx, path| {
1269 let source = ctx.source_by_path(&path).unwrap();
1270 let info = ctx.type_check(&source);
1271 let mapped = info
1272 .mapping
1273 .iter()
1274 .find_map(|(span, mapped)| {
1275 let range = source_range(&source, *span)?;
1276 (source.text().get(range) == Some("pat.map")).then_some(mapped)
1277 })
1278 .expect("pat.map must have a mapped type");
1279 let source_ty = Ty::from_types(mapped.clone().into_iter());
1280 let mut dumper = TypeDumper::new(
1281 &info,
1282 PackageTyckDumpOptions {
1283 max_type_chars: Some(1024),
1284 },
1285 );
1286 let dumped = dumper.dump(source_ty);
1287
1288 assert!(dumped.debug.contains("Type(array)"));
1289 assert!(dumped.debug.contains("Type(dictionary)"));
1290 assert!(dumped.debug.contains(".map"));
1291 });
1292 },
1293 );
1294 }
1295
1296 #[test]
1297 fn package_tyck_files_skip_unreadable_imports() {
1298 run_with_sources(
1299 r#"
1300// path: present.typ
1301#let present = true
1302-----
1303// path: main.typ
1304#import "present.typ"
1305#import "missing.typ"
1306"#,
1307 |verse, path| {
1308 run_with_ctx(verse, path, &|ctx, path| {
1309 let entrypoint = ctx.source_by_path(&path).unwrap().id();
1310 let files = collect_package_tyck_files(ctx, entrypoint, Default::default())
1311 .expect("missing imports should not abort a package scan");
1312
1313 assert_eq!(
1314 files
1315 .iter()
1316 .map(|file| file.path.as_str())
1317 .collect::<Vec<_>>(),
1318 ["main.typ", "present.typ"]
1319 );
1320 let main = files
1321 .iter()
1322 .find(|file| file.path == "main.typ")
1323 .expect("entrypoint must be dumped");
1324 assert_eq!(
1325 main.imports
1326 .iter()
1327 .map(|import| import.path.as_str())
1328 .collect::<Vec<_>>(),
1329 ["missing.typ", "present.typ"]
1330 );
1331 });
1332 },
1333 );
1334 }
1335}