feat(analysis): derive ipcfg resolve map from ExprInfo

This commit is contained in:
Hong Jiarong 2025-12-17 19:00:05 +08:00
parent 4fa2e7862d
commit 6f8a40cc0c
2 changed files with 141 additions and 1 deletions

View file

@ -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<CallEdge>,
}
/// 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<Span> {
let mut visited: FxHashSet<crate::ty::Interned<AnalysisRefExpr>> = FxHashSet::default();
let mut stack: Vec<Expr> = 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

View file

@ -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<ast::Ident<'_>> = None;
let mut use_ident: Option<ast::Ident<'_>> = 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<Span, crate::ty::Interned<RefExpr>> = 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())