mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: type check across modules (#232)
This commit is contained in:
parent
0b566f83de
commit
46f524de57
21 changed files with 230 additions and 63 deletions
|
@ -29,6 +29,7 @@ toml.workspace = true
|
|||
walkdir.workspace = true
|
||||
indexmap.workspace = true
|
||||
ecow.workspace = true
|
||||
siphasher.workspace = true
|
||||
|
||||
typst.workspace = true
|
||||
|
||||
|
|
|
@ -97,6 +97,21 @@ impl DefUseInfo {
|
|||
pub fn is_exported(&self, id: DefId) -> bool {
|
||||
self.exports_refs.contains(&id)
|
||||
}
|
||||
|
||||
/// Get the definition id of an exported symbol by its name.
|
||||
pub fn dep_hash(&self, fid: TypstFileId) -> u128 {
|
||||
use siphasher::sip128::Hasher128;
|
||||
let mut hasher = reflexo::hash::FingerprintSipHasherBase::default();
|
||||
for (dep_fid, def) in self.ident_defs.keys() {
|
||||
if fid != *dep_fid {
|
||||
continue;
|
||||
}
|
||||
fid.hash(&mut hasher);
|
||||
def.hash(&mut hasher);
|
||||
}
|
||||
|
||||
hasher.finish128().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_def_use_inner(
|
||||
|
|
|
@ -122,6 +122,7 @@ struct ComputingNode<Inputs, Output> {
|
|||
name: &'static str,
|
||||
computing: AtomicBool,
|
||||
inputs: RwLock<Option<Inputs>>,
|
||||
slow_validate: RwLock<Option<u128>>,
|
||||
output: RwLock<Option<Output>>,
|
||||
}
|
||||
|
||||
|
@ -162,6 +163,7 @@ impl<Inputs, Output> ComputingNode<Inputs, Output> {
|
|||
name,
|
||||
computing: AtomicBool::new(false),
|
||||
inputs: RwLock::new(None),
|
||||
slow_validate: RwLock::new(None),
|
||||
output: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +173,32 @@ impl<Inputs, Output> ComputingNode<Inputs, Output> {
|
|||
inputs: Inputs,
|
||||
compute: impl FnOnce(Option<Inputs>, Inputs) -> Option<Output>,
|
||||
) -> Result<Option<Output>, ()>
|
||||
where
|
||||
Inputs: ComputeDebug + Hash + Clone,
|
||||
Output: Clone,
|
||||
{
|
||||
self.compute_(inputs, Option::<fn() -> u128>::None, compute)
|
||||
}
|
||||
|
||||
fn compute_with_validate(
|
||||
&self,
|
||||
inputs: Inputs,
|
||||
slow_validate: impl FnOnce() -> u128,
|
||||
compute: impl FnOnce(Option<Inputs>, Inputs) -> Option<Output>,
|
||||
) -> Result<Option<Output>, ()>
|
||||
where
|
||||
Inputs: ComputeDebug + Hash + Clone,
|
||||
Output: Clone,
|
||||
{
|
||||
self.compute_(inputs, Some(slow_validate), compute)
|
||||
}
|
||||
|
||||
fn compute_(
|
||||
&self,
|
||||
inputs: Inputs,
|
||||
slow_validate: Option<impl FnOnce() -> u128>,
|
||||
compute: impl FnOnce(Option<Inputs>, Inputs) -> Option<Output>,
|
||||
) -> Result<Option<Output>, ()>
|
||||
where
|
||||
Inputs: ComputeDebug + Hash + Clone,
|
||||
Output: Clone,
|
||||
|
@ -183,12 +211,16 @@ impl<Inputs, Output> ComputingNode<Inputs, Output> {
|
|||
}
|
||||
let input_cmp = self.inputs.read();
|
||||
let res = Ok(match input_cmp.as_ref() {
|
||||
Some(s) if reflexo::hash::hash128(&inputs) == reflexo::hash::hash128(&s) => {
|
||||
Some(s)
|
||||
if reflexo::hash::hash128(&inputs) == reflexo::hash::hash128(&s)
|
||||
&& self.is_slow_validated(slow_validate) =>
|
||||
{
|
||||
log::debug!(
|
||||
"{}({:?}): hit cache",
|
||||
self.name,
|
||||
inputs.compute_debug_repr()
|
||||
);
|
||||
|
||||
self.output.read().clone()
|
||||
}
|
||||
s => {
|
||||
|
@ -206,6 +238,23 @@ impl<Inputs, Output> ComputingNode<Inputs, Output> {
|
|||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
res
|
||||
}
|
||||
|
||||
fn is_slow_validated(&self, slow_validate: Option<impl FnOnce() -> u128>) -> bool {
|
||||
if let Some(slow_validate) = slow_validate {
|
||||
let res = slow_validate();
|
||||
if self
|
||||
.slow_validate
|
||||
.read()
|
||||
.as_ref()
|
||||
.map_or(true, |e| *e != res)
|
||||
{
|
||||
*self.slow_validate.write() = Some(res);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache for module-level analysis results of a module.
|
||||
|
@ -518,15 +567,20 @@ impl<'w> AnalysisContext<'w> {
|
|||
if let Some(res) = self.caches.modules.entry(fid).or_default().type_check() {
|
||||
return Some(res);
|
||||
}
|
||||
let def_use = self.def_use(source.clone());
|
||||
|
||||
let cache = self.at_module(fid);
|
||||
|
||||
let tl = cache.type_check.clone();
|
||||
let res = tl
|
||||
.compute(source, |_before, after| {
|
||||
let next = crate::analysis::ty::type_check(self, after);
|
||||
next.or_else(|| tl.output.read().clone())
|
||||
})
|
||||
.compute_with_validate(
|
||||
source,
|
||||
|| def_use.map(|s| s.dep_hash(fid)).unwrap_or_default(),
|
||||
|_before, after| {
|
||||
let next = crate::analysis::ty::type_check(self, after);
|
||||
next.or_else(|| tl.output.read().clone())
|
||||
},
|
||||
)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ pub(crate) fn type_check(ctx: &mut AnalysisContext, source: Source) -> Option<Ar
|
|||
source: source.clone(),
|
||||
def_use_info,
|
||||
info: &mut info,
|
||||
externals: HashMap::new(),
|
||||
mode: InterpretMode::Markup,
|
||||
};
|
||||
let lnk = LinkedNode::new(source.root());
|
||||
|
@ -85,17 +86,6 @@ impl TypeCheckInfo {
|
|||
worker.simplify(ty, principal)
|
||||
}
|
||||
|
||||
pub fn var_at(
|
||||
&mut self,
|
||||
var_site: Span,
|
||||
def_id: DefId,
|
||||
f: impl FnOnce() -> FlowVar,
|
||||
) -> &mut FlowVar {
|
||||
let var = self.vars.entry(def_id).or_insert_with(f);
|
||||
Self::witness_(var_site, var.get_ref(), &mut self.mapping);
|
||||
var
|
||||
}
|
||||
|
||||
// todo: distinguish at least, at most
|
||||
pub fn witness_at_least(&mut self, site: Span, ty: FlowType) {
|
||||
Self::witness_(site, ty, &mut self.mapping);
|
||||
|
@ -141,6 +131,7 @@ struct TypeChecker<'a, 'w> {
|
|||
def_use_info: Arc<DefUseInfo>,
|
||||
|
||||
info: &'a mut TypeCheckInfo,
|
||||
externals: HashMap<DefId, Option<FlowType>>,
|
||||
mode: InterpretMode,
|
||||
}
|
||||
|
||||
|
@ -328,12 +319,11 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
|
|||
range: root.range(),
|
||||
};
|
||||
|
||||
let Some(def_id) = self.def_use_info.get_ref(&ident_ref) else {
|
||||
let Some(var) = self.get_var(root.span(), ident_ref) else {
|
||||
let s = root.span();
|
||||
let v = resolve_global_value(self.ctx, root, mode == InterpretMode::Math)?;
|
||||
return Some(FlowType::Value(Box::new((v, s))));
|
||||
};
|
||||
let var = self.info.vars.get(&def_id)?.clone();
|
||||
|
||||
Some(var.get_ref())
|
||||
}
|
||||
|
@ -707,24 +697,48 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
|
|||
.get_ref(&r)
|
||||
.or_else(|| Some(self.def_use_info.get_def(s.id()?, &r)?.0))?;
|
||||
|
||||
Some(self.info.var_at(s, def_id, || {
|
||||
// let store = FlowVarStore {
|
||||
// name: r.name.into(),
|
||||
// id: def_id,
|
||||
// lbs: Vec::new(),
|
||||
// ubs: Vec::new(),
|
||||
// };
|
||||
// FlowVar(Arc::new(RwLock::new(store)))
|
||||
FlowVar {
|
||||
name: r.name.into(),
|
||||
id: def_id,
|
||||
kind: FlowVarKind::Weak(Arc::new(RwLock::new(FlowVarStore {
|
||||
lbs: Vec::new(),
|
||||
ubs: Vec::new(),
|
||||
}))),
|
||||
// kind: FlowVarKind::Strong(FlowType::Any),
|
||||
}
|
||||
}))
|
||||
// todo: false positive of clippy
|
||||
#[allow(clippy::map_entry)]
|
||||
if !self.info.vars.contains_key(&def_id) {
|
||||
let def = self.check_external(def_id);
|
||||
let kind = FlowVarKind::Weak(Arc::new(RwLock::new(self.init_var(def))));
|
||||
self.info.vars.insert(
|
||||
def_id,
|
||||
FlowVar {
|
||||
name: r.name.into(),
|
||||
id: def_id,
|
||||
kind,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let var = self.info.vars.get_mut(&def_id).unwrap();
|
||||
TypeCheckInfo::witness_(s, var.get_ref(), &mut self.info.mapping);
|
||||
Some(var)
|
||||
}
|
||||
|
||||
fn check_external(&mut self, def_id: DefId) -> Option<FlowType> {
|
||||
if let Some(ty) = self.externals.get(&def_id) {
|
||||
return ty.clone();
|
||||
}
|
||||
|
||||
let (def_id, def_pos) = self.def_use_info.get_def_by_id(def_id)?;
|
||||
if def_id == self.source.id() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source = self.ctx.source_by_id(def_id).ok()?;
|
||||
let ext_def_use_info = self.ctx.def_use(source.clone())?;
|
||||
let ext_type_info = self.ctx.type_check(source)?;
|
||||
let (ext_def_id, _) = ext_def_use_info.get_def(
|
||||
def_id,
|
||||
&IdentRef {
|
||||
name: def_pos.name.clone(),
|
||||
range: def_pos.range.clone(),
|
||||
},
|
||||
)?;
|
||||
let ext_ty = ext_type_info.vars.get(&ext_def_id)?.get_ref();
|
||||
Some(ext_type_info.simplify(ext_ty, false))
|
||||
}
|
||||
|
||||
fn check_pattern(
|
||||
|
@ -1203,6 +1217,39 @@ impl<'a, 'w> TypeChecker<'a, 'w> {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_var(&mut self, def: Option<FlowType>) -> FlowVarStore {
|
||||
let mut store = FlowVarStore {
|
||||
lbs: vec![],
|
||||
ubs: vec![],
|
||||
};
|
||||
|
||||
let Some(def) = def else {
|
||||
return store;
|
||||
};
|
||||
|
||||
match def {
|
||||
FlowType::Var(v) => {
|
||||
let w = self.info.vars.get(&v.0).unwrap();
|
||||
match &w.kind {
|
||||
FlowVarKind::Weak(w) => {
|
||||
let w = w.read();
|
||||
store.lbs.extend(w.lbs.iter().cloned());
|
||||
store.ubs.extend(w.ubs.iter().cloned());
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowType::Let(v) => {
|
||||
store.lbs.extend(v.lbs.iter().cloned());
|
||||
store.ubs.extend(v.ubs.iter().cloned());
|
||||
}
|
||||
_ => {
|
||||
store.ubs.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
store
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/completion.rs
|
||||
description: Completion on / (364..365)
|
||||
description: Completion on / (360..361)
|
||||
expression: "JsonRepr::new_pure(results)"
|
||||
input_file: crates/tinymist-query/src/fixtures/completion/func_args2.typ
|
||||
---
|
||||
|
|
|
@ -4,4 +4,4 @@ description: "Check on \")\" (69)"
|
|||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/text_stroke2.typ
|
||||
---
|
||||
( ⪰ Any ⪯ Stroke)
|
||||
( ⪰ ( ⪰ Any ⪯ Stroke) | ( ⪰ Any ⪯ Stroke))
|
||||
|
|
|
@ -4,4 +4,4 @@ description: "Check on \"(\" (48)"
|
|||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/text_stroke4.typ
|
||||
---
|
||||
( ⪰ Any ⪯ Stroke)
|
||||
( ⪰ ( ⪰ Any ⪯ Stroke) | ( ⪰ Any ⪯ Stroke))
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
description: "Check on \"(\" (56)"
|
||||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/user_external.typ
|
||||
---
|
||||
( ⪯ TextFont & TextFont)
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/analysis.rs
|
||||
description: "Check on \"(\" (59)"
|
||||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/user_external_alias.typ
|
||||
---
|
||||
( ⪯ TextFont & TextFont)
|
|
@ -4,4 +4,4 @@ description: "Check on \"(\" (105)"
|
|||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/user_func.typ
|
||||
---
|
||||
TextFont
|
||||
( ⪯ TextFont & TextFont)
|
||||
|
|
|
@ -4,4 +4,4 @@ description: "Check on \")\" (98)"
|
|||
expression: literal_type
|
||||
input_file: crates/tinymist-query/src/fixtures/post_type_check/user_named.typ
|
||||
---
|
||||
( ⪰ ( ⪰ Any | None) | "font": Any)
|
||||
( ⪰ ( ⪰ Any | None) | "font": Any | ( ⪰ Any | None) | "font": Any)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// path: base.typ
|
||||
#let tmpl(content, font: none) = {
|
||||
set text(font: font)
|
||||
|
||||
content
|
||||
}
|
||||
-----
|
||||
#import "base.typ": *
|
||||
|
||||
#tmpl(font: /* position after */ ("Test",))[]
|
|
@ -0,0 +1,10 @@
|
|||
// path: base.typ
|
||||
#let tmpl(content, font: none) = {
|
||||
set text(font: font)
|
||||
|
||||
content
|
||||
}
|
||||
-----
|
||||
#import "base.typ": tmpl
|
||||
|
||||
#tmpl(font: /* position after */ ("Test",))[]
|
|
@ -8,4 +8,4 @@ input_file: crates/tinymist-query/src/fixtures/type_check/set_font.typ
|
|||
5..9 -> @font
|
||||
36..40 -> Func(text)
|
||||
41..51 -> (TextFont | Array<TextFont>)
|
||||
47..51 -> (TextFont | Array<TextFont>)
|
||||
47..51 -> (@font | (TextFont | Array<TextFont>))
|
||||
|
|
|
@ -8,3 +8,4 @@ input_file: crates/tinymist-query/src/fixtures/type_check/sig_template.typ
|
|||
---
|
||||
5..9 -> @tmpl
|
||||
10..17 -> @content
|
||||
21..28 -> @content
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -25,11 +25,11 @@ input_file: crates/tinymist-query/src/fixtures/type_check/text_font.typ
|
|||
82..86 -> Func(text)
|
||||
82..97 -> Element(text)
|
||||
87..94 -> (TextFont | Array<TextFont>)
|
||||
93..94 -> (TextFont | Array<TextFont>)
|
||||
93..94 -> (@x | (TextFont | Array<TextFont>))
|
||||
95..97 -> Type(content)
|
||||
103..104 -> @y
|
||||
118..122 -> Func(text)
|
||||
118..133 -> Element(text)
|
||||
123..130 -> (TextFont | Array<TextFont>)
|
||||
129..130 -> (TextFont | Array<TextFont>)
|
||||
129..130 -> (@y | (TextFont | Array<TextFont>))
|
||||
131..133 -> Type(content)
|
||||
|
|
|
@ -9,6 +9,8 @@ input_file: crates/tinymist-query/src/fixtures/type_check/with.typ
|
|||
---
|
||||
5..6 -> @f
|
||||
7..8 -> @x
|
||||
12..13 -> @x
|
||||
20..21 -> @g
|
||||
24..25 -> @f
|
||||
24..30 -> (@x) -> @x
|
||||
24..33 -> ((@x) -> @x).with(..[&(1)])
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::collections::{BTreeMap, HashSet};
|
|||
use ecow::{eco_format, EcoString};
|
||||
use lsp_types::{CompletionItem, CompletionTextEdit, InsertTextFormat, TextEdit};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use reflexo::path::{unix_slash, PathClean};
|
||||
use typst::foundations::{AutoValue, Func, Label, NoneValue, Type, Value};
|
||||
use typst::layout::{Dir, Length};
|
||||
|
@ -437,6 +438,8 @@ fn type_completion(
|
|||
label: f.clone(),
|
||||
apply: Some(eco_format!("{}: ${{}}", f)),
|
||||
detail: docs.map(Into::into),
|
||||
// todo: only vscode and neovim (0.9.1) support this
|
||||
command: Some("editor.action.triggerSuggest"),
|
||||
..Completion::default()
|
||||
});
|
||||
}
|
||||
|
@ -791,7 +794,7 @@ pub fn complete_literal(ctx: &mut CompletionContext) -> Option<()> {
|
|||
struct LitComplWorker<'a, 'b, 'w> {
|
||||
ctx: &'a mut CompletionContext<'b, 'w>,
|
||||
dict_lit: ast::Dict<'a>,
|
||||
existing: &'a OnceCell<HashSet<EcoString>>,
|
||||
existing: &'a OnceCell<Mutex<HashSet<EcoString>>>,
|
||||
}
|
||||
|
||||
let mut ctx = LitComplWorker {
|
||||
|
@ -808,26 +811,29 @@ pub fn complete_literal(ctx: &mut CompletionContext) -> Option<()> {
|
|||
}
|
||||
LitComplAction::Dict(dict_iface) => {
|
||||
let existing = self.existing.get_or_init(|| {
|
||||
self.dict_lit
|
||||
.items()
|
||||
.filter_map(|field| match field {
|
||||
ast::DictItem::Named(n) => Some(n.name().get().clone()),
|
||||
ast::DictItem::Keyed(k) => {
|
||||
let key = self.ctx.ctx.const_eval(k.key());
|
||||
if let Some(Value::Str(key)) = key {
|
||||
return Some(key.into());
|
||||
}
|
||||
Mutex::new(
|
||||
self.dict_lit
|
||||
.items()
|
||||
.filter_map(|field| match field {
|
||||
ast::DictItem::Named(n) => Some(n.name().get().clone()),
|
||||
ast::DictItem::Keyed(k) => {
|
||||
let key = self.ctx.ctx.const_eval(k.key());
|
||||
if let Some(Value::Str(key)) = key {
|
||||
return Some(key.into());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
// todo: var dict union
|
||||
ast::DictItem::Spread(_s) => None,
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
None
|
||||
}
|
||||
// todo: var dict union
|
||||
ast::DictItem::Spread(_s) => None,
|
||||
})
|
||||
.collect::<HashSet<_>>(),
|
||||
)
|
||||
});
|
||||
let mut existing = existing.lock();
|
||||
|
||||
for (key, _, _) in dict_iface.fields.iter() {
|
||||
if existing.contains(key) {
|
||||
if !existing.insert(key.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue