From 6f8a40cc0cc4e55713af1795e990ea4e60cc19bb Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Wed, 17 Dec 2025 19:00:05 +0800 Subject: [PATCH] feat(analysis): derive ipcfg resolve map from ExprInfo --- crates/tinymist-analysis/src/cfg/ipcfg.rs | 63 +++++++++++++++++- crates/tinymist-analysis/src/cfg/tests.rs | 79 +++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/tinymist-analysis/src/cfg/ipcfg.rs b/crates/tinymist-analysis/src/cfg/ipcfg.rs index ef61d38e6..55b25cba7 100644 --- a/crates/tinymist-analysis/src/cfg/ipcfg.rs +++ b/crates/tinymist-analysis/src/cfg/ipcfg.rs @@ -1,7 +1,9 @@ -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use typst::syntax::ast::AstNode; use typst::syntax::{Span, SyntaxNode, ast}; +use crate::syntax::{Expr, ExprInfo, RefExpr as AnalysisRefExpr}; + use super::builder::build_cfgs_many; use super::ir::*; @@ -31,6 +33,65 @@ pub struct InterproceduralCfg { pub calls: Vec, } +/// Builds a [`ResolveMap`] from an [`ExprInfo`] resolve table. +/// +/// The resulting map can be passed to [`build_interprocedural_cfg`] to enable +/// call edges for `let`-bound closures and imported symbols without requiring a +/// separate resolver pass. +/// +/// This is best-effort: only references that can be traced back to a concrete +/// definition span (e.g. `Decl::Func` / `Decl::Var`) are included. +pub fn resolve_map_from_expr_info(ei: &ExprInfo) -> ResolveMap { + fn resolved_def_span(reference: &AnalysisRefExpr) -> Option { + let mut visited: FxHashSet> = FxHashSet::default(); + let mut stack: Vec = Vec::new(); + + if let Some(step) = reference.step.clone() { + stack.push(step); + } + if let Some(root) = reference.root.clone() { + stack.push(root); + } + + while let Some(expr) = stack.pop() { + match expr { + Expr::Decl(decl) => { + if decl.is_def() { + return Some(decl.span()); + } + } + Expr::Ref(r) => { + if visited.insert(r.clone()) { + if let Some(step) = r.step.clone() { + stack.push(step); + } + if let Some(root) = r.root.clone() { + stack.push(root); + } + } + } + Expr::Select(select) => { + stack.push(select.lhs.clone()); + } + _ => {} + } + } + + None + } + + let mut out = ResolveMap::default(); + for (&use_span, reference) in ei.resolves.iter() { + if use_span.is_detached() { + continue; + } + if let Some(def_span) = resolved_def_span(reference.as_ref()) { + out.insert(use_span, def_span); + } + } + out +} + /// Builds per-body CFGs plus best-effort call edges between bodies. /// /// `resolves` can optionally map callee identifier spans at call sites to their diff --git a/crates/tinymist-analysis/src/cfg/tests.rs b/crates/tinymist-analysis/src/cfg/tests.rs index 2a9a6c542..8992846c0 100644 --- a/crates/tinymist-analysis/src/cfg/tests.rs +++ b/crates/tinymist-analysis/src/cfg/tests.rs @@ -1,10 +1,16 @@ use super::*; use std::path::Path; +use std::sync::Arc; +use rustc_hash::FxHashMap; use typst::syntax::Source; use typst::syntax::ast::AstNode; use typst::syntax::{FileId, Span, VirtualPath, ast}; +use typst::utils::LazyHash; + +use crate::docs::DocString; +use crate::syntax::{Decl, Expr, ExprInfo, ExprInfoRepr, LexicalScope, RefExpr}; fn walk_exprs<'a>(node: &'a typst::syntax::SyntaxNode, f: &mut impl FnMut(ast::Expr<'a>)) { for child in node.children() { @@ -239,6 +245,79 @@ fn ipcfg_let_var_bound_closure_call_edge_with_resolve_map() { ); } +#[test] +fn ipcfg_resolve_map_from_expr_info_enables_let_bound_call_edge() { + let source = Source::detached( + r#"#{ + let f(x) = { x } + f(1) +}"#, + ); + + let mut def_ident: Option> = None; + let mut use_ident: Option> = None; + walk_exprs(source.root(), &mut |expr| match expr { + ast::Expr::LetBinding(let_) => { + if let ast::LetBindingKind::Closure(ident) = let_.kind() + && ident.get() == "f" + { + def_ident = Some(ident); + } + } + ast::Expr::FuncCall(call) => { + if let ast::Expr::Ident(ident) = call.callee() + && ident.get() == "f" + { + use_ident = Some(ident); + } + } + _ => {} + }); + + let def_ident = def_ident.expect("def ident"); + let use_ident = use_ident.expect("use ident"); + + // Create a minimal ExprInfo with only the resolve we need: + // use-site ident span -> reference chain that roots at the definition decl. + let def_decl: crate::syntax::DeclExpr = Decl::func(def_ident).into(); + let use_decl: crate::syntax::DeclExpr = Decl::ident_ref(use_ident).into(); + let reference = RefExpr { + decl: use_decl, + step: Some(Expr::Decl(def_decl.clone())), + root: Some(Expr::Decl(def_decl.clone())), + term: None, + }; + + let mut resolves: FxHashMap> = FxHashMap::default(); + resolves.insert(use_ident.span(), crate::ty::Interned::new(reference)); + + let ei = ExprInfo::new(ExprInfoRepr { + fid: source.id(), + revision: 0, + source: source.clone(), + root: Expr::Star, + module_docstring: Arc::new(DocString::default()), + exports: Arc::new(LazyHash::new(LexicalScope::default())), + imports: FxHashMap::default(), + exprs: FxHashMap::default(), + resolves, + docstrings: FxHashMap::default(), + module_items: FxHashMap::default(), + }); + + let resolves = resolve_map_from_expr_info(&ei); + let ip = build_interprocedural_cfg(source.root(), Some(&resolves)); + let callee = ip + .cfgs + .decl_body(def_ident.span()) + .expect("callee body for declaration"); + assert!( + ip.calls.iter().any(|e| e.callee_body == callee), + "expected a call edge into the let-bound closure body, got {:#?}", + ip.calls + ); +} + fn source_at(path: &str, text: &str) -> Source { let id = FileId::new(None, VirtualPath::new(Path::new(path))); Source::new(id, text.to_owned())