feat: type check across modules (#232)

This commit is contained in:
Myriad-Dreamin 2024-05-05 17:19:56 +08:00 committed by GitHub
parent 0b566f83de
commit 46f524de57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 230 additions and 63 deletions

View file

@ -29,6 +29,7 @@ toml.workspace = true
walkdir.workspace = true
indexmap.workspace = true
ecow.workspace = true
siphasher.workspace = true
typst.workspace = true

View file

@ -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(

View file

@ -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();

View file

@ -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)]

View file

@ -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
---

View file

@ -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))

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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",))[]

View file

@ -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",))[]

View file

@ -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>))

View file

@ -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

View file

@ -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)

View file

@ -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)])

View file

@ -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;
}