mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
refactor: move expr and ty defs to analysis crate (#1633)
This commit is contained in:
parent
72e33e461d
commit
ac506dcc31
58 changed files with 968 additions and 890 deletions
|
@ -13,13 +13,30 @@ repository.workspace = true
|
|||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
comemo.workspace = true
|
||||
dashmap.workspace = true
|
||||
ecow.workspace = true
|
||||
ena.workspace = true
|
||||
hashbrown.workspace = true
|
||||
if_chain.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
rpds.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
serde.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
strum.workspace = true
|
||||
toml.workspace = true
|
||||
tinymist-derive.workspace = true
|
||||
tinymist-std.workspace = true
|
||||
tinymist-world.workspace = true
|
||||
toml.workspace = true
|
||||
triomphe.workspace = true
|
||||
typst.workspace = true
|
||||
typst-shim.workspace = true
|
||||
unscanny.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
|
411
crates/tinymist-analysis/src/adt/interner.rs
Normal file
411
crates/tinymist-analysis/src/adt/interner.rs
Normal file
|
@ -0,0 +1,411 @@
|
|||
//! Global `Arc`-based object interning infrastructure.
|
||||
//!
|
||||
//! Eventually this should probably be replaced with salsa-based interning.
|
||||
//!
|
||||
//! todo: This is less efficient as the arc object will change its reference
|
||||
//! count every time it is cloned. todo: we may be able to optimize use by
|
||||
//! following approach:
|
||||
//! ```plain
|
||||
//! fn run_analyze(f) {
|
||||
//! let local = thread_local_intern();
|
||||
//! let res = f(local);
|
||||
//! std::thread::spawn(move || gc(local));
|
||||
//! return res
|
||||
//! }
|
||||
//! ```
|
||||
//! However, this is out of scope for now.
|
||||
|
||||
use std::{
|
||||
fmt::{self, Debug, Display},
|
||||
hash::{BuildHasherDefault, Hash, Hasher},
|
||||
ops::Deref,
|
||||
sync::{LazyLock, OnceLock},
|
||||
};
|
||||
|
||||
use dashmap::{DashMap, SharedValue};
|
||||
use ecow::{EcoString, EcoVec};
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHasher;
|
||||
use triomphe::Arc;
|
||||
use typst::{foundations::Str, syntax::ast::Ident};
|
||||
|
||||
type InternMap<T> = DashMap<Arc<T>, (), BuildHasherDefault<FxHasher>>;
|
||||
type Guard<T> = dashmap::RwLockWriteGuard<
|
||||
'static,
|
||||
HashMap<Arc<T>, SharedValue<()>, BuildHasherDefault<FxHasher>>,
|
||||
>;
|
||||
|
||||
// https://news.ycombinator.com/item?id=22220342
|
||||
|
||||
pub struct Interned<T: Internable + ?Sized> {
|
||||
arc: Arc<T>,
|
||||
}
|
||||
|
||||
impl<T: Internable> Interned<T> {
|
||||
pub fn new(obj: T) -> Self {
|
||||
let (mut shard, hash) = Self::select(&obj);
|
||||
// Atomically,
|
||||
// - check if `obj` is already in the map
|
||||
// - if so, clone its `Arc` and return it
|
||||
// - if not, box it up, insert it, and return a clone
|
||||
// This needs to be atomic (locking the shard) to avoid races with other thread,
|
||||
// which could insert the same object between us looking it up and
|
||||
// inserting it.
|
||||
match shard.raw_entry_mut().from_key_hashed_nocheck(hash, &obj) {
|
||||
RawEntryMut::Occupied(occ) => Self {
|
||||
arc: occ.key().clone(),
|
||||
},
|
||||
RawEntryMut::Vacant(vac) => {
|
||||
T::storage().alloc().increment();
|
||||
Self {
|
||||
arc: vac
|
||||
.insert_hashed_nocheck(hash, Arc::new(obj), SharedValue::new(()))
|
||||
.0
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: It is dangerous to keep interned object temporarily (u128)
|
||||
// Case:
|
||||
// ```
|
||||
// insert(hash(Interned::new_str("a"))) == true
|
||||
// insert(hash(Interned::new_str("a"))) == true
|
||||
// ```
|
||||
impl Interned<str> {
|
||||
pub fn new_str(s: &str) -> Self {
|
||||
let (mut shard, hash) = Self::select(s);
|
||||
// Atomically,
|
||||
// - check if `obj` is already in the map
|
||||
// - if so, clone its `Arc` and return it
|
||||
// - if not, box it up, insert it, and return a clone
|
||||
// This needs to be atomic (locking the shard) to avoid races with other thread,
|
||||
// which could insert the same object between us looking it up and
|
||||
// inserting it.
|
||||
match shard.raw_entry_mut().from_key_hashed_nocheck(hash, s) {
|
||||
RawEntryMut::Occupied(occ) => Self {
|
||||
arc: occ.key().clone(),
|
||||
},
|
||||
RawEntryMut::Vacant(vac) => {
|
||||
str::storage().alloc().increment();
|
||||
|
||||
Self {
|
||||
arc: vac
|
||||
.insert_hashed_nocheck(hash, Arc::from(s), SharedValue::new(()))
|
||||
.0
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static EMPTY: LazyLock<Interned<str>> = LazyLock::new(|| Interned::new_str(""));
|
||||
impl Default for Interned<str> {
|
||||
fn default() -> Self {
|
||||
EMPTY.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Interned<str> {
|
||||
pub fn empty() -> &'static Self {
|
||||
&EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Interned<str> {
|
||||
fn from(s: &str) -> Self {
|
||||
Interned::new_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Str> for Interned<str> {
|
||||
fn from(s: Str) -> Self {
|
||||
Interned::new_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EcoString> for Interned<str> {
|
||||
fn from(s: EcoString) -> Self {
|
||||
Interned::new_str(&s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EcoString> for Interned<str> {
|
||||
fn from(s: &EcoString) -> Self {
|
||||
Interned::new_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ident<'_>> for Interned<str> {
|
||||
fn from(s: Ident<'_>) -> Self {
|
||||
Interned::new_str(s.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Interned<str>> for EcoString {
|
||||
fn from(s: &Interned<str>) -> Self {
|
||||
s.as_ref().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable> From<T> for Interned<T> {
|
||||
fn from(s: T) -> Self {
|
||||
Interned::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + Clone> From<&T> for Interned<T> {
|
||||
fn from(s: &T) -> Self {
|
||||
Interned::new(s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Interned<T> {
|
||||
#[inline]
|
||||
fn select(obj: &T) -> (Guard<T>, u64) {
|
||||
let storage = T::storage().get();
|
||||
let hash = {
|
||||
let mut hasher = std::hash::BuildHasher::build_hasher(storage.hasher());
|
||||
obj.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
let shard_idx = storage.determine_shard(hash as usize);
|
||||
let shard = &storage.shards()[shard_idx];
|
||||
(shard.write(), hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Drop for Interned<T> {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
// When the last `Ref` is dropped, remove the object from the global map.
|
||||
if Arc::count(&self.arc) == 2 {
|
||||
// Only `self` and the global map point to the object.
|
||||
|
||||
self.drop_slow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Interned<T> {
|
||||
#[cold]
|
||||
fn drop_slow(&mut self) {
|
||||
let (mut shard, hash) = Self::select(&self.arc);
|
||||
|
||||
if Arc::count(&self.arc) != 2 {
|
||||
// Another thread has interned another copy
|
||||
return;
|
||||
}
|
||||
|
||||
match shard
|
||||
.raw_entry_mut()
|
||||
.from_key_hashed_nocheck(hash, &self.arc)
|
||||
{
|
||||
RawEntryMut::Occupied(occ) => occ.remove(),
|
||||
RawEntryMut::Vacant(_) => unreachable!(),
|
||||
};
|
||||
|
||||
T::storage().alloc().decrement();
|
||||
|
||||
// Shrink the backing storage if the shard is less than 50% occupied.
|
||||
if shard.len() * 2 < shard.capacity() {
|
||||
shard.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares interned `Ref`s using pointer equality.
|
||||
impl<T: Internable> PartialEq for Interned<T> {
|
||||
// NOTE: No `?Sized` because `ptr_eq` doesn't work right with trait objects.
|
||||
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.arc, &other.arc)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable> Eq for Interned<T> {}
|
||||
|
||||
impl<T: Internable + PartialOrd> PartialOrd for Interned<T> {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.as_ref().partial_cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + Ord> Ord for Interned<T> {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
if self == other {
|
||||
std::cmp::Ordering::Equal
|
||||
} else {
|
||||
self.as_ref().cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Interned<str> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Interned<str> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
if self == other {
|
||||
std::cmp::Ordering::Equal
|
||||
} else {
|
||||
self.as_ref().cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Interned<str> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.arc, &other.arc)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Interned<str> {}
|
||||
|
||||
impl serde::Serialize for Interned<str> {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.arc.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Interned<str> {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct StrVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for StrVisitor {
|
||||
type Value = Interned<str>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
Ok(Interned::new_str(v))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(StrVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Hash for Interned<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// NOTE: Cast disposes vtable pointer / slice/str length.
|
||||
state.write_usize(Arc::as_ptr(&self.arc) as *const () as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> AsRef<T> for Interned<T> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.arc
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Deref for Interned<T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.arc
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> Clone for Interned<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
arc: self.arc.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug + Internable + ?Sized> Debug for Interned<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
(*self.arc).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display + Internable + ?Sized> Display for Interned<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
(*self.arc).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub static MAPS: Mutex<EcoVec<(&'static str, usize, Arc<AllocStats>)>> = Mutex::new(EcoVec::new());
|
||||
|
||||
pub struct InternStorage<T: ?Sized> {
|
||||
alloc: OnceLock<Arc<AllocStats>>,
|
||||
map: OnceLock<InternMap<T>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::new_without_default)] // this a const fn, so it can't be default
|
||||
impl<T: InternSize + ?Sized> InternStorage<T> {
|
||||
const SIZE: usize = T::INTERN_SIZE;
|
||||
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
alloc: OnceLock::new(),
|
||||
map: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> InternStorage<T> {
|
||||
fn alloc(&self) -> &Arc<AllocStats> {
|
||||
self.alloc.get_or_init(Arc::default)
|
||||
}
|
||||
|
||||
fn get(&self) -> &InternMap<T> {
|
||||
self.map.get_or_init(|| {
|
||||
MAPS.lock()
|
||||
.push((std::any::type_name::<T>(), Self::SIZE, self.alloc().clone()));
|
||||
DashMap::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InternSize {
|
||||
const INTERN_SIZE: usize;
|
||||
}
|
||||
|
||||
impl<T: Sized> InternSize for T {
|
||||
const INTERN_SIZE: usize = std::mem::size_of::<T>();
|
||||
}
|
||||
|
||||
impl InternSize for str {
|
||||
const INTERN_SIZE: usize = std::mem::size_of::<usize>() * 2;
|
||||
}
|
||||
|
||||
pub trait Internable: InternSize + Hash + Eq + 'static {
|
||||
fn storage() -> &'static InternStorage<Self>;
|
||||
}
|
||||
|
||||
/// Implements `Internable` for a given list of types, making them usable with
|
||||
/// `Interned`.
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! _impl_internable {
|
||||
( $($t:ty),+ $(,)? ) => { $(
|
||||
impl $crate::adt::interner::Internable for $t {
|
||||
fn storage() -> &'static $crate::adt::interner::InternStorage<Self> {
|
||||
static STORAGE: $crate::adt::interner::InternStorage<$t> = $crate::adt::interner::InternStorage::new();
|
||||
&STORAGE
|
||||
}
|
||||
}
|
||||
)+ };
|
||||
}
|
||||
|
||||
pub use crate::_impl_internable as impl_internable;
|
||||
use crate::stats::AllocStats;
|
||||
|
||||
impl_internable!(str,);
|
4
crates/tinymist-analysis/src/adt/mod.rs
Normal file
4
crates/tinymist-analysis/src/adt/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
pub mod interner;
|
||||
pub mod snapshot_map;
|
170
crates/tinymist-analysis/src/adt/snapshot_map.rs
Normal file
170
crates/tinymist-analysis/src/adt/snapshot_map.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
//! Upstream [rustc_data_structures::snapshot_map].
|
||||
//! Last checked commit: f4bb4500ddb4
|
||||
//! Last checked time: 2023-12-28
|
||||
//!
|
||||
//! [rustc_data_structures::snapshot_map]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_data_structures/src/snapshot_map/mod.rs
|
||||
|
||||
#![allow(missing_docs)]
|
||||
#![allow(unused)]
|
||||
|
||||
use ena::undo_log::{Rollback, Snapshots, UndoLogs, VecLog};
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
use std::hash::Hash;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops;
|
||||
|
||||
pub use ena::undo_log::Snapshot;
|
||||
|
||||
type FxHashMap<K, V> = rustc_hash::FxHashMap<K, V>;
|
||||
|
||||
pub type SnapshotMapStorage<K, V> = SnapshotMap<K, V, FxHashMap<K, V>, ()>;
|
||||
pub type SnapshotMapRef<'a, K, V, L> = SnapshotMap<K, V, &'a mut FxHashMap<K, V>, &'a mut L>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SnapshotMap<K, V, M = FxHashMap<K, V>, L = VecLog<UndoLog<K, V>>> {
|
||||
map: M,
|
||||
undo_log: L,
|
||||
_marker: PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
// HACK(eddyb) manual impl avoids `Default` bounds on `K` and `V`.
|
||||
impl<K, V, M, L> Default for SnapshotMap<K, V, M, L>
|
||||
where
|
||||
M: Default,
|
||||
L: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
SnapshotMap {
|
||||
map: Default::default(),
|
||||
undo_log: Default::default(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum UndoLog<K, V> {
|
||||
Inserted(K),
|
||||
Overwrite(K, V),
|
||||
Purged,
|
||||
}
|
||||
|
||||
impl<K, V, M, L> SnapshotMap<K, V, M, L> {
|
||||
#[inline]
|
||||
pub fn with_log<L2>(&mut self, undo_log: L2) -> SnapshotMap<K, V, &mut M, L2> {
|
||||
SnapshotMap {
|
||||
map: &mut self.map,
|
||||
undo_log,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, M, L> SnapshotMap<K, V, M, L>
|
||||
where
|
||||
K: Hash + Clone + Eq,
|
||||
M: BorrowMut<FxHashMap<K, V>> + Borrow<FxHashMap<K, V>>,
|
||||
L: UndoLogs<UndoLog<K, V>>,
|
||||
{
|
||||
pub fn clear(&mut self) {
|
||||
self.map.borrow_mut().clear();
|
||||
self.undo_log.clear();
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> impl Iterator<Item = (&K, &V)> {
|
||||
self.map.borrow().iter()
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &V> {
|
||||
self.map.borrow().values()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K, value: V) -> bool {
|
||||
match self.map.borrow_mut().insert(key.clone(), value) {
|
||||
None => {
|
||||
self.undo_log.push(UndoLog::Inserted(key));
|
||||
true
|
||||
}
|
||||
Some(old_value) => {
|
||||
self.undo_log.push(UndoLog::Overwrite(key, old_value));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: K) -> bool {
|
||||
match self.map.borrow_mut().remove(&key) {
|
||||
Some(old_value) => {
|
||||
self.undo_log.push(UndoLog::Overwrite(key, old_value));
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<Q>(&self, k: &Q) -> Option<&V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq + ?Sized,
|
||||
{
|
||||
self.map.borrow().get(k)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> SnapshotMap<K, V>
|
||||
where
|
||||
K: Hash + Clone + Eq,
|
||||
{
|
||||
pub fn snapshot(&mut self) -> Snapshot {
|
||||
self.undo_log.start_snapshot()
|
||||
}
|
||||
|
||||
pub fn commit(&mut self, snapshot: Snapshot) {
|
||||
self.undo_log.commit(snapshot)
|
||||
}
|
||||
|
||||
pub fn rollback_to(&mut self, snapshot: Snapshot) {
|
||||
let map = &mut self.map;
|
||||
self.undo_log.rollback_to(|| map, snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'k, K, V, M, L> ops::Index<&'k K> for SnapshotMap<K, V, M, L>
|
||||
where
|
||||
K: Hash + Clone + Eq,
|
||||
M: Borrow<FxHashMap<K, V>>,
|
||||
{
|
||||
type Output = V;
|
||||
fn index(&self, key: &'k K) -> &V {
|
||||
&self.map.borrow()[key]
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, M, L> Rollback<UndoLog<K, V>> for SnapshotMap<K, V, M, L>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
M: Rollback<UndoLog<K, V>>,
|
||||
{
|
||||
fn reverse(&mut self, undo: UndoLog<K, V>) {
|
||||
self.map.reverse(undo)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Rollback<UndoLog<K, V>> for FxHashMap<K, V>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
fn reverse(&mut self, undo: UndoLog<K, V>) {
|
||||
match undo {
|
||||
UndoLog::Inserted(key) => {
|
||||
self.remove(&key);
|
||||
}
|
||||
|
||||
UndoLog::Overwrite(key, old_value) => {
|
||||
self.insert(key, old_value);
|
||||
}
|
||||
|
||||
UndoLog::Purged => {}
|
||||
}
|
||||
}
|
||||
}
|
6
crates/tinymist-analysis/src/docs.rs
Normal file
6
crates/tinymist-analysis/src/docs.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
mod def;
|
||||
pub use def::*;
|
||||
mod tidy;
|
||||
pub use tidy::*;
|
296
crates/tinymist-analysis/src/docs/def.rs
Normal file
296
crates/tinymist-analysis/src/docs/def.rs
Normal file
|
@ -0,0 +1,296 @@
|
|||
use core::fmt;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::tidy::*;
|
||||
use crate::ty::{Interned, ParamAttrs, ParamTy, Ty};
|
||||
use crate::upstream::plain_docs_sentence;
|
||||
|
||||
type TypeRepr = Option<(
|
||||
/* short */ EcoString,
|
||||
/* long */ EcoString,
|
||||
/* value */ EcoString,
|
||||
)>;
|
||||
|
||||
/// Documentation about a definition (without type information).
|
||||
pub type UntypedDefDocs = DefDocsT<()>;
|
||||
/// Documentation about a definition.
|
||||
pub type DefDocs = DefDocsT<TypeRepr>;
|
||||
|
||||
/// Documentation about a definition.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum DefDocsT<T> {
|
||||
/// Documentation about a function.
|
||||
#[serde(rename = "func")]
|
||||
Function(Box<SignatureDocsT<T>>),
|
||||
/// Documentation about a variable.
|
||||
#[serde(rename = "var")]
|
||||
Variable(VarDocsT<T>),
|
||||
/// Documentation about a module.
|
||||
#[serde(rename = "module")]
|
||||
Module(TidyModuleDocs),
|
||||
/// Other kinds of documentation.
|
||||
#[serde(rename = "plain")]
|
||||
Plain {
|
||||
/// The content of the documentation.
|
||||
docs: EcoString,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T> DefDocsT<T> {
|
||||
/// Get the markdown representation of the documentation.
|
||||
pub fn docs(&self) -> &EcoString {
|
||||
match self {
|
||||
Self::Function(docs) => &docs.docs,
|
||||
Self::Variable(docs) => &docs.docs,
|
||||
Self::Module(docs) => &docs.docs,
|
||||
Self::Plain { docs } => docs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefDocs {
|
||||
/// Get full documentation for the signature.
|
||||
pub fn hover_docs(&self) -> EcoString {
|
||||
match self {
|
||||
DefDocs::Function(docs) => docs.hover_docs().clone(),
|
||||
_ => plain_docs_sentence(self.docs()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a primary function signature.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignatureDocsT<T> {
|
||||
/// Documentation for the function.
|
||||
pub docs: EcoString,
|
||||
/// The positional parameters.
|
||||
pub pos: Vec<ParamDocsT<T>>,
|
||||
/// The named parameters.
|
||||
pub named: BTreeMap<Interned<str>, ParamDocsT<T>>,
|
||||
/// The rest parameter.
|
||||
pub rest: Option<ParamDocsT<T>>,
|
||||
/// The return type.
|
||||
pub ret_ty: T,
|
||||
/// The full documentation for the signature.
|
||||
#[serde(skip)]
|
||||
pub hover_docs: OnceLock<EcoString>,
|
||||
}
|
||||
|
||||
impl SignatureDocsT<TypeRepr> {
|
||||
/// Get full documentation for the signature.
|
||||
pub fn hover_docs(&self) -> &EcoString {
|
||||
self.hover_docs
|
||||
.get_or_init(|| plain_docs_sentence(&format!("{}", SigHoverDocs(self))))
|
||||
}
|
||||
}
|
||||
|
||||
struct SigHoverDocs<'a>(&'a SignatureDocs);
|
||||
|
||||
impl fmt::Display for SigHoverDocs<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let docs = self.0;
|
||||
let base_docs = docs.docs.trim();
|
||||
|
||||
if !base_docs.is_empty() {
|
||||
f.write_str(base_docs)?;
|
||||
}
|
||||
|
||||
fn write_param_docs(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
docs: &ParamDocsT<TypeRepr>,
|
||||
kind: &str,
|
||||
is_first: &mut bool,
|
||||
) -> fmt::Result {
|
||||
if *is_first {
|
||||
*is_first = false;
|
||||
write!(f, "\n\n## {}\n\n", docs.name)?;
|
||||
} else {
|
||||
write!(f, "\n\n## {} ({kind})\n\n", docs.name)?;
|
||||
}
|
||||
|
||||
// p.cano_type.0
|
||||
if let Some(t) = &docs.cano_type {
|
||||
write!(f, "```typc\ntype: {}\n```\n\n", t.2)?;
|
||||
}
|
||||
|
||||
f.write_str(docs.docs.trim())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if !docs.pos.is_empty() {
|
||||
f.write_str("\n\n# Positional Parameters")?;
|
||||
|
||||
let mut is_first = true;
|
||||
for pos_docs in &docs.pos {
|
||||
write_param_docs(f, pos_docs, "positional", &mut is_first)?;
|
||||
}
|
||||
}
|
||||
|
||||
if docs.rest.is_some() {
|
||||
f.write_str("\n\n# Rest Parameters")?;
|
||||
|
||||
let mut is_first = true;
|
||||
if let Some(rest) = &docs.rest {
|
||||
write_param_docs(f, rest, "spread right", &mut is_first)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !docs.named.is_empty() {
|
||||
f.write_str("\n\n# Named Parameters")?;
|
||||
|
||||
let mut is_first = true;
|
||||
for named_docs in docs.named.values() {
|
||||
write_param_docs(f, named_docs, "named", &mut is_first)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation about a signature.
|
||||
pub type UntypedSignatureDocs = SignatureDocsT<()>;
|
||||
/// Documentation about a signature.
|
||||
pub type SignatureDocs = SignatureDocsT<TypeRepr>;
|
||||
|
||||
impl SignatureDocs {
|
||||
/// Get the markdown representation of the documentation.
|
||||
pub fn print(&self, f: &mut impl std::fmt::Write) -> fmt::Result {
|
||||
let mut is_first = true;
|
||||
let mut write_sep = |f: &mut dyn std::fmt::Write| {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
return f.write_str("\n ");
|
||||
}
|
||||
f.write_str(",\n ")
|
||||
};
|
||||
|
||||
f.write_char('(')?;
|
||||
for pos_docs in &self.pos {
|
||||
write_sep(f)?;
|
||||
f.write_str(&pos_docs.name)?;
|
||||
if let Some(t) = &pos_docs.cano_type {
|
||||
write!(f, ": {}", t.0)?;
|
||||
}
|
||||
}
|
||||
if let Some(rest) = &self.rest {
|
||||
write_sep(f)?;
|
||||
f.write_str("..")?;
|
||||
f.write_str(&rest.name)?;
|
||||
if let Some(t) = &rest.cano_type {
|
||||
write!(f, ": {}", t.0)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.named.is_empty() {
|
||||
let mut name_prints = vec![];
|
||||
for v in self.named.values() {
|
||||
let ty = v.cano_type.as_ref().map(|t| &t.0);
|
||||
name_prints.push((v.name.clone(), ty, v.default.clone()))
|
||||
}
|
||||
name_prints.sort();
|
||||
for (name, ty, val) in name_prints {
|
||||
write_sep(f)?;
|
||||
let val = val.as_deref().unwrap_or("any");
|
||||
let mut default = val.trim();
|
||||
if default.starts_with('{') && default.ends_with('}') && default.len() > 30 {
|
||||
default = "{ .. }"
|
||||
}
|
||||
if default.starts_with('`') && default.ends_with('`') && default.len() > 30 {
|
||||
default = "raw"
|
||||
}
|
||||
if default.starts_with('[') && default.ends_with(']') && default.len() > 30 {
|
||||
default = "content"
|
||||
}
|
||||
f.write_str(&name)?;
|
||||
if let Some(ty) = ty {
|
||||
write!(f, ": {ty}")?;
|
||||
}
|
||||
if default.contains('\n') {
|
||||
write!(f, " = {}", default.replace("\n", "\n "))?;
|
||||
} else {
|
||||
write!(f, " = {default}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_first {
|
||||
f.write_str(",\n")?;
|
||||
}
|
||||
f.write_char(')')?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation about a variable (without type information).
|
||||
pub type UntypedVarDocs = VarDocsT<()>;
|
||||
/// Documentation about a variable.
|
||||
pub type VarDocs = VarDocsT<Option<(EcoString, EcoString, EcoString)>>;
|
||||
|
||||
/// Describes a primary pattern binding.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VarDocsT<T> {
|
||||
/// Documentation for the pattern binding.
|
||||
pub docs: EcoString,
|
||||
/// The inferred type of the pattern binding source.
|
||||
pub return_ty: T,
|
||||
/// Cached documentation for the definition.
|
||||
#[serde(skip)]
|
||||
pub def_docs: OnceLock<String>,
|
||||
}
|
||||
|
||||
impl VarDocs {
|
||||
/// Get the markdown representation of the documentation.
|
||||
pub fn def_docs(&self) -> &String {
|
||||
self.def_docs
|
||||
.get_or_init(|| plain_docs_sentence(&self.docs).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation about a parameter (without type information).
|
||||
pub type TypelessParamDocs = ParamDocsT<()>;
|
||||
/// Documentation about a parameter.
|
||||
pub type ParamDocs = ParamDocsT<TypeRepr>;
|
||||
|
||||
/// Describes a function parameter.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ParamDocsT<T> {
|
||||
/// The parameter's name.
|
||||
pub name: Interned<str>,
|
||||
/// Documentation for the parameter.
|
||||
pub docs: EcoString,
|
||||
/// Inferred type of the parameter.
|
||||
pub cano_type: T,
|
||||
/// The parameter's default name as value.
|
||||
pub default: Option<EcoString>,
|
||||
/// The attribute of the parameter.
|
||||
#[serde(flatten)]
|
||||
pub attrs: ParamAttrs,
|
||||
}
|
||||
|
||||
impl ParamDocs {
|
||||
pub fn new(param: &ParamTy, ty: Option<&Ty>) -> Self {
|
||||
Self {
|
||||
name: param.name.as_ref().into(),
|
||||
docs: param.docs.clone().unwrap_or_default(),
|
||||
cano_type: format_ty(ty.or(Some(¶m.ty))),
|
||||
default: param.default.clone(),
|
||||
attrs: param.attrs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_ty(ty: Option<&Ty>) -> TypeRepr {
|
||||
let ty = ty?;
|
||||
let short = ty.repr().unwrap_or_else(|| "any".into());
|
||||
let long = eco_format!("{ty:?}");
|
||||
let value = ty.value_repr().unwrap_or_else(|| "".into());
|
||||
|
||||
Some((short, long, value))
|
||||
}
|
317
crates/tinymist-analysis/src/docs/tidy.rs
Normal file
317
crates/tinymist-analysis/src/docs/tidy.rs
Normal file
|
@ -0,0 +1,317 @@
|
|||
use ecow::EcoString;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typst::diag::StrResult;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TidyParamDocs {
|
||||
pub name: EcoString,
|
||||
pub docs: EcoString,
|
||||
pub types: EcoString,
|
||||
pub default: Option<EcoString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TidyPatDocs {
|
||||
pub docs: EcoString,
|
||||
pub return_ty: Option<EcoString>,
|
||||
pub params: Vec<TidyParamDocs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TidyModuleDocs {
|
||||
pub docs: EcoString,
|
||||
}
|
||||
|
||||
pub fn identify_pat_docs(converted: &str) -> StrResult<TidyPatDocs> {
|
||||
let lines = converted.lines().collect::<Vec<_>>();
|
||||
|
||||
let mut matching_return_ty = true;
|
||||
let mut buf = vec![];
|
||||
let mut params = vec![];
|
||||
let mut return_ty = None;
|
||||
let mut break_line = None;
|
||||
|
||||
let mut line_width = lines.len();
|
||||
'search: loop {
|
||||
if line_width == 0 {
|
||||
break;
|
||||
}
|
||||
line_width -= 1;
|
||||
|
||||
let line = lines[line_width];
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
loop {
|
||||
if matching_return_ty {
|
||||
matching_return_ty = false;
|
||||
let Some(w) = line.trim_start().strip_prefix("->") else {
|
||||
// break_line = Some(i);
|
||||
continue;
|
||||
};
|
||||
|
||||
break_line = Some(line_width);
|
||||
return_ty = Some(w.trim().into());
|
||||
break;
|
||||
}
|
||||
|
||||
let Some(mut line) = line
|
||||
.trim_end()
|
||||
.strip_suffix("<!-- typlite:end:list-item 0 -->")
|
||||
else {
|
||||
break_line = Some(line_width + 1);
|
||||
break 'search;
|
||||
};
|
||||
let mut current_line_no = line_width;
|
||||
|
||||
loop {
|
||||
// <!-- typlite:begin:list-item -->
|
||||
let t = line
|
||||
.trim_start()
|
||||
.strip_prefix("- ")
|
||||
.and_then(|t| t.trim().strip_prefix("<!-- typlite:begin:list-item 0 -->"));
|
||||
|
||||
let line_content = match t {
|
||||
Some(t) => {
|
||||
buf.push(t);
|
||||
break;
|
||||
}
|
||||
None => line,
|
||||
};
|
||||
|
||||
buf.push(line_content);
|
||||
|
||||
if current_line_no == 0 {
|
||||
break_line = Some(line_width + 1);
|
||||
break 'search;
|
||||
}
|
||||
current_line_no -= 1;
|
||||
line = lines[current_line_no];
|
||||
}
|
||||
|
||||
let mut buf = std::mem::take(&mut buf);
|
||||
buf.reverse();
|
||||
|
||||
let Some(first_line) = buf.first_mut() else {
|
||||
break_line = Some(line_width + 1);
|
||||
break 'search;
|
||||
};
|
||||
*first_line = first_line.trim();
|
||||
|
||||
let Some(param_line) = None.or_else(|| {
|
||||
let (param_name, rest) = first_line.split_once(" ")?;
|
||||
let (type_content, rest) = match_brace(rest.trim_start().strip_prefix("(")?)?;
|
||||
let (_, rest) = rest.split_once(":")?;
|
||||
*first_line = rest.trim();
|
||||
Some((param_name.into(), type_content.into()))
|
||||
}) else {
|
||||
break_line = Some(line_width + 1);
|
||||
break 'search;
|
||||
};
|
||||
|
||||
line_width = current_line_no;
|
||||
params.push(TidyParamDocs {
|
||||
name: param_line.0,
|
||||
types: param_line.1,
|
||||
default: None,
|
||||
docs: buf.into_iter().join("\n").into(),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let docs = match break_line {
|
||||
Some(line_no) => (lines[..line_no]).iter().copied().join("\n").into(),
|
||||
None => converted.into(),
|
||||
};
|
||||
|
||||
params.reverse();
|
||||
Ok(TidyPatDocs {
|
||||
docs,
|
||||
return_ty,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn identify_tidy_module_docs(docs: EcoString) -> StrResult<TidyModuleDocs> {
|
||||
Ok(TidyModuleDocs { docs })
|
||||
}
|
||||
|
||||
fn match_brace(trim_start: &str) -> Option<(&str, &str)> {
|
||||
let mut brace_count = 1;
|
||||
let mut end = 0;
|
||||
for (idx, ch) in trim_start.char_indices() {
|
||||
match ch {
|
||||
'(' => brace_count += 1,
|
||||
')' => brace_count -= 1,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if brace_count == 0 {
|
||||
end = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if brace_count != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (type_content, rest) = trim_start.split_at(end);
|
||||
Some((type_content, rest))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::TidyParamDocs;
|
||||
|
||||
fn func(s: &str) -> String {
|
||||
let docs = super::identify_pat_docs(s).unwrap();
|
||||
let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
|
||||
if let Some(t) = docs.return_ty {
|
||||
res.push_str(&format!("\n>>return\n{t}\n<<return"));
|
||||
}
|
||||
for TidyParamDocs {
|
||||
name,
|
||||
types,
|
||||
docs,
|
||||
default: _,
|
||||
} in docs.params
|
||||
{
|
||||
let _ = write!(res, "\n>>arg {name}: {types}\n{docs}\n<< arg");
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn var(s: &str) -> String {
|
||||
let docs = super::identify_pat_docs(s).unwrap();
|
||||
let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
|
||||
if let Some(t) = docs.return_ty {
|
||||
res.push_str(&format!("\n>>return\n{t}\n<<return"));
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identify_tidy_docs() {
|
||||
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||
|
||||
See @@show-module() for outputting the results of this function.
|
||||
|
||||
- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||
references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a
|
||||
functions are documented and fail if some are not.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available
|
||||
in all function and parameter descriptions.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`.
|
||||
This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 -->
|
||||
-> string"###), @r"
|
||||
>> docs:
|
||||
These again are dictionaries with the keys
|
||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
|
||||
|
||||
See @@show-module() for outputting the results of this function.
|
||||
<< docs
|
||||
>>return
|
||||
string
|
||||
<<return
|
||||
>>arg content: string
|
||||
Content of `.typ` file to analyze for docstrings.
|
||||
<< arg
|
||||
>>arg name: string
|
||||
The name for the module.
|
||||
<< arg
|
||||
>>arg label-prefix: auto, string
|
||||
The label-prefix for internal function
|
||||
references. If `auto`, the label-prefix name will be the module name.
|
||||
<< arg
|
||||
>>arg require-all-parameters: boolean
|
||||
Require that all parameters of a
|
||||
functions are documented and fail if some are not.
|
||||
<< arg
|
||||
>>arg scope: dictionary
|
||||
A dictionary of definitions that are then available
|
||||
in all function and parameter descriptions.
|
||||
<< arg
|
||||
>>arg preamble: string
|
||||
Code to prepend to all code snippets shown with `#example()`.
|
||||
This can for instance be used to import something from the scope.
|
||||
<< arg
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identify_tidy_docs_nested() {
|
||||
insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
|
||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||
|
||||
See @@show-module() for outputting the results of this function.
|
||||
|
||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||
- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function
|
||||
references. If `auto`, the label-prefix name will be the module name.
|
||||
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
||||
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
|
||||
-> string"###), @r"
|
||||
>> docs:
|
||||
These again are dictionaries with the keys
|
||||
- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
|
||||
|
||||
See @@show-module() for outputting the results of this function.
|
||||
<< docs
|
||||
>>return
|
||||
string
|
||||
<<return
|
||||
>>arg name: string
|
||||
The name for the module.
|
||||
<< arg
|
||||
>>arg label-prefix: auto, string
|
||||
The label-prefix for internal function
|
||||
references. If `auto`, the label-prefix name will be the module name.
|
||||
- <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
|
||||
- <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 -->
|
||||
<< arg
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identify_tidy_docs3() {
|
||||
insta::assert_snapshot!(var(r###"See @@show-module() for outputting the results of this function.
|
||||
-> string"###), @r"
|
||||
>> docs:
|
||||
See @@show-module() for outputting the results of this function.
|
||||
<< docs
|
||||
>>return
|
||||
string
|
||||
<<return
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identify_tidy_docs4() {
|
||||
insta::assert_snapshot!(var(r###"
|
||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||
-> string"###), @r"
|
||||
>> docs:
|
||||
|
||||
- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 -->
|
||||
<< docs
|
||||
>>return
|
||||
string
|
||||
<<return
|
||||
");
|
||||
}
|
||||
}
|
|
@ -1,8 +1,19 @@
|
|||
//! Tinymist Analysis
|
||||
|
||||
pub mod adt;
|
||||
pub mod docs;
|
||||
pub mod location;
|
||||
mod prelude;
|
||||
mod sig;
|
||||
pub mod stats;
|
||||
pub mod syntax;
|
||||
pub mod ty;
|
||||
pub mod upstream;
|
||||
|
||||
pub use sig::*;
|
||||
pub use track_values::*;
|
||||
|
||||
mod prelude;
|
||||
mod track_values;
|
||||
|
||||
/// Completely disabled log
|
||||
#[macro_export]
|
||||
|
|
377
crates/tinymist-analysis/src/sig.rs
Normal file
377
crates/tinymist-analysis/src/sig.rs
Normal file
|
@ -0,0 +1,377 @@
|
|||
//! Analysis of function signatures.
|
||||
|
||||
use core::fmt;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||
use typst::foundations::{Closure, Func};
|
||||
use typst::syntax::ast::AstNode;
|
||||
use typst::syntax::{ast, SyntaxKind};
|
||||
use typst::utils::LazyHash;
|
||||
|
||||
// use super::{BoundChecker, Definition};
|
||||
use crate::ty::{InsTy, ParamTy, SigTy, StrRef, Ty};
|
||||
use crate::ty::{Interned, ParamAttrs};
|
||||
use crate::upstream::truncated_repr;
|
||||
// use crate::upstream::truncated_repr;
|
||||
|
||||
/// Describes a function signature.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Signature {
|
||||
/// A primary function signature.
|
||||
Primary(Arc<PrimarySignature>),
|
||||
/// A partially applied function signature.
|
||||
Partial(Arc<PartialSignature>),
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
/// Returns the primary signature if it is one.
|
||||
pub fn primary(&self) -> &Arc<PrimarySignature> {
|
||||
match self {
|
||||
Signature::Primary(sig) => sig,
|
||||
Signature::Partial(sig) => &sig.signature,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the with bindings of the signature.
|
||||
pub fn bindings(&self) -> &[ArgsInfo] {
|
||||
match self {
|
||||
Signature::Primary(_) => &[],
|
||||
Signature::Partial(sig) => &sig.with_stack,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the all parameters of the signature.
|
||||
pub fn params(&self) -> impl Iterator<Item = (&Interned<ParamTy>, Option<&Ty>)> {
|
||||
let primary = self.primary().params();
|
||||
// todo: with stack
|
||||
primary
|
||||
}
|
||||
|
||||
/// Returns the type of the signature.
|
||||
pub fn type_sig(&self) -> Interned<SigTy> {
|
||||
let primary = self.primary().sig_ty.clone();
|
||||
// todo: with stack
|
||||
primary
|
||||
}
|
||||
|
||||
/// Returns the shift applied to the signature.
|
||||
pub fn param_shift(&self) -> usize {
|
||||
match self {
|
||||
Signature::Primary(_) => 0,
|
||||
Signature::Partial(sig) => sig
|
||||
.with_stack
|
||||
.iter()
|
||||
.map(|ws| ws.items.len())
|
||||
.sum::<usize>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a primary function signature.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrimarySignature {
|
||||
/// The documentation of the function
|
||||
pub docs: Option<EcoString>,
|
||||
/// The documentation of the parameter.
|
||||
pub param_specs: Vec<Interned<ParamTy>>,
|
||||
/// Whether the function has fill, stroke, or size parameters.
|
||||
pub has_fill_or_size_or_stroke: bool,
|
||||
/// The associated signature type.
|
||||
pub sig_ty: Interned<SigTy>,
|
||||
/// Whether the signature is broken.
|
||||
pub _broken: bool,
|
||||
}
|
||||
|
||||
impl PrimarySignature {
|
||||
/// Returns the number of positional parameters of the function.
|
||||
pub fn pos_size(&self) -> usize {
|
||||
self.sig_ty.name_started as usize
|
||||
}
|
||||
|
||||
/// Returns the positional parameters of the function.
|
||||
pub fn pos(&self) -> &[Interned<ParamTy>] {
|
||||
&self.param_specs[..self.pos_size()]
|
||||
}
|
||||
|
||||
/// Returns the positional parameters of the function.
|
||||
pub fn get_pos(&self, offset: usize) -> Option<&Interned<ParamTy>> {
|
||||
self.pos().get(offset)
|
||||
}
|
||||
|
||||
/// Returns the named parameters of the function.
|
||||
pub fn named(&self) -> &[Interned<ParamTy>] {
|
||||
&self.param_specs[self.pos_size()..self.pos_size() + self.sig_ty.names.names.len()]
|
||||
}
|
||||
|
||||
/// Returns the named parameters of the function.
|
||||
pub fn get_named(&self, name: &StrRef) -> Option<&Interned<ParamTy>> {
|
||||
self.named().get(self.sig_ty.names.find(name)?)
|
||||
}
|
||||
|
||||
/// Returns the name of the rest parameter of the function.
|
||||
pub fn has_spread_right(&self) -> bool {
|
||||
self.sig_ty.spread_right
|
||||
}
|
||||
|
||||
/// Returns the rest parameter of the function.
|
||||
pub fn rest(&self) -> Option<&Interned<ParamTy>> {
|
||||
self.has_spread_right()
|
||||
.then(|| &self.param_specs[self.pos_size() + self.sig_ty.names.names.len()])
|
||||
}
|
||||
|
||||
/// Returns the all parameters of the function.
|
||||
pub fn params(&self) -> impl Iterator<Item = (&Interned<ParamTy>, Option<&Ty>)> {
|
||||
let pos = self.pos();
|
||||
let named = self.named();
|
||||
let rest = self.rest();
|
||||
let type_sig = &self.sig_ty;
|
||||
let pos = pos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, pos)| (pos, type_sig.pos(idx)));
|
||||
let named = named.iter().map(|x| (x, type_sig.named(&x.name)));
|
||||
let rest = rest.into_iter().map(|x| (x, type_sig.rest_param()));
|
||||
|
||||
pos.chain(named).chain(rest)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a function argument instance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArgInfo {
|
||||
/// The argument's name.
|
||||
pub name: Option<StrRef>,
|
||||
/// The argument's term.
|
||||
pub term: Option<Ty>,
|
||||
}
|
||||
|
||||
/// Describes a function argument list.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArgsInfo {
|
||||
/// The arguments.
|
||||
pub items: EcoVec<ArgInfo>,
|
||||
}
|
||||
|
||||
/// Describes a function signature that is already partially applied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PartialSignature {
|
||||
/// The positional parameters.
|
||||
pub signature: Arc<PrimarySignature>,
|
||||
/// The stack of `fn.with(..)` calls.
|
||||
pub with_stack: EcoVec<ArgsInfo>,
|
||||
}
|
||||
|
||||
/// Gets the signature of a function.
|
||||
#[comemo::memoize]
|
||||
pub fn func_signature(func: Func) -> Signature {
|
||||
use typst::foundations::func::Repr;
|
||||
let mut with_stack = eco_vec![];
|
||||
let mut func = func;
|
||||
while let Repr::With(with) = func.inner() {
|
||||
let (inner, args) = with.as_ref();
|
||||
with_stack.push(ArgsInfo {
|
||||
items: args
|
||||
.items
|
||||
.iter()
|
||||
.map(|arg| ArgInfo {
|
||||
name: arg.name.clone().map(From::from),
|
||||
term: Some(Ty::Value(InsTy::new(arg.value.v.clone()))),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
func = inner.clone();
|
||||
}
|
||||
|
||||
let mut pos_tys = vec![];
|
||||
let mut named_tys = Vec::new();
|
||||
let mut rest_ty = None;
|
||||
|
||||
let mut named_specs = BTreeMap::new();
|
||||
let mut param_specs = Vec::new();
|
||||
let mut rest_spec = None;
|
||||
|
||||
let mut broken = false;
|
||||
let mut has_fill_or_size_or_stroke = false;
|
||||
|
||||
let mut add_param = |param: Interned<ParamTy>| {
|
||||
let name = param.name.clone();
|
||||
if param.attrs.named {
|
||||
if matches!(name.as_ref(), "fill" | "stroke" | "size") {
|
||||
has_fill_or_size_or_stroke = true;
|
||||
}
|
||||
named_tys.push((name.clone(), param.ty.clone()));
|
||||
named_specs.insert(name.clone(), param.clone());
|
||||
}
|
||||
|
||||
if param.attrs.variadic {
|
||||
if rest_ty.is_some() {
|
||||
broken = true;
|
||||
} else {
|
||||
rest_ty = Some(param.ty.clone());
|
||||
rest_spec = Some(param);
|
||||
}
|
||||
} else if param.attrs.positional {
|
||||
// todo: we have some params that are both positional and named
|
||||
pos_tys.push(param.ty.clone());
|
||||
param_specs.push(param);
|
||||
}
|
||||
};
|
||||
|
||||
let ret_ty = match func.inner() {
|
||||
Repr::With(..) => unreachable!(),
|
||||
Repr::Closure(closure) => {
|
||||
analyze_closure_signature(closure.clone(), &mut add_param);
|
||||
None
|
||||
}
|
||||
Repr::Element(..) | Repr::Native(..) | Repr::Plugin(..) => {
|
||||
for param in func.params().unwrap_or_default() {
|
||||
add_param(Interned::new(ParamTy {
|
||||
name: param.name.into(),
|
||||
docs: Some(param.docs.into()),
|
||||
default: param.default.map(|default| truncated_repr(&default())),
|
||||
ty: Ty::from_param_site(&func, param),
|
||||
attrs: param.into(),
|
||||
}));
|
||||
}
|
||||
|
||||
func.returns().map(|r| Ty::from_return_site(&func, r))
|
||||
}
|
||||
};
|
||||
|
||||
let sig_ty = SigTy::new(pos_tys.into_iter(), named_tys, None, rest_ty, ret_ty);
|
||||
|
||||
for name in &sig_ty.names.names {
|
||||
let Some(param) = named_specs.get(name) else {
|
||||
continue;
|
||||
};
|
||||
param_specs.push(param.clone());
|
||||
}
|
||||
if let Some(doc) = rest_spec {
|
||||
param_specs.push(doc);
|
||||
}
|
||||
|
||||
let signature = Arc::new(PrimarySignature {
|
||||
docs: func.docs().map(From::from),
|
||||
param_specs,
|
||||
has_fill_or_size_or_stroke,
|
||||
sig_ty: sig_ty.into(),
|
||||
_broken: broken,
|
||||
});
|
||||
|
||||
log::trace!("got signature {signature:?}");
|
||||
|
||||
if with_stack.is_empty() {
|
||||
return Signature::Primary(signature);
|
||||
}
|
||||
|
||||
Signature::Partial(Arc::new(PartialSignature {
|
||||
signature,
|
||||
with_stack,
|
||||
}))
|
||||
}
|
||||
|
||||
fn analyze_closure_signature(
|
||||
closure: Arc<LazyHash<Closure>>,
|
||||
add_param: &mut impl FnMut(Interned<ParamTy>),
|
||||
) {
|
||||
log::trace!("closure signature for: {:?}", closure.node.kind());
|
||||
|
||||
let closure = &closure.node;
|
||||
let closure_ast = match closure.kind() {
|
||||
SyntaxKind::Closure => closure.cast::<ast::Closure>().unwrap(),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for param in closure_ast.params().children() {
|
||||
match param {
|
||||
ast::Param::Pos(pos) => {
|
||||
let name = format!("{}", PatternDisplay(&pos));
|
||||
add_param(Interned::new(ParamTy {
|
||||
name: name.as_str().into(),
|
||||
docs: None,
|
||||
default: None,
|
||||
ty: Ty::Any,
|
||||
attrs: ParamAttrs::positional(),
|
||||
}));
|
||||
}
|
||||
// todo: pattern
|
||||
ast::Param::Named(named) => {
|
||||
let default = unwrap_parens(named.expr()).to_untyped().clone().into_text();
|
||||
add_param(Interned::new(ParamTy {
|
||||
name: named.name().get().into(),
|
||||
docs: Some(eco_format!("Default value: {default}")),
|
||||
default: Some(default),
|
||||
ty: Ty::Any,
|
||||
attrs: ParamAttrs::named(),
|
||||
}));
|
||||
}
|
||||
ast::Param::Spread(spread) => {
|
||||
let sink = spread.sink_ident().map(|sink| sink.as_str());
|
||||
add_param(Interned::new(ParamTy {
|
||||
name: sink.unwrap_or_default().into(),
|
||||
docs: None,
|
||||
default: None,
|
||||
ty: Ty::Any,
|
||||
attrs: ParamAttrs::variadic(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PatternDisplay<'a>(&'a ast::Pattern<'a>);
|
||||
|
||||
impl fmt::Display for PatternDisplay<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
ast::Pattern::Normal(ast::Expr::Ident(ident)) => f.write_str(ident.as_str()),
|
||||
ast::Pattern::Normal(_) => f.write_str("?"), // unreachable?
|
||||
ast::Pattern::Placeholder(_) => f.write_str("_"),
|
||||
ast::Pattern::Parenthesized(paren_expr) => {
|
||||
write!(f, "{}", PatternDisplay(&paren_expr.pattern()))
|
||||
}
|
||||
ast::Pattern::Destructuring(destructing) => {
|
||||
write!(f, "(")?;
|
||||
let mut first = true;
|
||||
for item in destructing.items() {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
match item {
|
||||
ast::DestructuringItem::Pattern(pos) => {
|
||||
write!(f, "{}", PatternDisplay(&pos))?
|
||||
}
|
||||
ast::DestructuringItem::Named(named) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
named.name().as_str(),
|
||||
unwrap_parens(named.expr()).to_untyped().text()
|
||||
)?,
|
||||
ast::DestructuringItem::Spread(spread) => write!(
|
||||
f,
|
||||
"..{}",
|
||||
spread
|
||||
.sink_ident()
|
||||
.map(|sink| sink.as_str())
|
||||
.unwrap_or_default()
|
||||
)?,
|
||||
}
|
||||
}
|
||||
write!(f, ")")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_parens(mut expr: ast::Expr) -> ast::Expr {
|
||||
while let ast::Expr::Parenthesized(paren_expr) = expr {
|
||||
expr = paren_expr.expr();
|
||||
}
|
||||
|
||||
expr
|
||||
}
|
80
crates/tinymist-analysis/src/stats.rs
Normal file
80
crates/tinymist-analysis/src/stats.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
//! Tinymist Analysis Statistics
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// Statistics about the allocation
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AllocStats {
|
||||
/// The number of allocated objects.
|
||||
pub allocated: AtomicUsize,
|
||||
/// The number of dropped objects.
|
||||
pub dropped: AtomicUsize,
|
||||
}
|
||||
|
||||
impl AllocStats {
|
||||
/// increment the statistics.
|
||||
pub fn increment(&self) {
|
||||
self.allocated.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// decrement the statistics.
|
||||
pub fn decrement(&self) {
|
||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Report the statistics of the allocation.
|
||||
pub fn report() -> String {
|
||||
let maps = crate::adt::interner::MAPS.lock().clone();
|
||||
let mut data = Vec::new();
|
||||
for (name, sz, map) in maps {
|
||||
let allocated = map.allocated.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let dropped = map.dropped.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let alive = allocated.saturating_sub(dropped);
|
||||
data.push((name, sz * alive, allocated, dropped, alive));
|
||||
}
|
||||
|
||||
// sort by total
|
||||
data.sort_by(|x, y| y.4.cmp(&x.4));
|
||||
|
||||
// format to html
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str(r#"<div>
|
||||
<style>
|
||||
table.alloc-stats { width: 100%; border-collapse: collapse; }
|
||||
table.alloc-stats th, table.alloc-stats td { border: 1px solid black; padding: 8px; text-align: center; }
|
||||
table.alloc-stats th.name-column, table.alloc-stats td.name-column { text-align: left; }
|
||||
table.alloc-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
table.alloc-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
|
||||
}
|
||||
</style>
|
||||
<table class="alloc-stats"><tr><th class="name-column">Name</th><th>Alive</th><th>Allocated</th><th>Dropped</th><th>Size</th></tr>"#);
|
||||
|
||||
for (name, sz, allocated, dropped, alive) in data {
|
||||
html.push_str("<tr>");
|
||||
html.push_str(&format!(r#"<td class="name-column">{name}</td>"#));
|
||||
html.push_str(&format!("<td>{alive}</td>"));
|
||||
html.push_str(&format!("<td>{allocated}</td>"));
|
||||
html.push_str(&format!("<td>{dropped}</td>"));
|
||||
html.push_str(&format!("<td>{}</td>", human_size(sz)));
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
html.push_str("</table>");
|
||||
html.push_str("</div>");
|
||||
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
fn human_size(size: usize) -> String {
|
||||
let units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let mut unit = 0;
|
||||
let mut size = size as f64;
|
||||
while size >= 768.0 && unit < units.len() {
|
||||
size /= 1024.0;
|
||||
unit += 1;
|
||||
}
|
||||
format!("{:.2} {}", size, units[unit])
|
||||
}
|
|
@ -2,9 +2,17 @@
|
|||
//!
|
||||
//! This module must hide all **AST details** from the rest of the codebase.
|
||||
|
||||
// todo: remove this
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod import;
|
||||
pub use import::*;
|
||||
pub mod comment;
|
||||
pub use comment::*;
|
||||
pub mod matcher;
|
||||
pub use matcher::*;
|
||||
|
||||
pub mod def;
|
||||
pub use def::*;
|
||||
pub(crate) mod repr;
|
||||
use repr::*;
|
||||
|
|
991
crates/tinymist-analysis/src/syntax/def.rs
Normal file
991
crates/tinymist-analysis/src/syntax/def.rs
Normal file
|
@ -0,0 +1,991 @@
|
|||
use core::fmt;
|
||||
use std::{collections::BTreeMap, ops::Range};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_derive::DeclEnum;
|
||||
use tinymist_std::DefId;
|
||||
use tinymist_world::package::PackageSpec;
|
||||
use typst::{
|
||||
foundations::{Element, Func, Module, Type, Value},
|
||||
syntax::{Span, SyntaxNode},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
adt::interner::impl_internable,
|
||||
prelude::*,
|
||||
ty::{InsTy, Interned, SelectTy, Ty, TypeVar},
|
||||
};
|
||||
|
||||
use super::{ExprDescriber, ExprPrinter};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Expr {
|
||||
/// A sequence of expressions
|
||||
Block(Interned<Vec<Expr>>),
|
||||
/// An array literal
|
||||
Array(Interned<ArgsExpr>),
|
||||
/// A dict literal
|
||||
Dict(Interned<ArgsExpr>),
|
||||
/// An args literal
|
||||
Args(Interned<ArgsExpr>),
|
||||
/// A pattern
|
||||
Pattern(Interned<Pattern>),
|
||||
/// An element literal
|
||||
Element(Interned<ElementExpr>),
|
||||
/// An unary operation
|
||||
Unary(Interned<UnExpr>),
|
||||
/// A binary operation
|
||||
Binary(Interned<BinExpr>),
|
||||
/// A function call
|
||||
Apply(Interned<ApplyExpr>),
|
||||
/// A function
|
||||
Func(Interned<FuncExpr>),
|
||||
/// A let
|
||||
Let(Interned<LetExpr>),
|
||||
/// A show
|
||||
Show(Interned<ShowExpr>),
|
||||
/// A set
|
||||
Set(Interned<SetExpr>),
|
||||
/// A reference
|
||||
Ref(Interned<RefExpr>),
|
||||
/// A content reference
|
||||
ContentRef(Interned<ContentRefExpr>),
|
||||
/// A select
|
||||
Select(Interned<SelectExpr>),
|
||||
/// An import
|
||||
Import(Interned<ImportExpr>),
|
||||
/// An include
|
||||
Include(Interned<IncludeExpr>),
|
||||
/// A contextual
|
||||
Contextual(Interned<Expr>),
|
||||
/// A conditional
|
||||
Conditional(Interned<IfExpr>),
|
||||
/// A while loop
|
||||
WhileLoop(Interned<WhileExpr>),
|
||||
/// A for loop
|
||||
ForLoop(Interned<ForExpr>),
|
||||
/// A type
|
||||
Type(Ty),
|
||||
/// A declaration
|
||||
Decl(DeclExpr),
|
||||
/// A star import
|
||||
Star,
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
pub fn repr(&self) -> EcoString {
|
||||
let mut s = EcoString::new();
|
||||
let _ = ExprDescriber::new(&mut s).write_expr(self);
|
||||
s
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
Expr::Decl(decl) => decl.span(),
|
||||
Expr::Select(select) => select.span,
|
||||
Expr::Apply(apply) => apply.span,
|
||||
_ => Span::detached(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_id(&self) -> Option<TypstFileId> {
|
||||
match self {
|
||||
Expr::Decl(decl) => decl.file_id(),
|
||||
_ => self.span().id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Expr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
ExprPrinter::new(f).write_expr(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub type LexicalScope = rpds::RedBlackTreeMapSync<Interned<str>, Expr>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExprScope {
|
||||
Lexical(LexicalScope),
|
||||
Module(Module),
|
||||
Func(Func),
|
||||
Type(Type),
|
||||
}
|
||||
|
||||
impl ExprScope {
|
||||
pub fn empty() -> Self {
|
||||
ExprScope::Lexical(LexicalScope::default())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
ExprScope::Lexical(scope) => scope.is_empty(),
|
||||
ExprScope::Module(module) => is_empty_scope(module.scope()),
|
||||
ExprScope::Func(func) => func.scope().is_none_or(is_empty_scope),
|
||||
ExprScope::Type(ty) => is_empty_scope(ty.scope()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &Interned<str>) -> (Option<Expr>, Option<Ty>) {
|
||||
let (of, val) = match self {
|
||||
ExprScope::Lexical(scope) => {
|
||||
crate::log_debug_ct!("evaluating: {name:?} in {scope:?}");
|
||||
(scope.get(name).cloned(), None)
|
||||
}
|
||||
ExprScope::Module(module) => {
|
||||
let v = module.scope().get(name);
|
||||
// let decl =
|
||||
// v.and_then(|_| Some(Decl::external(module.file_id()?,
|
||||
// name.clone()).into()));
|
||||
(None, v)
|
||||
}
|
||||
ExprScope::Func(func) => (None, func.scope().unwrap().get(name)),
|
||||
ExprScope::Type(ty) => (None, ty.scope().get(name)),
|
||||
};
|
||||
|
||||
// ref_expr.of = of.clone();
|
||||
// ref_expr.val = val.map(|v| Ty::Value(InsTy::new(v.clone())));
|
||||
// return ref_expr;
|
||||
(
|
||||
of,
|
||||
val.cloned()
|
||||
.map(|val| Ty::Value(InsTy::new(val.read().to_owned()))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn merge_into(&self, exports: &mut LexicalScope) {
|
||||
match self {
|
||||
ExprScope::Lexical(scope) => {
|
||||
for (name, expr) in scope.iter() {
|
||||
exports.insert_mut(name.clone(), expr.clone());
|
||||
}
|
||||
}
|
||||
ExprScope::Module(module) => {
|
||||
crate::log_debug_ct!("imported: {module:?}");
|
||||
let v = Interned::new(Ty::Value(InsTy::new(Value::Module(module.clone()))));
|
||||
for (name, _) in module.scope().iter() {
|
||||
let name: Interned<str> = name.into();
|
||||
exports.insert_mut(name.clone(), select_of(v.clone(), name));
|
||||
}
|
||||
}
|
||||
ExprScope::Func(func) => {
|
||||
if let Some(scope) = func.scope() {
|
||||
let v = Interned::new(Ty::Value(InsTy::new(Value::Func(func.clone()))));
|
||||
for (name, _) in scope.iter() {
|
||||
let name: Interned<str> = name.into();
|
||||
exports.insert_mut(name.clone(), select_of(v.clone(), name));
|
||||
}
|
||||
}
|
||||
}
|
||||
ExprScope::Type(ty) => {
|
||||
let v = Interned::new(Ty::Value(InsTy::new(Value::Type(*ty))));
|
||||
for (name, _) in ty.scope().iter() {
|
||||
let name: Interned<str> = name.into();
|
||||
exports.insert_mut(name.clone(), select_of(v.clone(), name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_of(source: Interned<Ty>, name: Interned<str>) -> Expr {
|
||||
Expr::Type(Ty::Select(SelectTy::new(source, name)))
|
||||
}
|
||||
|
||||
/// Kind of a definition.
|
||||
#[derive(Debug, Default, Clone, Copy, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DefKind {
|
||||
/// A definition for some constant.
|
||||
#[default]
|
||||
Constant,
|
||||
/// A definition for some function.
|
||||
Function,
|
||||
/// A definition for some variable.
|
||||
Variable,
|
||||
/// A definition for some module.
|
||||
Module,
|
||||
/// A definition for some struct.
|
||||
Struct,
|
||||
/// A definition for some reference.
|
||||
Reference,
|
||||
}
|
||||
|
||||
impl fmt::Display for DefKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Constant => write!(f, "constant"),
|
||||
Self::Function => write!(f, "function"),
|
||||
Self::Variable => write!(f, "variable"),
|
||||
Self::Module => write!(f, "module"),
|
||||
Self::Struct => write!(f, "struct"),
|
||||
Self::Reference => write!(f, "reference"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DeclExpr = Interned<Decl>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash, DeclEnum)]
|
||||
pub enum Decl {
|
||||
Func(SpannedDecl),
|
||||
ImportAlias(SpannedDecl),
|
||||
Var(SpannedDecl),
|
||||
IdentRef(SpannedDecl),
|
||||
Module(ModuleDecl),
|
||||
ModuleAlias(SpannedDecl),
|
||||
PathStem(SpannedDecl),
|
||||
ImportPath(SpannedDecl),
|
||||
IncludePath(SpannedDecl),
|
||||
Import(SpannedDecl),
|
||||
ContentRef(SpannedDecl),
|
||||
Label(SpannedDecl),
|
||||
StrName(SpannedDecl),
|
||||
ModuleImport(SpanDecl),
|
||||
Closure(SpanDecl),
|
||||
Pattern(SpanDecl),
|
||||
Spread(SpanDecl),
|
||||
Content(SpanDecl),
|
||||
Constant(SpanDecl),
|
||||
BibEntry(NameRangeDecl),
|
||||
Docs(DocsDecl),
|
||||
Generated(GeneratedDecl),
|
||||
}
|
||||
|
||||
impl Decl {
|
||||
pub fn func(ident: ast::Ident) -> Self {
|
||||
Self::Func(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lit(name: &str) -> Self {
|
||||
Self::Var(SpannedDecl {
|
||||
name: name.into(),
|
||||
at: Span::detached(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lit_(name: Interned<str>) -> Self {
|
||||
Self::Var(SpannedDecl {
|
||||
name,
|
||||
at: Span::detached(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn var(ident: ast::Ident) -> Self {
|
||||
Self::Var(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn import_alias(ident: ast::Ident) -> Self {
|
||||
Self::ImportAlias(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ident_ref(ident: ast::Ident) -> Self {
|
||||
Self::IdentRef(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn math_ident_ref(ident: ast::MathIdent) -> Self {
|
||||
Self::IdentRef(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn module(name: Interned<str>, fid: TypstFileId) -> Self {
|
||||
Self::Module(ModuleDecl { name, fid })
|
||||
}
|
||||
|
||||
pub fn module_alias(ident: ast::Ident) -> Self {
|
||||
Self::ModuleAlias(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn import(ident: ast::Ident) -> Self {
|
||||
Self::Import(SpannedDecl {
|
||||
name: ident.get().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn label(name: &str, at: Span) -> Self {
|
||||
Self::Label(SpannedDecl {
|
||||
name: name.into(),
|
||||
at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ref_(ident: ast::Ref) -> Self {
|
||||
Self::ContentRef(SpannedDecl {
|
||||
name: ident.target().into(),
|
||||
at: ident.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn str_name(s: SyntaxNode, name: &str) -> Decl {
|
||||
Self::StrName(SpannedDecl {
|
||||
name: name.into(),
|
||||
at: s.span(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn calc_path_stem(s: &str) -> Interned<str> {
|
||||
use std::str::FromStr;
|
||||
let name = if s.starts_with('@') {
|
||||
let spec = PackageSpec::from_str(s).ok();
|
||||
spec.map(|spec| Interned::new_str(spec.name.as_str()))
|
||||
} else {
|
||||
let stem = Path::new(s).file_stem();
|
||||
stem.and_then(|stem| Some(Interned::new_str(stem.to_str()?)))
|
||||
};
|
||||
name.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn path_stem(s: SyntaxNode, name: Interned<str>) -> Self {
|
||||
Self::PathStem(SpannedDecl { name, at: s.span() })
|
||||
}
|
||||
|
||||
pub fn import_path(s: Span, name: Interned<str>) -> Self {
|
||||
Self::ImportPath(SpannedDecl { name, at: s })
|
||||
}
|
||||
|
||||
pub fn include_path(s: Span, name: Interned<str>) -> Self {
|
||||
Self::IncludePath(SpannedDecl { name, at: s })
|
||||
}
|
||||
|
||||
pub fn module_import(s: Span) -> Self {
|
||||
Self::ModuleImport(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn closure(s: Span) -> Self {
|
||||
Self::Closure(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn pattern(s: Span) -> Self {
|
||||
Self::Pattern(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn spread(s: Span) -> Self {
|
||||
Self::Spread(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn content(s: Span) -> Self {
|
||||
Self::Content(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn constant(s: Span) -> Self {
|
||||
Self::Constant(SpanDecl(s))
|
||||
}
|
||||
|
||||
pub fn docs(base: Interned<Decl>, var: Interned<TypeVar>) -> Self {
|
||||
Self::Docs(DocsDecl { base, var })
|
||||
}
|
||||
|
||||
pub fn generated(def_id: DefId) -> Self {
|
||||
Self::Generated(GeneratedDecl(def_id))
|
||||
}
|
||||
|
||||
pub fn bib_entry(
|
||||
name: Interned<str>,
|
||||
fid: TypstFileId,
|
||||
name_range: Range<usize>,
|
||||
range: Option<Range<usize>>,
|
||||
) -> Self {
|
||||
Self::BibEntry(NameRangeDecl {
|
||||
name,
|
||||
at: Box::new((fid, name_range, range)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_def(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Func(..)
|
||||
| Self::BibEntry(..)
|
||||
| Self::Closure(..)
|
||||
| Self::Var(..)
|
||||
| Self::Label(..)
|
||||
| Self::StrName(..)
|
||||
| Self::Module(..)
|
||||
| Self::ModuleImport(..)
|
||||
| Self::PathStem(..)
|
||||
| Self::ImportPath(..)
|
||||
| Self::IncludePath(..)
|
||||
| Self::Spread(..)
|
||||
| Self::Generated(..)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> DefKind {
|
||||
use Decl::*;
|
||||
match self {
|
||||
ModuleAlias(..) | Module(..) | PathStem(..) | ImportPath(..) | IncludePath(..) => {
|
||||
DefKind::Module
|
||||
}
|
||||
// Type(_) => DocStringKind::Struct,
|
||||
Func(..) | Closure(..) => DefKind::Function,
|
||||
Label(..) | BibEntry(..) | ContentRef(..) => DefKind::Reference,
|
||||
IdentRef(..) | ImportAlias(..) | Import(..) | Var(..) => DefKind::Variable,
|
||||
Pattern(..) | Docs(..) | Generated(..) | Constant(..) | StrName(..)
|
||||
| ModuleImport(..) | Content(..) | Spread(..) => DefKind::Constant,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets file location of the declaration.
|
||||
pub fn file_id(&self) -> Option<TypstFileId> {
|
||||
match self {
|
||||
Self::Module(ModuleDecl { fid, .. }) => Some(*fid),
|
||||
Self::BibEntry(NameRangeDecl { at, .. }) => Some(at.0),
|
||||
that => that.span().id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets full range of the declaration.
|
||||
pub fn full_range(&self) -> Option<Range<usize>> {
|
||||
if let Decl::BibEntry(decl) = self {
|
||||
return decl.at.2.clone();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn as_def(this: &Interned<Self>, val: Option<Ty>) -> Interned<RefExpr> {
|
||||
let def: Expr = this.clone().into();
|
||||
Interned::new(RefExpr {
|
||||
decl: this.clone(),
|
||||
step: Some(def.clone()),
|
||||
root: Some(def),
|
||||
term: val,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Decl {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let base = match (self, other) {
|
||||
(Self::Generated(l), Self::Generated(r)) => l.0 .0.cmp(&r.0 .0),
|
||||
(Self::Module(l), Self::Module(r)) => l.fid.cmp(&r.fid),
|
||||
(Self::Docs(l), Self::Docs(r)) => l.var.cmp(&r.var).then_with(|| l.base.cmp(&r.base)),
|
||||
_ => self.span().into_raw().cmp(&other.span().into_raw()),
|
||||
};
|
||||
|
||||
base.then_with(|| self.name().cmp(other.name()))
|
||||
}
|
||||
}
|
||||
|
||||
trait StrictCmp {
|
||||
/// Low-performance comparison but it is free from the concurrency issue.
|
||||
/// This is only used for making stable test snapshots.
|
||||
fn strict_cmp(&self, other: &Self) -> std::cmp::Ordering;
|
||||
}
|
||||
|
||||
impl Decl {
|
||||
pub fn strict_cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let base = match (self, other) {
|
||||
(Self::Generated(l), Self::Generated(r)) => l.0 .0.cmp(&r.0 .0),
|
||||
(Self::Module(l), Self::Module(r)) => l.fid.strict_cmp(&r.fid),
|
||||
(Self::Docs(l), Self::Docs(r)) => l
|
||||
.var
|
||||
.strict_cmp(&r.var)
|
||||
.then_with(|| l.base.strict_cmp(&r.base)),
|
||||
_ => self.span().strict_cmp(&other.span()),
|
||||
};
|
||||
|
||||
base.then_with(|| self.name().cmp(other.name()))
|
||||
}
|
||||
}
|
||||
|
||||
impl StrictCmp for TypstFileId {
|
||||
fn strict_cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.package()
|
||||
.map(ToString::to_string)
|
||||
.cmp(&other.package().map(ToString::to_string))
|
||||
.then_with(|| self.vpath().cmp(other.vpath()))
|
||||
}
|
||||
}
|
||||
impl<T: StrictCmp> StrictCmp for Option<T> {
|
||||
fn strict_cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(Some(l), Some(r)) => l.strict_cmp(r),
|
||||
(Some(_), None) => std::cmp::Ordering::Greater,
|
||||
(None, Some(_)) => std::cmp::Ordering::Less,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StrictCmp for Span {
|
||||
fn strict_cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.id()
|
||||
.strict_cmp(&other.id())
|
||||
.then_with(|| self.into_raw().cmp(&other.into_raw()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Decl {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Decl> for Expr {
|
||||
fn from(decl: Decl) -> Self {
|
||||
Expr::Decl(decl.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeclExpr> for Expr {
|
||||
fn from(decl: DeclExpr) -> Self {
|
||||
Expr::Decl(decl)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpannedDecl {
|
||||
name: Interned<str>,
|
||||
at: Span,
|
||||
}
|
||||
|
||||
impl SpannedDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
self.at
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SpannedDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.name.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NameRangeDecl {
|
||||
pub name: Interned<str>,
|
||||
pub at: Box<(TypstFileId, Range<usize>, Option<Range<usize>>)>,
|
||||
}
|
||||
|
||||
impl NameRangeDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Span::detached()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for NameRangeDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.name.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ModuleDecl {
|
||||
pub name: Interned<str>,
|
||||
pub fid: TypstFileId,
|
||||
}
|
||||
|
||||
impl ModuleDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Span::detached()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ModuleDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.name.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DocsDecl {
|
||||
base: Interned<Decl>,
|
||||
var: Interned<TypeVar>,
|
||||
}
|
||||
|
||||
impl DocsDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
Interned::empty()
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Span::detached()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DocsDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}, {:?}", self.base, self.var)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SpanDecl(Span);
|
||||
|
||||
impl SpanDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
Interned::empty()
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SpanDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "..")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub struct GeneratedDecl(DefId);
|
||||
|
||||
impl GeneratedDecl {
|
||||
fn name(&self) -> &Interned<str> {
|
||||
Interned::empty()
|
||||
}
|
||||
|
||||
fn span(&self) -> Span {
|
||||
Span::detached()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for GeneratedDecl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub type UnExpr = UnInst<Expr>;
|
||||
pub type BinExpr = BinInst<Expr>;
|
||||
|
||||
pub type ExportMap = BTreeMap<Interned<str>, Expr>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ArgExpr {
|
||||
Pos(Expr),
|
||||
Named(Box<(DeclExpr, Expr)>),
|
||||
NamedRt(Box<(Expr, Expr)>),
|
||||
Spread(Expr),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Pattern {
|
||||
Expr(Expr),
|
||||
Simple(Interned<Decl>),
|
||||
Sig(Box<PatternSig>),
|
||||
}
|
||||
|
||||
impl fmt::Display for Pattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
ExprPrinter::new(f).write_pattern(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn repr(&self) -> EcoString {
|
||||
let mut s = EcoString::new();
|
||||
let _ = ExprDescriber::new(&mut s).write_pattern(self);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PatternSig {
|
||||
pub pos: EcoVec<Interned<Pattern>>,
|
||||
pub named: EcoVec<(DeclExpr, Interned<Pattern>)>,
|
||||
pub spread_left: Option<(DeclExpr, Interned<Pattern>)>,
|
||||
pub spread_right: Option<(DeclExpr, Interned<Pattern>)>,
|
||||
}
|
||||
|
||||
impl Pattern {}
|
||||
|
||||
impl_internable!(Decl,);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ContentSeqExpr {
|
||||
pub ty: Ty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct RefExpr {
|
||||
pub decl: DeclExpr,
|
||||
pub step: Option<Expr>,
|
||||
pub root: Option<Expr>,
|
||||
pub term: Option<Ty>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ContentRefExpr {
|
||||
pub ident: DeclExpr,
|
||||
pub of: Option<DeclExpr>,
|
||||
pub body: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SelectExpr {
|
||||
pub lhs: Expr,
|
||||
pub key: DeclExpr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl SelectExpr {
|
||||
pub fn new(key: DeclExpr, lhs: Expr) -> Interned<Self> {
|
||||
Interned::new(Self {
|
||||
key,
|
||||
lhs,
|
||||
span: Span::detached(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ArgsExpr {
|
||||
pub args: Vec<ArgExpr>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl ArgsExpr {
|
||||
pub fn new(span: Span, args: Vec<ArgExpr>) -> Interned<Self> {
|
||||
Interned::new(Self { args, span })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ElementExpr {
|
||||
pub elem: Element,
|
||||
pub content: EcoVec<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ApplyExpr {
|
||||
pub callee: Expr,
|
||||
pub args: Expr,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FuncExpr {
|
||||
pub decl: DeclExpr,
|
||||
pub params: PatternSig,
|
||||
pub body: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LetExpr {
|
||||
/// Span of the pattern
|
||||
pub span: Span,
|
||||
pub pattern: Interned<Pattern>,
|
||||
pub body: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ShowExpr {
|
||||
pub selector: Option<Expr>,
|
||||
pub edit: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SetExpr {
|
||||
pub target: Expr,
|
||||
pub args: Expr,
|
||||
pub cond: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ImportExpr {
|
||||
pub decl: Interned<RefExpr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct IncludeExpr {
|
||||
pub source: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct IfExpr {
|
||||
pub cond: Expr,
|
||||
pub then: Expr,
|
||||
pub else_: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct WhileExpr {
|
||||
pub cond: Expr,
|
||||
pub body: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ForExpr {
|
||||
pub pattern: Interned<Pattern>,
|
||||
pub iter: Expr,
|
||||
pub body: Expr,
|
||||
}
|
||||
|
||||
/// The kind of unary operation
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum UnaryOp {
|
||||
/// The (arithmetic) positive operation
|
||||
/// `+t`
|
||||
Pos,
|
||||
/// The (arithmetic) negate operation
|
||||
/// `-t`
|
||||
Neg,
|
||||
/// The (logical) not operation
|
||||
/// `not t`
|
||||
Not,
|
||||
/// The return operation
|
||||
/// `return t`
|
||||
Return,
|
||||
/// The typst context operation
|
||||
/// `context t`
|
||||
Context,
|
||||
/// The spreading operation
|
||||
/// `..t`
|
||||
Spread,
|
||||
/// The not element of operation
|
||||
/// `not in t`
|
||||
NotElementOf,
|
||||
/// The element of operation
|
||||
/// `in t`
|
||||
ElementOf,
|
||||
/// The type of operation
|
||||
/// `type(t)`
|
||||
TypeOf,
|
||||
}
|
||||
|
||||
/// A unary operation type
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub struct UnInst<T> {
|
||||
/// The operand of the unary operation
|
||||
pub lhs: T,
|
||||
/// The kind of the unary operation
|
||||
pub op: UnaryOp,
|
||||
}
|
||||
|
||||
impl<T: Ord> PartialOrd for UnInst<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for UnInst<T> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let op_as_int = self.op as u8;
|
||||
let other_op_as_int = other.op as u8;
|
||||
op_as_int
|
||||
.cmp(&other_op_as_int)
|
||||
.then_with(|| self.lhs.cmp(&other.lhs))
|
||||
}
|
||||
}
|
||||
|
||||
impl UnInst<Expr> {
|
||||
/// Create a unary operation type
|
||||
pub fn new(op: UnaryOp, lhs: Expr) -> Interned<Self> {
|
||||
Interned::new(Self { lhs, op })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UnInst<T> {
|
||||
/// Get the operands of the unary operation
|
||||
pub fn operands(&self) -> [&T; 1] {
|
||||
[&self.lhs]
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of binary operation
|
||||
pub type BinaryOp = ast::BinOp;
|
||||
|
||||
/// A binary operation type
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub struct BinInst<T> {
|
||||
/// The operands of the binary operation
|
||||
pub operands: (T, T),
|
||||
/// The kind of the binary operation
|
||||
pub op: BinaryOp,
|
||||
}
|
||||
|
||||
impl<T: Ord> PartialOrd for BinInst<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for BinInst<T> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
let op_as_int = self.op as u8;
|
||||
let other_op_as_int = other.op as u8;
|
||||
op_as_int
|
||||
.cmp(&other_op_as_int)
|
||||
.then_with(|| self.operands.cmp(&other.operands))
|
||||
}
|
||||
}
|
||||
|
||||
impl BinInst<Expr> {
|
||||
/// Create a binary operation type
|
||||
pub fn new(op: BinaryOp, lhs: Expr, rhs: Expr) -> Interned<Self> {
|
||||
Interned::new(Self {
|
||||
operands: (lhs, rhs),
|
||||
op,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BinInst<T> {
|
||||
/// Get the operands of the binary operation
|
||||
pub fn operands(&self) -> [&T; 2] {
|
||||
[&self.operands.0, &self.operands.1]
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_scope(scope: &typst::foundations::Scope) -> bool {
|
||||
scope.iter().next().is_none()
|
||||
}
|
||||
|
||||
impl_internable!(
|
||||
Expr,
|
||||
ArgsExpr,
|
||||
ElementExpr,
|
||||
ContentSeqExpr,
|
||||
RefExpr,
|
||||
ContentRefExpr,
|
||||
SelectExpr,
|
||||
ImportExpr,
|
||||
IncludeExpr,
|
||||
IfExpr,
|
||||
WhileExpr,
|
||||
ForExpr,
|
||||
FuncExpr,
|
||||
LetExpr,
|
||||
ShowExpr,
|
||||
SetExpr,
|
||||
Pattern,
|
||||
EcoVec<(Decl, Expr)>,
|
||||
Vec<ArgExpr>,
|
||||
Vec<Expr>,
|
||||
UnInst<Expr>,
|
||||
BinInst<Expr>,
|
||||
ApplyExpr,
|
||||
);
|
607
crates/tinymist-analysis/src/syntax/repr.rs
Normal file
607
crates/tinymist-analysis/src/syntax/repr.rs
Normal file
|
@ -0,0 +1,607 @@
|
|||
use core::fmt;
|
||||
|
||||
use super::def::*;
|
||||
use crate::ty::{Interned, Ty};
|
||||
|
||||
pub(in crate::syntax) struct ExprPrinter<'a, T: fmt::Write> {
|
||||
f: &'a mut T,
|
||||
indent: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: fmt::Write> ExprPrinter<'a, T> {
|
||||
pub fn new(f: &'a mut T) -> Self {
|
||||
Self { f, indent: 0 }
|
||||
}
|
||||
|
||||
pub fn write_decl(&mut self, decl: &Decl) -> fmt::Result {
|
||||
write!(self.f, "{decl:?}")
|
||||
}
|
||||
|
||||
pub fn write_expr(&mut self, expr: &Expr) -> fmt::Result {
|
||||
match expr {
|
||||
Expr::Block(exprs) => self.write_seq(exprs),
|
||||
Expr::Array(elems) => self.write_array(&elems.args),
|
||||
Expr::Dict(elems) => self.write_dict(&elems.args),
|
||||
Expr::Args(args) => self.write_args(&args.args),
|
||||
Expr::Pattern(pat) => self.write_pattern(pat),
|
||||
Expr::Element(elem) => self.write_element(elem),
|
||||
Expr::Unary(unary) => self.write_unary(unary),
|
||||
Expr::Binary(binary) => self.write_binary(binary),
|
||||
Expr::Apply(apply) => self.write_apply(apply),
|
||||
Expr::Func(func) => self.write_func(func),
|
||||
Expr::Let(let_expr) => self.write_let(let_expr),
|
||||
Expr::Show(show) => self.write_show(show),
|
||||
Expr::Set(set) => self.write_set(set),
|
||||
Expr::Ref(reference) => self.write_ref(reference),
|
||||
Expr::ContentRef(content_ref) => self.write_content_ref(content_ref),
|
||||
Expr::Select(sel) => self.write_select(sel),
|
||||
Expr::Import(import) => self.write_import(import),
|
||||
Expr::Include(include) => self.write_include(include),
|
||||
Expr::Contextual(contextual) => self.write_contextual(contextual),
|
||||
Expr::Conditional(if_expr) => self.write_conditional(if_expr),
|
||||
Expr::WhileLoop(while_expr) => self.write_while_loop(while_expr),
|
||||
Expr::ForLoop(for_expr) => self.write_for_loop(for_expr),
|
||||
Expr::Type(ty) => self.write_type(ty),
|
||||
Expr::Decl(decl) => self.write_decl(decl),
|
||||
Expr::Star => self.write_star(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_indent(&mut self) -> fmt::Result {
|
||||
write!(self.f, "{:indent$}", "", indent = self.indent)
|
||||
}
|
||||
|
||||
fn write_seq(&mut self, exprs: &Interned<Vec<Expr>>) -> fmt::Result {
|
||||
writeln!(self.f, "[")?;
|
||||
self.indent += 1;
|
||||
for expr in exprs.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_expr(expr)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
write!(self.f, "]")
|
||||
}
|
||||
|
||||
fn write_array(&mut self, elems: &[ArgExpr]) -> fmt::Result {
|
||||
writeln!(self.f, "(")?;
|
||||
self.indent += 1;
|
||||
for arg in elems.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_dict(&mut self, elems: &[ArgExpr]) -> fmt::Result {
|
||||
writeln!(self.f, "(:")?;
|
||||
self.indent += 1;
|
||||
for arg in elems.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_args(&mut self, args: &[ArgExpr]) -> fmt::Result {
|
||||
writeln!(self.f, "(")?;
|
||||
for arg in args.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_arg(&mut self, arg: &ArgExpr) -> fmt::Result {
|
||||
match arg {
|
||||
ArgExpr::Pos(pos) => self.write_expr(pos),
|
||||
ArgExpr::Named(named) => {
|
||||
let (name, val) = named.as_ref();
|
||||
write!(self.f, "{name:?}: ")?;
|
||||
self.write_expr(val)
|
||||
}
|
||||
ArgExpr::NamedRt(named) => {
|
||||
let (key, val) = named.as_ref();
|
||||
self.write_expr(key)?;
|
||||
write!(self.f, ": ")?;
|
||||
self.write_expr(val)
|
||||
}
|
||||
ArgExpr::Spread(spread) => {
|
||||
write!(self.f, "..")?;
|
||||
self.write_expr(spread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_pattern(&mut self, pat: &Pattern) -> fmt::Result {
|
||||
match pat {
|
||||
Pattern::Expr(expr) => self.write_expr(expr),
|
||||
Pattern::Simple(decl) => self.write_decl(decl),
|
||||
Pattern::Sig(sig) => self.write_pattern_sig(sig),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pattern_sig(&mut self, sig: &PatternSig) -> fmt::Result {
|
||||
self.f.write_str("pat(\n")?;
|
||||
self.indent += 1;
|
||||
for pos in &sig.pos {
|
||||
self.write_indent()?;
|
||||
self.write_pattern(pos)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
for (name, named) in &sig.named {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "{name:?} = ")?;
|
||||
self.write_pattern(named)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
if let Some((name, spread_left)) = &sig.spread_left {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "..{name:?}: ")?;
|
||||
self.write_pattern(spread_left)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
if let Some((name, spread_right)) = &sig.spread_right {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "..{name:?}: ")?;
|
||||
self.write_pattern(spread_right)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_element(&mut self, elem: &Interned<ElementExpr>) -> fmt::Result {
|
||||
self.f.write_str("elem(\n")?;
|
||||
self.indent += 1;
|
||||
for v in &elem.content {
|
||||
self.write_indent()?;
|
||||
self.write_expr(v)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_unary(&mut self, unary: &Interned<UnExpr>) -> fmt::Result {
|
||||
write!(self.f, "un({:?})(", unary.op)?;
|
||||
self.write_expr(&unary.lhs)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_binary(&mut self, binary: &Interned<BinExpr>) -> fmt::Result {
|
||||
let [lhs, rhs] = binary.operands();
|
||||
write!(self.f, "bin({:?})(", binary.op)?;
|
||||
self.write_expr(lhs)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(rhs)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_apply(&mut self, apply: &Interned<ApplyExpr>) -> fmt::Result {
|
||||
write!(self.f, "apply(")?;
|
||||
self.write_expr(&apply.callee)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(&apply.args)?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_func(&mut self, func: &Interned<FuncExpr>) -> fmt::Result {
|
||||
write!(self.f, "func[{:?}](", func.decl)?;
|
||||
self.write_pattern_sig(&func.params)?;
|
||||
write!(self.f, " = ")?;
|
||||
self.write_expr(&func.body)?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_let(&mut self, let_expr: &Interned<LetExpr>) -> fmt::Result {
|
||||
write!(self.f, "let(")?;
|
||||
self.write_pattern(&let_expr.pattern)?;
|
||||
if let Some(body) = &let_expr.body {
|
||||
write!(self.f, " = ")?;
|
||||
self.write_expr(body)?;
|
||||
}
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_show(&mut self, show: &Interned<ShowExpr>) -> fmt::Result {
|
||||
write!(self.f, "show(")?;
|
||||
if let Some(selector) = &show.selector {
|
||||
self.write_expr(selector)?;
|
||||
self.f.write_str(", ")?;
|
||||
}
|
||||
self.write_expr(&show.edit)?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_set(&mut self, set: &Interned<SetExpr>) -> fmt::Result {
|
||||
write!(self.f, "set(")?;
|
||||
self.write_expr(&set.target)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(&set.args)?;
|
||||
if let Some(cond) = &set.cond {
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(cond)?;
|
||||
}
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_ref(&mut self, reference: &Interned<RefExpr>) -> fmt::Result {
|
||||
write!(self.f, "ref({:?}", reference.decl)?;
|
||||
if let Some(step) = &reference.step {
|
||||
self.f.write_str(", step = ")?;
|
||||
self.write_expr(step)?;
|
||||
}
|
||||
if let Some(of) = &reference.root {
|
||||
self.f.write_str(", root = ")?;
|
||||
self.write_expr(of)?;
|
||||
}
|
||||
if let Some(val) = &reference.term {
|
||||
write!(self.f, ", val = {val:?}")?;
|
||||
}
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_content_ref(&mut self, content_ref: &Interned<ContentRefExpr>) -> fmt::Result {
|
||||
write!(self.f, "content_ref({:?}", content_ref.ident)?;
|
||||
if let Some(of) = &content_ref.of {
|
||||
self.f.write_str(", ")?;
|
||||
self.write_decl(of)?;
|
||||
}
|
||||
if let Some(val) = &content_ref.body {
|
||||
self.write_expr(val)?;
|
||||
}
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_select(&mut self, sel: &Interned<SelectExpr>) -> fmt::Result {
|
||||
write!(self.f, "(")?;
|
||||
self.write_expr(&sel.lhs)?;
|
||||
self.f.write_str(").")?;
|
||||
self.write_decl(&sel.key)
|
||||
}
|
||||
|
||||
fn write_import(&mut self, import: &Interned<ImportExpr>) -> fmt::Result {
|
||||
self.f.write_str("import(")?;
|
||||
self.write_decl(&import.decl.decl)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_include(&mut self, include: &Interned<IncludeExpr>) -> fmt::Result {
|
||||
self.f.write_str("include(")?;
|
||||
self.write_expr(&include.source)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_contextual(&mut self, contextual: &Interned<Expr>) -> fmt::Result {
|
||||
self.f.write_str("contextual(")?;
|
||||
self.write_expr(contextual)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_conditional(&mut self, if_expr: &Interned<IfExpr>) -> fmt::Result {
|
||||
self.f.write_str("if(")?;
|
||||
self.write_expr(&if_expr.cond)?;
|
||||
self.f.write_str(", then = ")?;
|
||||
self.write_expr(&if_expr.then)?;
|
||||
self.f.write_str(", else = ")?;
|
||||
self.write_expr(&if_expr.else_)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_while_loop(&mut self, while_expr: &Interned<WhileExpr>) -> fmt::Result {
|
||||
self.f.write_str("while(")?;
|
||||
self.write_expr(&while_expr.cond)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(&while_expr.body)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_for_loop(&mut self, for_expr: &Interned<ForExpr>) -> fmt::Result {
|
||||
self.f.write_str("for(")?;
|
||||
self.write_pattern(&for_expr.pattern)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(&for_expr.iter)?;
|
||||
self.f.write_str(", ")?;
|
||||
self.write_expr(&for_expr.body)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_type(&mut self, ty: &Ty) -> fmt::Result {
|
||||
let formatted = ty.describe();
|
||||
let formatted = formatted.as_deref().unwrap_or("any");
|
||||
self.f.write_str(formatted)
|
||||
}
|
||||
|
||||
fn write_star(&mut self) -> fmt::Result {
|
||||
self.f.write_str("*")
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::syntax) struct ExprDescriber<'a, T: fmt::Write> {
|
||||
f: &'a mut T,
|
||||
indent: usize,
|
||||
}
|
||||
|
||||
impl<'a, T: fmt::Write> ExprDescriber<'a, T> {
|
||||
pub fn new(f: &'a mut T) -> Self {
|
||||
Self { f, indent: 0 }
|
||||
}
|
||||
|
||||
pub fn write_decl(&mut self, decl: &Decl) -> fmt::Result {
|
||||
use DefKind::*;
|
||||
let shorter = matches!(decl.kind(), Function | Variable | Module);
|
||||
if shorter && !decl.name().is_empty() {
|
||||
return write!(self.f, "{}", decl.name());
|
||||
}
|
||||
|
||||
write!(self.f, "{decl:?}")
|
||||
}
|
||||
|
||||
pub fn write_expr(&mut self, expr: &Expr) -> fmt::Result {
|
||||
match expr {
|
||||
Expr::Block(..) => self.f.write_str("Expr(..)"),
|
||||
Expr::Array(elems) => self.write_array(&elems.args),
|
||||
Expr::Dict(elems) => self.write_dict(&elems.args),
|
||||
Expr::Args(args) => self.write_args(&args.args),
|
||||
Expr::Pattern(pat) => self.write_pattern(pat),
|
||||
Expr::Element(elem) => self.write_element(elem),
|
||||
Expr::Unary(unary) => self.write_unary(unary),
|
||||
Expr::Binary(binary) => self.write_binary(binary),
|
||||
Expr::Apply(apply) => self.write_apply(apply),
|
||||
Expr::Func(func) => self.write_func(func),
|
||||
Expr::Ref(ref_expr) => self.write_ref(ref_expr),
|
||||
Expr::ContentRef(content_ref) => self.write_content_ref(content_ref),
|
||||
Expr::Select(sel) => self.write_select(sel),
|
||||
Expr::Import(import) => self.write_import(import),
|
||||
Expr::Include(include) => self.write_include(include),
|
||||
Expr::Contextual(..) => self.f.write_str("content"),
|
||||
Expr::Let(..) | Expr::Show(..) | Expr::Set(..) => self.f.write_str("Expr(..)"),
|
||||
Expr::Conditional(..) | Expr::WhileLoop(..) | Expr::ForLoop(..) => {
|
||||
self.f.write_str("Expr(..)")
|
||||
}
|
||||
Expr::Type(ty) => self.write_type(ty),
|
||||
Expr::Decl(decl) => self.write_decl(decl),
|
||||
Expr::Star => self.f.write_str("*"),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_indent(&mut self) -> fmt::Result {
|
||||
write!(self.f, "{:indent$}", "", indent = self.indent)
|
||||
}
|
||||
|
||||
fn write_array(&mut self, elems: &[ArgExpr]) -> fmt::Result {
|
||||
if elems.len() <= 1 {
|
||||
self.f.write_char('(')?;
|
||||
if let Some(arg) = elems.first() {
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",")?
|
||||
}
|
||||
return self.f.write_str(")");
|
||||
}
|
||||
|
||||
writeln!(self.f, "(")?;
|
||||
self.indent += 1;
|
||||
for arg in elems.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_dict(&mut self, elems: &[ArgExpr]) -> fmt::Result {
|
||||
if elems.len() <= 1 {
|
||||
self.f.write_char('(')?;
|
||||
if let Some(arg) = elems.first() {
|
||||
self.write_arg(arg)?;
|
||||
} else {
|
||||
self.f.write_str(":")?
|
||||
}
|
||||
return self.f.write_str(")");
|
||||
}
|
||||
|
||||
writeln!(self.f, "(:")?;
|
||||
self.indent += 1;
|
||||
for arg in elems.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_args(&mut self, args: &[ArgExpr]) -> fmt::Result {
|
||||
writeln!(self.f, "(")?;
|
||||
for arg in args.iter() {
|
||||
self.write_indent()?;
|
||||
self.write_arg(arg)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.write_indent()?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_arg(&mut self, arg: &ArgExpr) -> fmt::Result {
|
||||
match arg {
|
||||
ArgExpr::Pos(pos) => self.write_expr(pos),
|
||||
ArgExpr::Named(named) => {
|
||||
let (k, v) = named.as_ref();
|
||||
self.write_decl(k)?;
|
||||
write!(self.f, ": ")?;
|
||||
self.write_expr(v)
|
||||
}
|
||||
ArgExpr::NamedRt(named) => {
|
||||
let n = named.as_ref();
|
||||
self.write_expr(&n.0)?;
|
||||
write!(self.f, ": ")?;
|
||||
self.write_expr(&n.1)
|
||||
}
|
||||
ArgExpr::Spread(spread) => {
|
||||
write!(self.f, "..")?;
|
||||
self.write_expr(spread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_pattern(&mut self, pat: &Pattern) -> fmt::Result {
|
||||
match pat {
|
||||
Pattern::Expr(expr) => self.write_expr(expr),
|
||||
Pattern::Simple(decl) => self.write_decl(decl),
|
||||
Pattern::Sig(sig) => self.write_pattern_sig(sig),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pattern_sig(&mut self, sig: &PatternSig) -> fmt::Result {
|
||||
self.f.write_str("pat(\n")?;
|
||||
self.indent += 1;
|
||||
for pos in &sig.pos {
|
||||
self.write_indent()?;
|
||||
self.write_pattern(pos)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
for (name, pat) in &sig.named {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "{name:?} = ")?;
|
||||
self.write_pattern(pat)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
if let Some((k, rest)) = &sig.spread_left {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "..{k:?}: ")?;
|
||||
self.write_pattern(rest)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
if let Some((k, rest)) = &sig.spread_right {
|
||||
self.write_indent()?;
|
||||
write!(self.f, "..{k:?}: ")?;
|
||||
self.write_pattern(rest)?;
|
||||
self.f.write_str(",\n")?;
|
||||
}
|
||||
self.indent -= 1;
|
||||
self.write_indent()?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_element(&mut self, elem: &Interned<ElementExpr>) -> fmt::Result {
|
||||
write!(self.f, "{:?}", elem.elem.name())
|
||||
}
|
||||
|
||||
fn write_unary(&mut self, unary: &Interned<UnExpr>) -> fmt::Result {
|
||||
use UnaryOp::*;
|
||||
match unary.op {
|
||||
Pos => {
|
||||
self.f.write_str("+")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
Neg => {
|
||||
self.f.write_str("-")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
Not => {
|
||||
self.f.write_str("not ")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
Return => {
|
||||
self.f.write_str("return ")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
Context => {
|
||||
self.f.write_str("context ")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
Spread => {
|
||||
self.f.write_str("..")?;
|
||||
self.write_expr(&unary.lhs)
|
||||
}
|
||||
NotElementOf => {
|
||||
self.f.write_str("not elementOf(")?;
|
||||
self.write_expr(&unary.lhs)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
ElementOf => {
|
||||
self.f.write_str("elementOf(")?;
|
||||
self.write_expr(&unary.lhs)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
TypeOf => {
|
||||
self.f.write_str("typeOf(")?;
|
||||
self.write_expr(&unary.lhs)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_binary(&mut self, binary: &Interned<BinExpr>) -> fmt::Result {
|
||||
let [lhs, rhs] = binary.operands();
|
||||
self.write_expr(lhs)?;
|
||||
write!(self.f, " {} ", binary.op.as_str())?;
|
||||
self.write_expr(rhs)
|
||||
}
|
||||
|
||||
fn write_apply(&mut self, apply: &Interned<ApplyExpr>) -> fmt::Result {
|
||||
self.write_expr(&apply.callee)?;
|
||||
write!(self.f, "(")?;
|
||||
self.write_expr(&apply.args)?;
|
||||
write!(self.f, ")")
|
||||
}
|
||||
|
||||
fn write_func(&mut self, func: &Interned<FuncExpr>) -> fmt::Result {
|
||||
self.write_decl(&func.decl)
|
||||
}
|
||||
|
||||
fn write_ref(&mut self, resolved: &Interned<RefExpr>) -> fmt::Result {
|
||||
if let Some(root) = &resolved.root {
|
||||
return self.write_expr(root);
|
||||
}
|
||||
if let Some(term) = &resolved.term {
|
||||
return self.write_type(term);
|
||||
}
|
||||
|
||||
write!(self.f, "undefined({:?})", resolved.decl)
|
||||
}
|
||||
|
||||
fn write_content_ref(&mut self, content_ref: &Interned<ContentRefExpr>) -> fmt::Result {
|
||||
write!(self.f, "@{:?}", content_ref.ident)
|
||||
}
|
||||
|
||||
fn write_select(&mut self, sel: &Interned<SelectExpr>) -> fmt::Result {
|
||||
write!(self.f, "")?;
|
||||
self.write_expr(&sel.lhs)?;
|
||||
self.f.write_str(".")?;
|
||||
self.write_decl(&sel.key)
|
||||
}
|
||||
|
||||
fn write_import(&mut self, import: &Interned<ImportExpr>) -> fmt::Result {
|
||||
self.f.write_str("import(")?;
|
||||
self.write_decl(&import.decl.decl)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_include(&mut self, include: &Interned<IncludeExpr>) -> fmt::Result {
|
||||
self.f.write_str("include(")?;
|
||||
self.write_expr(&include.source)?;
|
||||
self.f.write_str(")")
|
||||
}
|
||||
|
||||
fn write_type(&mut self, ty: &Ty) -> fmt::Result {
|
||||
let formatted = ty.describe();
|
||||
let formatted = formatted.as_deref().unwrap_or("any");
|
||||
self.f.write_str(formatted)
|
||||
}
|
||||
}
|
155
crates/tinymist-analysis/src/track_values.rs
Normal file
155
crates/tinymist-analysis/src/track_values.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
//! Dynamic analysis of an expression or import statement.
|
||||
|
||||
use comemo::Track;
|
||||
use ecow::*;
|
||||
use tinymist_std::typst::{TypstDocument, TypstPagedDocument};
|
||||
use typst::engine::{Engine, Route, Sink, Traced};
|
||||
use typst::foundations::{Context, Label, Scopes, Styles, Value};
|
||||
use typst::introspection::Introspector;
|
||||
use typst::model::BibliographyElem;
|
||||
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode};
|
||||
use typst::World;
|
||||
use typst_shim::eval::Vm;
|
||||
|
||||
/// Try to determine a set of possible values for an expression.
|
||||
pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<(Value, Option<Styles>)> {
|
||||
if let Some(parent) = node.parent() {
|
||||
if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 {
|
||||
return analyze_expr(world, parent);
|
||||
}
|
||||
}
|
||||
|
||||
analyze_expr_(world, node.get())
|
||||
}
|
||||
|
||||
/// Try to determine a set of possible values for an expression.
|
||||
pub fn analyze_expr_(world: &dyn World, node: &SyntaxNode) -> EcoVec<(Value, Option<Styles>)> {
|
||||
let Some(expr) = node.cast::<ast::Expr>() else {
|
||||
return eco_vec![];
|
||||
};
|
||||
|
||||
let val = match expr {
|
||||
ast::Expr::None(_) => Value::None,
|
||||
ast::Expr::Auto(_) => Value::Auto,
|
||||
ast::Expr::Bool(v) => Value::Bool(v.get()),
|
||||
ast::Expr::Int(v) => Value::Int(v.get()),
|
||||
ast::Expr::Float(v) => Value::Float(v.get()),
|
||||
ast::Expr::Numeric(v) => Value::numeric(v.get()),
|
||||
ast::Expr::Str(v) => Value::Str(v.get().into()),
|
||||
_ => {
|
||||
if node.kind() == SyntaxKind::Contextual {
|
||||
if let Some(child) = node.children().last() {
|
||||
return analyze_expr_(world, child);
|
||||
}
|
||||
}
|
||||
|
||||
return typst::trace::<TypstPagedDocument>(world, node.span());
|
||||
}
|
||||
};
|
||||
|
||||
eco_vec![(val, None)]
|
||||
}
|
||||
|
||||
/// Try to load a module from the current source file.
|
||||
pub fn analyze_import_(world: &dyn World, source: &SyntaxNode) -> (Option<Value>, Option<Value>) {
|
||||
let source_span = source.span();
|
||||
let Some((source, _)) = analyze_expr_(world, source).into_iter().next() else {
|
||||
return (None, None);
|
||||
};
|
||||
if source.scope().is_some() {
|
||||
return (Some(source.clone()), Some(source));
|
||||
}
|
||||
|
||||
let introspector = Introspector::default();
|
||||
let traced = Traced::default();
|
||||
let mut sink = Sink::new();
|
||||
let engine = Engine {
|
||||
routines: &typst::ROUTINES,
|
||||
world: world.track(),
|
||||
route: Route::default(),
|
||||
introspector: introspector.track(),
|
||||
traced: traced.track(),
|
||||
sink: sink.track_mut(),
|
||||
};
|
||||
|
||||
let context = Context::none();
|
||||
let mut vm = Vm::new(
|
||||
engine,
|
||||
context.track(),
|
||||
Scopes::new(Some(world.library())),
|
||||
Span::detached(),
|
||||
);
|
||||
let module = match source.clone() {
|
||||
Value::Str(path) => typst_shim::eval::import(&mut vm.engine, &path, source_span)
|
||||
.ok()
|
||||
.map(Value::Module),
|
||||
Value::Module(module) => Some(Value::Module(module)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(Some(source), module)
|
||||
}
|
||||
|
||||
/// A label with a description and details.
|
||||
pub struct DynLabel {
|
||||
/// The label itself.
|
||||
pub label: Label,
|
||||
/// A description of the label.
|
||||
pub label_desc: Option<EcoString>,
|
||||
/// Additional details about the label.
|
||||
pub detail: Option<EcoString>,
|
||||
/// The title of the bibliography entry. Not present for non-bibliography
|
||||
/// labels.
|
||||
pub bib_title: Option<EcoString>,
|
||||
}
|
||||
|
||||
/// Find all labels and details for them.
|
||||
///
|
||||
/// Returns:
|
||||
/// - All labels and descriptions for them, if available
|
||||
/// - A split offset: All labels before this offset belong to nodes, all after
|
||||
/// belong to a bibliography.
|
||||
pub fn analyze_labels(document: &TypstDocument) -> (Vec<DynLabel>, usize) {
|
||||
let mut output = vec![];
|
||||
|
||||
// Labels in the document.
|
||||
for elem in document.introspector().all() {
|
||||
let Some(label) = elem.label() else { continue };
|
||||
let (is_derived, details) = {
|
||||
let derived = elem
|
||||
.get_by_name("caption")
|
||||
.or_else(|_| elem.get_by_name("body"));
|
||||
|
||||
match derived {
|
||||
Ok(Value::Content(content)) => (true, content.plain_text()),
|
||||
Ok(Value::Str(s)) => (true, s.into()),
|
||||
Ok(_) => (false, elem.plain_text()),
|
||||
Err(_) => (false, elem.plain_text()),
|
||||
}
|
||||
};
|
||||
output.push(DynLabel {
|
||||
label,
|
||||
label_desc: Some(if is_derived {
|
||||
details.clone()
|
||||
} else {
|
||||
eco_format!("{}(..)", elem.func().name())
|
||||
}),
|
||||
detail: Some(details),
|
||||
bib_title: None,
|
||||
});
|
||||
}
|
||||
|
||||
let split = output.len();
|
||||
|
||||
// Bibliography keys.
|
||||
for (label, detail) in BibliographyElem::keys(document.introspector().track()) {
|
||||
output.push(DynLabel {
|
||||
label,
|
||||
label_desc: detail.clone(),
|
||||
detail: detail.clone(),
|
||||
bib_title: detail,
|
||||
});
|
||||
}
|
||||
|
||||
(output, split)
|
||||
}
|
64
crates/tinymist-analysis/src/ty/apply.rs
Normal file
64
crates/tinymist-analysis/src/ty/apply.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use super::{Sig, SigChecker, SigSurfaceKind, TyCtx};
|
||||
use crate::ty::prelude::*;
|
||||
|
||||
pub trait ApplyChecker: TyCtx {
|
||||
fn apply(&mut self, sig: Sig, arguments: &Interned<ArgsTy>, pol: bool);
|
||||
}
|
||||
|
||||
static EMPTY_ARGS: LazyLock<Interned<ArgsTy>> = LazyLock::new(|| ArgsTy::default().into());
|
||||
|
||||
impl Ty {
|
||||
/// Call the given type with the given arguments.
|
||||
pub fn call(&self, args: &Interned<ArgsTy>, pol: bool, c: &mut impl ApplyChecker) {
|
||||
ApplySigChecker(c, args).ty(self, SigSurfaceKind::Call, pol);
|
||||
}
|
||||
|
||||
/// Get the tuple element type of the given type.
|
||||
pub fn tuple_element_of(&self, pol: bool, c: &mut impl ApplyChecker) {
|
||||
ApplySigChecker(c, &EMPTY_ARGS).ty(self, SigSurfaceKind::Array, pol);
|
||||
}
|
||||
|
||||
/// Get the element type of the given type.
|
||||
pub fn element_of(&self, pol: bool, c: &mut impl ApplyChecker) {
|
||||
ApplySigChecker(c, &EMPTY_ARGS).ty(self, SigSurfaceKind::ArrayOrDict, pol);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(0)]
|
||||
pub struct ApplySigChecker<'a, T: ApplyChecker>(&'a mut T, &'a Interned<ArgsTy>);
|
||||
|
||||
impl<T: ApplyChecker> ApplySigChecker<'_, T> {
|
||||
fn ty(&mut self, ty: &Ty, surface: SigSurfaceKind, pol: bool) {
|
||||
ty.sig_surface(pol, surface, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApplyChecker> SigChecker for ApplySigChecker<'_, T> {
|
||||
fn check(&mut self, cano_sig: Sig, ctx: &mut super::SigCheckContext, pol: bool) -> Option<()> {
|
||||
let (cano_sig, is_partialize) = match cano_sig {
|
||||
Sig::Partialize(sig) => (*sig, true),
|
||||
sig => (sig, false),
|
||||
};
|
||||
// Bind the arguments to the canonical signature.
|
||||
let partial_sig = if ctx.args.is_empty() {
|
||||
cano_sig
|
||||
} else {
|
||||
Sig::With {
|
||||
sig: &cano_sig,
|
||||
withs: &ctx.args,
|
||||
at: &ctx.at,
|
||||
}
|
||||
};
|
||||
let partial_sig = if is_partialize {
|
||||
Sig::Partialize(&partial_sig)
|
||||
} else {
|
||||
partial_sig
|
||||
};
|
||||
|
||||
self.0.apply(partial_sig, self.1, pol);
|
||||
Some(())
|
||||
}
|
||||
}
|
171
crates/tinymist-analysis/src/ty/bound.rs
Normal file
171
crates/tinymist-analysis/src/ty/bound.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use typst::foundations::{self, Func};
|
||||
|
||||
use crate::ty::prelude::*;
|
||||
|
||||
pub trait BoundChecker: Sized + TyCtx {
|
||||
fn collect(&mut self, ty: &Ty, pol: bool);
|
||||
|
||||
fn check_var(&mut self, u: &Interned<TypeVar>, pol: bool) {
|
||||
self.check_var_rec(u, pol);
|
||||
}
|
||||
|
||||
fn check_var_rec(&mut self, u: &Interned<TypeVar>, pol: bool) {
|
||||
let Some(w) = self.global_bounds(u, pol) else {
|
||||
return;
|
||||
};
|
||||
let mut ctx = BoundCheckContext;
|
||||
ctx.tys(w.ubs.iter(), pol, self);
|
||||
ctx.tys(w.lbs.iter(), !pol, self);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(0)]
|
||||
pub struct BoundPred<'a, T: TyCtx, F>(pub &'a T, pub F);
|
||||
|
||||
impl<'a, T: TyCtx, F> BoundPred<'a, T, F> {
|
||||
pub fn new(t: &'a T, f: F) -> Self {
|
||||
Self(t, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TyCtx, F> BoundChecker for BoundPred<'_, T, F>
|
||||
where
|
||||
F: FnMut(&Ty, bool),
|
||||
{
|
||||
fn collect(&mut self, ty: &Ty, pol: bool) {
|
||||
self.1(ty, pol);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum DocSource {
|
||||
Var(Interned<TypeVar>),
|
||||
Ins(Interned<InsTy>),
|
||||
Builtin(BuiltinTy),
|
||||
}
|
||||
|
||||
impl DocSource {
|
||||
/// Regard doc source as function.
|
||||
pub fn as_func(&self) -> Option<Func> {
|
||||
match self {
|
||||
Self::Var(..) => None,
|
||||
Self::Builtin(BuiltinTy::Type(ty)) => Some(ty.constructor().ok()?),
|
||||
Self::Builtin(BuiltinTy::Element(ty)) => Some((*ty).into()),
|
||||
Self::Builtin(..) => None,
|
||||
Self::Ins(ins_ty) => match &ins_ty.val {
|
||||
foundations::Value::Func(func) => Some(func.clone()),
|
||||
foundations::Value::Type(ty) => Some(ty.constructor().ok()?),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
/// Check if the given type has bounds (is combinated).
|
||||
pub fn has_bounds(&self) -> bool {
|
||||
matches!(self, Ty::Union(_) | Ty::Let(_) | Ty::Var(_))
|
||||
}
|
||||
|
||||
/// Convert type to doc source
|
||||
pub fn as_source(&self) -> Option<DocSource> {
|
||||
match self {
|
||||
Ty::Builtin(ty @ (BuiltinTy::Type(..) | BuiltinTy::Element(..))) => {
|
||||
Some(DocSource::Builtin(ty.clone()))
|
||||
}
|
||||
Ty::Value(ty) => match &ty.val {
|
||||
foundations::Value::Type(..) | foundations::Value::Func(..) => {
|
||||
Some(DocSource::Ins(ty.clone()))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sources of the given type.
|
||||
pub fn sources(&self) -> Vec<DocSource> {
|
||||
let mut results = vec![];
|
||||
fn collect(ty: &Ty, results: &mut Vec<DocSource>) {
|
||||
use Ty::*;
|
||||
if let Some(src) = ty.as_source() {
|
||||
results.push(src);
|
||||
return;
|
||||
}
|
||||
match ty {
|
||||
Any | Boolean(_) | If(..) | Builtin(..) | Value(..) => {}
|
||||
Dict(..) | Array(..) | Tuple(..) | Func(..) | Args(..) | Pattern(..) => {}
|
||||
Unary(..) | Binary(..) => {}
|
||||
Param(ty) => {
|
||||
// todo: doc source can be param ty
|
||||
collect(&ty.ty, results);
|
||||
}
|
||||
Union(ty) => {
|
||||
for ty in ty.iter() {
|
||||
collect(ty, results);
|
||||
}
|
||||
}
|
||||
Let(ty) => {
|
||||
for ty in ty.ubs.iter() {
|
||||
collect(ty, results);
|
||||
}
|
||||
for ty in ty.lbs.iter() {
|
||||
collect(ty, results);
|
||||
}
|
||||
}
|
||||
Var(ty) => {
|
||||
results.push(DocSource::Var(ty.clone()));
|
||||
}
|
||||
With(ty) => collect(&ty.sig, results),
|
||||
Select(ty) => {
|
||||
// todo: do this correctly
|
||||
if matches!(ty.select.deref(), "with" | "where") {
|
||||
collect(&ty.ty, results);
|
||||
}
|
||||
|
||||
// collect(&ty.ty, results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect(self, &mut results);
|
||||
results
|
||||
}
|
||||
|
||||
/// Profile the bounds of the given type.
|
||||
pub fn bounds(&self, pol: bool, checker: &mut impl BoundChecker) {
|
||||
BoundCheckContext.ty(self, pol, checker);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoundCheckContext;
|
||||
|
||||
impl BoundCheckContext {
|
||||
fn tys<'a>(&mut self, tys: impl Iterator<Item = &'a Ty>, pol: bool, c: &mut impl BoundChecker) {
|
||||
for ty in tys {
|
||||
self.ty(ty, pol, c);
|
||||
}
|
||||
}
|
||||
|
||||
fn ty(&mut self, ty: &Ty, pol: bool, checker: &mut impl BoundChecker) {
|
||||
match ty {
|
||||
Ty::Union(u) => {
|
||||
self.tys(u.iter(), pol, checker);
|
||||
}
|
||||
Ty::Let(u) => {
|
||||
self.tys(u.ubs.iter(), pol, checker);
|
||||
self.tys(u.lbs.iter(), !pol, checker);
|
||||
}
|
||||
Ty::Var(u) => checker.check_var(u, pol),
|
||||
// todo: calculate these operators
|
||||
// Ty::Select(_) => {}
|
||||
// Ty::Unary(_) => {}
|
||||
// Ty::Binary(_) => {}
|
||||
// Ty::If(_) => {}
|
||||
ty => checker.collect(ty, pol),
|
||||
}
|
||||
}
|
||||
}
|
762
crates/tinymist-analysis/src/ty/builtin.rs
Normal file
762
crates/tinymist-analysis/src/ty/builtin.rs
Normal file
|
@ -0,0 +1,762 @@
|
|||
use core::fmt;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use regex::RegexSet;
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
use typst::foundations::{CastInfo, Regex};
|
||||
use typst::layout::Ratio;
|
||||
use typst::syntax::FileId;
|
||||
use typst::{
|
||||
foundations::{AutoValue, Content, Func, NoneValue, ParamInfo, Type, Value},
|
||||
layout::Length,
|
||||
};
|
||||
|
||||
use crate::syntax::Decl;
|
||||
use crate::ty::*;
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, EnumIter)]
|
||||
pub enum PathPreference {
|
||||
Source { allow_package: bool },
|
||||
Wasm,
|
||||
Csv,
|
||||
Image,
|
||||
Json,
|
||||
Yaml,
|
||||
Xml,
|
||||
Toml,
|
||||
Csl,
|
||||
Bibliography,
|
||||
RawTheme,
|
||||
RawSyntax,
|
||||
Special,
|
||||
None,
|
||||
}
|
||||
|
||||
impl PathPreference {
|
||||
pub fn ext_matcher(&self) -> &'static RegexSet {
|
||||
type RegSet = LazyLock<RegexSet>;
|
||||
|
||||
fn make_regex(patterns: &[&str]) -> RegexSet {
|
||||
let patterns = patterns.iter().map(|pattern| format!("(?i)^{pattern}$"));
|
||||
RegexSet::new(patterns).unwrap()
|
||||
}
|
||||
|
||||
static SOURCE_REGSET: RegSet = RegSet::new(|| make_regex(&["typ", "typc"]));
|
||||
static WASM_REGSET: RegSet = RegSet::new(|| make_regex(&["wasm"]));
|
||||
static IMAGE_REGSET: RegSet = RegSet::new(|| {
|
||||
make_regex(&[
|
||||
"ico", "bmp", "png", "webp", "jpg", "jpeg", "jfif", "tiff", "gif", "svg", "svgz",
|
||||
])
|
||||
});
|
||||
static JSON_REGSET: RegSet = RegSet::new(|| make_regex(&["json", "jsonc", "json5"]));
|
||||
static YAML_REGSET: RegSet = RegSet::new(|| make_regex(&["yaml", "yml"]));
|
||||
static XML_REGSET: RegSet = RegSet::new(|| make_regex(&["xml"]));
|
||||
static TOML_REGSET: RegSet = RegSet::new(|| make_regex(&["toml"]));
|
||||
static CSV_REGSET: RegSet = RegSet::new(|| make_regex(&["csv"]));
|
||||
static BIB_REGSET: RegSet = RegSet::new(|| make_regex(&["yaml", "yml", "bib"]));
|
||||
static CSL_REGSET: RegSet = RegSet::new(|| make_regex(&["csl"]));
|
||||
static RAW_THEME_REGSET: RegSet = RegSet::new(|| make_regex(&["tmTheme", "xml"]));
|
||||
static RAW_SYNTAX_REGSET: RegSet =
|
||||
RegSet::new(|| make_regex(&["tmLanguage", "sublime-syntax"]));
|
||||
|
||||
static ALL_REGSET: RegSet = RegSet::new(|| RegexSet::new([r".*"]).unwrap());
|
||||
static ALL_SPECIAL_REGSET: RegSet = RegSet::new(|| {
|
||||
RegexSet::new({
|
||||
let patterns = SOURCE_REGSET.patterns();
|
||||
let patterns = patterns.iter().chain(WASM_REGSET.patterns());
|
||||
let patterns = patterns.chain(IMAGE_REGSET.patterns());
|
||||
let patterns = patterns.chain(JSON_REGSET.patterns());
|
||||
let patterns = patterns.chain(YAML_REGSET.patterns());
|
||||
let patterns = patterns.chain(XML_REGSET.patterns());
|
||||
let patterns = patterns.chain(TOML_REGSET.patterns());
|
||||
let patterns = patterns.chain(CSV_REGSET.patterns());
|
||||
let patterns = patterns.chain(BIB_REGSET.patterns());
|
||||
let patterns = patterns.chain(CSL_REGSET.patterns());
|
||||
let patterns = patterns.chain(RAW_THEME_REGSET.patterns());
|
||||
patterns.chain(RAW_SYNTAX_REGSET.patterns())
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
match self {
|
||||
PathPreference::Source { .. } => &SOURCE_REGSET,
|
||||
PathPreference::Wasm => &WASM_REGSET,
|
||||
PathPreference::Csv => &CSV_REGSET,
|
||||
PathPreference::Image => &IMAGE_REGSET,
|
||||
PathPreference::Json => &JSON_REGSET,
|
||||
PathPreference::Yaml => &YAML_REGSET,
|
||||
PathPreference::Xml => &XML_REGSET,
|
||||
PathPreference::Toml => &TOML_REGSET,
|
||||
PathPreference::Csl => &CSL_REGSET,
|
||||
PathPreference::Bibliography => &BIB_REGSET,
|
||||
PathPreference::RawTheme => &RAW_THEME_REGSET,
|
||||
PathPreference::RawSyntax => &RAW_SYNTAX_REGSET,
|
||||
PathPreference::Special => &ALL_SPECIAL_REGSET,
|
||||
PathPreference::None => &ALL_REGSET,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_match(&self, path: &Path) -> bool {
|
||||
let ext = path.extension().and_then(|ext| ext.to_str());
|
||||
ext.is_some_and(|ext| self.ext_matcher().is_match(ext))
|
||||
}
|
||||
|
||||
pub fn from_ext(path: &str) -> Option<Self> {
|
||||
PathPreference::iter().find(|preference| preference.is_match(std::path::Path::new(path)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
pub fn from_cast_info(ty: &CastInfo) -> Ty {
|
||||
match &ty {
|
||||
CastInfo::Any => Ty::Any,
|
||||
CastInfo::Value(val, doc) => Ty::Value(InsTy::new_doc(val.clone(), *doc)),
|
||||
CastInfo::Type(ty) => Ty::Builtin(BuiltinTy::Type(*ty)),
|
||||
CastInfo::Union(types) => {
|
||||
Ty::iter_union(UnionIter(vec![types.as_slice().iter()]).map(Self::from_cast_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_param_site(func: &Func, param: &ParamInfo) -> Ty {
|
||||
use typst::foundations::func::Repr;
|
||||
match func.inner() {
|
||||
Repr::Element(..) | Repr::Native(..) | Repr::Plugin(..) => {
|
||||
if let Some(ty) = param_mapping(func, param) {
|
||||
return ty;
|
||||
}
|
||||
}
|
||||
Repr::Closure(_) => {}
|
||||
Repr::With(w) => return Ty::from_param_site(&w.0, param),
|
||||
};
|
||||
|
||||
Self::from_cast_info(¶m.input)
|
||||
}
|
||||
|
||||
pub(crate) fn from_return_site(func: &Func, ty: &'_ CastInfo) -> Self {
|
||||
use typst::foundations::func::Repr;
|
||||
match func.inner() {
|
||||
Repr::Element(elem) => return Ty::Builtin(BuiltinTy::Content(Some(*elem))),
|
||||
Repr::Closure(_) | Repr::Plugin(_) => {}
|
||||
Repr::With(w) => return Ty::from_return_site(&w.0, ty),
|
||||
Repr::Native(_) => {}
|
||||
};
|
||||
|
||||
Self::from_cast_info(ty)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnionIter<'a>(Vec<std::slice::Iter<'a, CastInfo>>);
|
||||
|
||||
impl<'a> Iterator for UnionIter<'a> {
|
||||
type Item = &'a CastInfo;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let iter = self.0.last_mut()?;
|
||||
if let Some(ty) = iter.next() {
|
||||
match ty {
|
||||
CastInfo::Union(types) => {
|
||||
self.0.push(types.as_slice().iter());
|
||||
}
|
||||
_ => return Some(ty),
|
||||
}
|
||||
} else {
|
||||
self.0.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: we can write some proto files for builtin sigs
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BuiltinSig<'a> {
|
||||
/// Map a function over a tuple.
|
||||
TupleMap(&'a Ty),
|
||||
/// Get element of a tuple.
|
||||
TupleAt(&'a Ty),
|
||||
}
|
||||
|
||||
/// A package identifier.
|
||||
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct PackageId {
|
||||
pub namespace: StrRef,
|
||||
pub name: StrRef,
|
||||
}
|
||||
|
||||
impl fmt::Debug for PackageId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "@{}/{}", self.namespace, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FileId> for PackageId {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: FileId) -> Result<Self, Self::Error> {
|
||||
let Some(spec) = value.package() else {
|
||||
return Err(());
|
||||
};
|
||||
Ok(PackageId {
|
||||
namespace: spec.namespace.as_str().into(),
|
||||
name: spec.name.as_str().into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum BuiltinTy {
|
||||
Clause,
|
||||
Undef,
|
||||
Space,
|
||||
None,
|
||||
Break,
|
||||
Continue,
|
||||
Infer,
|
||||
FlowNone,
|
||||
Auto,
|
||||
|
||||
Args,
|
||||
Color,
|
||||
TextSize,
|
||||
TextFont,
|
||||
TextFeature,
|
||||
TextLang,
|
||||
TextRegion,
|
||||
|
||||
Label,
|
||||
CiteLabel,
|
||||
RefLabel,
|
||||
Dir,
|
||||
Length,
|
||||
Float,
|
||||
|
||||
Stroke,
|
||||
Margin,
|
||||
Inset,
|
||||
Outset,
|
||||
Radius,
|
||||
|
||||
Tag(Box<(StrRef, Option<Interned<PackageId>>)>),
|
||||
|
||||
/// A value having a specific type.
|
||||
Type(typst::foundations::Type),
|
||||
/// A value of some type.
|
||||
TypeType(typst::foundations::Type),
|
||||
/// A content having a specific element type.
|
||||
Content(Option<typst::foundations::Element>),
|
||||
/// A value of some element type.
|
||||
Element(typst::foundations::Element),
|
||||
|
||||
Module(Interned<Decl>),
|
||||
Path(PathPreference),
|
||||
}
|
||||
|
||||
impl fmt::Debug for BuiltinTy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
BuiltinTy::Clause => f.write_str("Clause"),
|
||||
BuiltinTy::Undef => f.write_str("Undef"),
|
||||
BuiltinTy::Content(ty) => {
|
||||
if let Some(ty) = ty {
|
||||
write!(f, "Content({})", ty.name())
|
||||
} else {
|
||||
f.write_str("Content")
|
||||
}
|
||||
}
|
||||
BuiltinTy::Space => f.write_str("Space"),
|
||||
BuiltinTy::None => f.write_str("None"),
|
||||
BuiltinTy::Break => f.write_str("Break"),
|
||||
BuiltinTy::Continue => f.write_str("Continue"),
|
||||
BuiltinTy::Infer => f.write_str("Infer"),
|
||||
BuiltinTy::FlowNone => f.write_str("FlowNone"),
|
||||
BuiltinTy::Auto => f.write_str("Auto"),
|
||||
|
||||
BuiltinTy::Args => write!(f, "Args"),
|
||||
BuiltinTy::Color => write!(f, "Color"),
|
||||
BuiltinTy::TextSize => write!(f, "TextSize"),
|
||||
BuiltinTy::TextFont => write!(f, "TextFont"),
|
||||
BuiltinTy::TextFeature => write!(f, "TextFeature"),
|
||||
BuiltinTy::TextLang => write!(f, "TextLang"),
|
||||
BuiltinTy::TextRegion => write!(f, "TextRegion"),
|
||||
BuiltinTy::Dir => write!(f, "Dir"),
|
||||
BuiltinTy::Length => write!(f, "Length"),
|
||||
BuiltinTy::Label => write!(f, "Label"),
|
||||
BuiltinTy::CiteLabel => write!(f, "CiteLabel"),
|
||||
BuiltinTy::RefLabel => write!(f, "RefLabel"),
|
||||
BuiltinTy::Float => write!(f, "Float"),
|
||||
BuiltinTy::Stroke => write!(f, "Stroke"),
|
||||
BuiltinTy::Margin => write!(f, "Margin"),
|
||||
BuiltinTy::Inset => write!(f, "Inset"),
|
||||
BuiltinTy::Outset => write!(f, "Outset"),
|
||||
BuiltinTy::Radius => write!(f, "Radius"),
|
||||
BuiltinTy::TypeType(ty) => write!(f, "TypeType({})", ty.short_name()),
|
||||
BuiltinTy::Type(ty) => write!(f, "Type({})", ty.short_name()),
|
||||
BuiltinTy::Element(elem) => elem.fmt(f),
|
||||
BuiltinTy::Tag(tag) => {
|
||||
let (name, id) = tag.as_ref();
|
||||
if let Some(id) = id {
|
||||
write!(f, "Tag({name:?}) of {id:?}")
|
||||
} else {
|
||||
write!(f, "Tag({name:?})")
|
||||
}
|
||||
}
|
||||
BuiltinTy::Module(decl) => write!(f, "{decl:?}"),
|
||||
BuiltinTy::Path(preference) => write!(f, "Path({preference:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BuiltinTy {
|
||||
pub fn from_value(builtin: &Value) -> Ty {
|
||||
if let Value::Bool(v) = builtin {
|
||||
return Ty::Boolean(Some(*v));
|
||||
}
|
||||
|
||||
Self::from_builtin(builtin.ty())
|
||||
}
|
||||
|
||||
pub fn from_builtin(builtin: Type) -> Ty {
|
||||
if builtin == Type::of::<AutoValue>() {
|
||||
return Ty::Builtin(BuiltinTy::Auto);
|
||||
}
|
||||
if builtin == Type::of::<NoneValue>() {
|
||||
return Ty::Builtin(BuiltinTy::None);
|
||||
}
|
||||
if builtin == Type::of::<typst::visualize::Color>() {
|
||||
return Color.literally();
|
||||
}
|
||||
if builtin == Type::of::<bool>() {
|
||||
return Ty::Builtin(BuiltinTy::None);
|
||||
}
|
||||
if builtin == Type::of::<f64>() {
|
||||
return Float.literally();
|
||||
}
|
||||
if builtin == Type::of::<Length>() {
|
||||
return Length.literally();
|
||||
}
|
||||
if builtin == Type::of::<Content>() {
|
||||
return Ty::Builtin(BuiltinTy::Content(Option::None));
|
||||
}
|
||||
|
||||
BuiltinTy::Type(builtin).literally()
|
||||
}
|
||||
|
||||
pub(crate) fn describe(&self) -> EcoString {
|
||||
let res = match self {
|
||||
BuiltinTy::Clause => "any",
|
||||
BuiltinTy::Undef => "any",
|
||||
BuiltinTy::Content(ty) => {
|
||||
return if let Some(ty) = ty {
|
||||
eco_format!("content({})", ty.name())
|
||||
} else {
|
||||
"content".into()
|
||||
};
|
||||
}
|
||||
BuiltinTy::Space => "content",
|
||||
BuiltinTy::None => "none",
|
||||
BuiltinTy::Break => "break",
|
||||
BuiltinTy::Continue => "continue",
|
||||
BuiltinTy::Infer => "any",
|
||||
BuiltinTy::FlowNone => "none",
|
||||
BuiltinTy::Auto => "auto",
|
||||
|
||||
BuiltinTy::Args => "arguments",
|
||||
BuiltinTy::Color => "color",
|
||||
BuiltinTy::TextSize => "text.size",
|
||||
BuiltinTy::TextFont => "text.font",
|
||||
BuiltinTy::TextFeature => "text.feature",
|
||||
BuiltinTy::TextLang => "text.lang",
|
||||
BuiltinTy::TextRegion => "text.region",
|
||||
BuiltinTy::Dir => "dir",
|
||||
BuiltinTy::Length => "length",
|
||||
BuiltinTy::Float => "float",
|
||||
BuiltinTy::Label => "label",
|
||||
BuiltinTy::CiteLabel => "cite-label",
|
||||
BuiltinTy::RefLabel => "ref-label",
|
||||
BuiltinTy::Stroke => "stroke",
|
||||
BuiltinTy::Margin => "margin",
|
||||
BuiltinTy::Inset => "inset",
|
||||
BuiltinTy::Outset => "outset",
|
||||
BuiltinTy::Radius => "radius",
|
||||
BuiltinTy::TypeType(..) => "type",
|
||||
BuiltinTy::Type(ty) => ty.short_name(),
|
||||
BuiltinTy::Element(ty) => ty.name(),
|
||||
BuiltinTy::Tag(tag) => {
|
||||
let (name, id) = tag.as_ref();
|
||||
return if let Some(id) = id {
|
||||
eco_format!("tag {name} of {id:?}")
|
||||
} else {
|
||||
eco_format!("tag {name}")
|
||||
};
|
||||
}
|
||||
BuiltinTy::Module(m) => return eco_format!("module({})", m.name()),
|
||||
BuiltinTy::Path(s) => match s {
|
||||
PathPreference::None => "[any]",
|
||||
PathPreference::Special => "[any]",
|
||||
PathPreference::Source { .. } => "[source]",
|
||||
PathPreference::Wasm => "[wasm]",
|
||||
PathPreference::Csv => "[csv]",
|
||||
PathPreference::Image => "[image]",
|
||||
PathPreference::Json => "[json]",
|
||||
PathPreference::Yaml => "[yaml]",
|
||||
PathPreference::Xml => "[xml]",
|
||||
PathPreference::Toml => "[toml]",
|
||||
PathPreference::Csl => "[csl]",
|
||||
PathPreference::Bibliography => "[bib]",
|
||||
PathPreference::RawTheme => "[theme]",
|
||||
PathPreference::RawSyntax => "[syntax]",
|
||||
},
|
||||
};
|
||||
|
||||
res.into()
|
||||
}
|
||||
}
|
||||
|
||||
use BuiltinTy::*;
|
||||
|
||||
fn literally(s: impl FlowBuiltinLiterally) -> Ty {
|
||||
s.literally()
|
||||
}
|
||||
|
||||
trait FlowBuiltinLiterally {
|
||||
fn literally(self) -> Ty;
|
||||
}
|
||||
|
||||
impl FlowBuiltinLiterally for &str {
|
||||
fn literally(self) -> Ty {
|
||||
Ty::Value(InsTy::new(Value::Str(self.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowBuiltinLiterally for BuiltinTy {
|
||||
fn literally(self) -> Ty {
|
||||
Ty::Builtin(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowBuiltinLiterally for Ty {
|
||||
fn literally(self) -> Ty {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// separate by middle
|
||||
macro_rules! flow_builtin_union_inner {
|
||||
($literal_kind:expr) => {
|
||||
literally($literal_kind)
|
||||
};
|
||||
($($x:expr),+ $(,)?) => {
|
||||
Vec::from_iter([
|
||||
$(flow_builtin_union_inner!($x)),*
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! flow_union {
|
||||
// the first one is string
|
||||
($($b:tt)*) => {
|
||||
Ty::iter_union(flow_builtin_union_inner!( $($b)* ).into_iter())
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
macro_rules! flow_record {
|
||||
($($name:expr => $ty:expr),* $(,)?) => {
|
||||
RecordTy::new(vec![
|
||||
$(
|
||||
(
|
||||
$name.into(),
|
||||
$ty,
|
||||
),
|
||||
)*
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn param_mapping(func: &Func, param: &ParamInfo) -> Option<Ty> {
|
||||
// todo: remove path params which is compatible with 0.12.0
|
||||
match (func.name()?, param.name) {
|
||||
// todo: pdf.embed
|
||||
("embed", "path") => Some(literally(Path(PathPreference::None))),
|
||||
("cbor", "path" | "source") => Some(literally(Path(PathPreference::None))),
|
||||
("plugin", "source") => Some(literally(Path(PathPreference::Wasm))),
|
||||
("csv", "path" | "source") => Some(literally(Path(PathPreference::Csv))),
|
||||
("image", "path" | "source") => Some(literally(Path(PathPreference::Image))),
|
||||
("read", "path" | "source") => Some(literally(Path(PathPreference::None))),
|
||||
("json", "path" | "source") => Some(literally(Path(PathPreference::Json))),
|
||||
("yaml", "path" | "source") => Some(literally(Path(PathPreference::Yaml))),
|
||||
("xml", "path" | "source") => Some(literally(Path(PathPreference::Xml))),
|
||||
("toml", "path" | "source") => Some(literally(Path(PathPreference::Toml))),
|
||||
("raw", "theme") => Some(literally(Path(PathPreference::RawTheme))),
|
||||
("raw", "syntaxes") => Some(literally(Path(PathPreference::RawSyntax))),
|
||||
("bibliography" | "cite", "style") => Some(Ty::iter_union([
|
||||
literally(Path(PathPreference::Csl)),
|
||||
Ty::from_cast_info(¶m.input),
|
||||
])),
|
||||
("cite", "key") => Some(Ty::iter_union([literally(CiteLabel)])),
|
||||
("ref", "target") => Some(Ty::iter_union([literally(RefLabel)])),
|
||||
("footnote", "body") => Some(Ty::iter_union([
|
||||
literally(RefLabel),
|
||||
Ty::from_cast_info(¶m.input),
|
||||
])),
|
||||
("link", "dest") => {
|
||||
static LINK_DEST_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
flow_union!(
|
||||
literally(RefLabel),
|
||||
Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Str>())),
|
||||
Ty::Builtin(BuiltinTy::Type(Type::of::<typst::introspection::Location>())),
|
||||
Ty::Dict(RecordTy::new(vec![
|
||||
("x".into(), literally(Length)),
|
||||
("y".into(), literally(Length)),
|
||||
])),
|
||||
)
|
||||
});
|
||||
Some(LINK_DEST_TYPE.clone())
|
||||
}
|
||||
("bibliography", "path" | "sources") => {
|
||||
static BIB_PATH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
let bib_path_ty = literally(Path(PathPreference::Bibliography));
|
||||
Ty::iter_union([bib_path_ty.clone(), Ty::Array(bib_path_ty.into())])
|
||||
});
|
||||
Some(BIB_PATH_TYPE.clone())
|
||||
}
|
||||
("text", "size") => Some(literally(TextSize)),
|
||||
("text", "font") => {
|
||||
// todo: the dict can be completed, but we have bugs...
|
||||
static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
Ty::iter_union([literally(TextFont), Ty::Array(literally(TextFont).into())])
|
||||
});
|
||||
Some(FONT_TYPE.clone())
|
||||
}
|
||||
("text", "feature") => {
|
||||
static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
Ty::iter_union([
|
||||
// todo: the key can only be the text feature
|
||||
Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Dict>())),
|
||||
Ty::Array(literally(TextFeature).into()),
|
||||
])
|
||||
});
|
||||
Some(FONT_TYPE.clone())
|
||||
}
|
||||
("text", "costs") => {
|
||||
static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
Ty::Dict(flow_record!(
|
||||
"hyphenation" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
|
||||
"runt" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
|
||||
"widow" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
|
||||
"orphan" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
|
||||
))
|
||||
});
|
||||
Some(FONT_TYPE.clone())
|
||||
}
|
||||
("text", "lang") => Some(literally(TextLang)),
|
||||
("text", "region") => Some(literally(TextRegion)),
|
||||
("text" | "stack", "dir") => Some(literally(Dir)),
|
||||
("par", "first-line-indent") => {
|
||||
static FIRST_LINE_INDENT: LazyLock<Ty> = LazyLock::new(|| {
|
||||
Ty::iter_union([
|
||||
literally(Length),
|
||||
Ty::Dict(RecordTy::new(vec![
|
||||
("amount".into(), literally(Length)),
|
||||
("all".into(), Ty::Boolean(Option::None)),
|
||||
])),
|
||||
])
|
||||
});
|
||||
Some(FIRST_LINE_INDENT.clone())
|
||||
}
|
||||
(
|
||||
// todo: polygon.regular
|
||||
"page" | "highlight" | "text" | "path" | "curve" | "rect" | "ellipse" | "circle"
|
||||
| "polygon" | "box" | "block" | "table" | "regular",
|
||||
"fill",
|
||||
) => Some(literally(Color)),
|
||||
(
|
||||
// todo: table.cell
|
||||
"table" | "cell" | "block" | "box" | "circle" | "ellipse" | "rect" | "square",
|
||||
"inset",
|
||||
) => Some(literally(Inset)),
|
||||
("block" | "box" | "circle" | "ellipse" | "rect" | "square", "outset") => {
|
||||
Some(literally(Outset))
|
||||
}
|
||||
("block" | "box" | "rect" | "square" | "highlight", "radius") => Some(literally(Radius)),
|
||||
("grid" | "table", "columns" | "rows" | "gutter" | "column-gutter" | "row-gutter") => {
|
||||
static COLUMN_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
flow_union!(
|
||||
Ty::Value(InsTy::new(Value::Auto)),
|
||||
Ty::Value(InsTy::new(Value::Type(Type::of::<i64>()))),
|
||||
literally(Length),
|
||||
Ty::Array(literally(Length).into()),
|
||||
)
|
||||
});
|
||||
Some(COLUMN_TYPE.clone())
|
||||
}
|
||||
("pattern" | "tiling", "size") => {
|
||||
static PATTERN_SIZE_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
flow_union!(
|
||||
Ty::Value(InsTy::new(Value::Auto)),
|
||||
Ty::Array(Ty::Builtin(Length).into()),
|
||||
)
|
||||
});
|
||||
Some(PATTERN_SIZE_TYPE.clone())
|
||||
}
|
||||
("stroke", "dash") => Some(FLOW_STROKE_DASH_TYPE.clone()),
|
||||
(
|
||||
//todo: table.cell, table.hline, table.vline, math.cancel, grid.cell, polygon.regular
|
||||
"cancel" | "highlight" | "overline" | "strike" | "underline" | "text" | "path"
|
||||
| "curve" | "rect" | "ellipse" | "circle" | "polygon" | "box" | "block" | "table"
|
||||
| "line" | "cell" | "hline" | "vline" | "regular",
|
||||
"stroke",
|
||||
) => Some(Ty::Builtin(Stroke)),
|
||||
("page", "margin") => Some(Ty::Builtin(Margin)),
|
||||
_ => Option::None,
|
||||
}
|
||||
}
|
||||
|
||||
static FLOW_STROKE_DASH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
|
||||
flow_union!(
|
||||
"solid",
|
||||
"dotted",
|
||||
"densely-dotted",
|
||||
"loosely-dotted",
|
||||
"dashed",
|
||||
"densely-dashed",
|
||||
"loosely-dashed",
|
||||
"dash-dotted",
|
||||
"densely-dash-dotted",
|
||||
"loosely-dash-dotted",
|
||||
Ty::Array(flow_union!("dot", literally(Float)).into()),
|
||||
Ty::Dict(flow_record!(
|
||||
"array" => Ty::Array(flow_union!("dot", literally(Float)).into()),
|
||||
"phase" => literally(Length),
|
||||
))
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_STROKE_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"paint" => literally(Color),
|
||||
"thickness" => literally(Length),
|
||||
"cap" => flow_union!("butt", "round", "square"),
|
||||
"join" => flow_union!("miter", "round", "bevel"),
|
||||
"dash" => FLOW_STROKE_DASH_TYPE.clone(),
|
||||
"miter-limit" => literally(Float),
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_MARGIN_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"top" => literally(Length),
|
||||
"right" => literally(Length),
|
||||
"bottom" => literally(Length),
|
||||
"left" => literally(Length),
|
||||
"inside" => literally(Length),
|
||||
"outside" => literally(Length),
|
||||
"x" => literally(Length),
|
||||
"y" => literally(Length),
|
||||
"rest" => literally(Length),
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_INSET_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"top" => literally(Length),
|
||||
"right" => literally(Length),
|
||||
"bottom" => literally(Length),
|
||||
"left" => literally(Length),
|
||||
"x" => literally(Length),
|
||||
"y" => literally(Length),
|
||||
"rest" => literally(Length),
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_OUTSET_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"top" => literally(Length),
|
||||
"right" => literally(Length),
|
||||
"bottom" => literally(Length),
|
||||
"left" => literally(Length),
|
||||
"x" => literally(Length),
|
||||
"y" => literally(Length),
|
||||
"rest" => literally(Length),
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_RADIUS_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"top" => literally(Length),
|
||||
"right" => literally(Length),
|
||||
"bottom" => literally(Length),
|
||||
"left" => literally(Length),
|
||||
"top-left" => literally(Length),
|
||||
"top-right" => literally(Length),
|
||||
"bottom-left" => literally(Length),
|
||||
"bottom-right" => literally(Length),
|
||||
"rest" => literally(Length),
|
||||
)
|
||||
});
|
||||
|
||||
pub static FLOW_TEXT_FONT_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
|
||||
flow_record!(
|
||||
"name" => literally(TextFont),
|
||||
"covers" => flow_union!("latin-in-cjk", BuiltinTy::Type(Type::of::<Regex>())),
|
||||
)
|
||||
});
|
||||
|
||||
// todo bad case: array.fold
|
||||
// todo bad case: datetime
|
||||
// todo bad case: selector
|
||||
// todo: function signatures, for example: `locate(loc => ...)`
|
||||
|
||||
// todo: numbering/supplement
|
||||
// todo: grid/table.fill/align/stroke/inset can be a function
|
||||
// todo: math.cancel.angle can be a function
|
||||
// todo: math.mat.augment
|
||||
// todo: csv.row-type can be an array or a dictionary
|
||||
// todo: text.stylistic-set is an array of integer
|
||||
// todo: raw.lang can be completed
|
||||
// todo: smartquote.quotes can be an array or a dictionary
|
||||
// todo: mat.augment can be a dictionary
|
||||
// todo: pdf.embed mime-type can be special
|
||||
|
||||
// ISO 639
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::syntax::Decl;
|
||||
|
||||
use super::{SigTy, Ty, TypeVar};
|
||||
|
||||
#[test]
|
||||
fn test_image_extension() {
|
||||
let path = "test.png";
|
||||
let preference = super::PathPreference::from_ext(path).unwrap();
|
||||
assert_eq!(preference, super::PathPreference::Image);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_extension_uppercase() {
|
||||
let path = "TEST.PNG";
|
||||
let preference = super::PathPreference::from_ext(path).unwrap();
|
||||
assert_eq!(preference, super::PathPreference::Image);
|
||||
}
|
||||
|
||||
// todo: map function
|
||||
// Technical Note for implementing a map function:
|
||||
// `u`, `v` is in level 2
|
||||
// instantiate a `v` as the return type of the map function.
|
||||
#[test]
|
||||
fn test_map() {
|
||||
let u = Ty::Var(TypeVar::new("u".into(), Decl::lit("u").into()));
|
||||
let v = Ty::Var(TypeVar::new("v".into(), Decl::lit("v").into()));
|
||||
let mapper_fn =
|
||||
Ty::Func(SigTy::new([u].into_iter(), None, None, None, Some(v.clone())).into());
|
||||
let map_fn =
|
||||
Ty::Func(SigTy::new([mapper_fn].into_iter(), None, None, None, Some(v)).into());
|
||||
let _ = map_fn;
|
||||
// println!("{map_fn:?}");
|
||||
}
|
||||
}
|
111
crates/tinymist-analysis/src/ty/convert.rs
Normal file
111
crates/tinymist-analysis/src/ty/convert.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use typst::syntax::Span;
|
||||
|
||||
use crate::func_signature;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn is_plain_value(value: &Value) -> bool {
|
||||
matches!(
|
||||
value,
|
||||
Value::Label(..)
|
||||
| Value::None
|
||||
| Value::Auto
|
||||
| Value::Bool(..)
|
||||
| Value::Int(..)
|
||||
| Value::Float(..)
|
||||
| Value::Decimal(..)
|
||||
| Value::Length(..)
|
||||
| Value::Angle(..)
|
||||
| Value::Ratio(..)
|
||||
| Value::Relative(..)
|
||||
| Value::Fraction(..)
|
||||
| Value::Color(..)
|
||||
| Value::Gradient(..)
|
||||
| Value::Tiling(..)
|
||||
| Value::Symbol(..)
|
||||
| Value::Version(..)
|
||||
| Value::Str(..)
|
||||
| Value::Bytes(..)
|
||||
| Value::Datetime(..)
|
||||
| Value::Duration(..)
|
||||
| Value::Content(..)
|
||||
| Value::Styles(..)
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the type of a value.
|
||||
#[comemo::memoize]
|
||||
pub fn term_value(value: &Value) -> Ty {
|
||||
match value {
|
||||
Value::Array(a) => {
|
||||
let values = a
|
||||
.iter()
|
||||
.map(|v| term_value_rec(v, Span::detached()))
|
||||
.collect::<Vec<_>>();
|
||||
Ty::Tuple(values.into())
|
||||
}
|
||||
// todo: term arguments
|
||||
Value::Args(..) => Ty::Builtin(BuiltinTy::Args),
|
||||
Value::Dict(dict) => {
|
||||
let values = dict
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str().into(), term_value_rec(v, Span::detached())))
|
||||
.collect();
|
||||
Ty::Dict(RecordTy::new(values))
|
||||
}
|
||||
Value::Module(module) => {
|
||||
let values = module
|
||||
.scope()
|
||||
.iter()
|
||||
.map(|(k, b)| (k.into(), term_value_rec(b.read(), b.span())))
|
||||
.collect();
|
||||
Ty::Dict(RecordTy::new(values))
|
||||
}
|
||||
Value::Type(ty) => Ty::Builtin(BuiltinTy::TypeType(*ty)),
|
||||
Value::Dyn(dyn_val) => Ty::Builtin(BuiltinTy::Type(dyn_val.ty())),
|
||||
Value::Func(func) => Ty::Func(func_signature(func.clone()).type_sig()),
|
||||
_ if is_plain_value(value) => Ty::Value(InsTy::new(value.clone())),
|
||||
_ => Ty::Any,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn term_value_rec(value: &Value, s: Span) -> Ty {
|
||||
match value {
|
||||
Value::Type(ty) => Ty::Builtin(BuiltinTy::TypeType(*ty)),
|
||||
Value::Dyn(v) => Ty::Builtin(BuiltinTy::Type(v.ty())),
|
||||
Value::None
|
||||
| Value::Auto
|
||||
| Value::Array(..)
|
||||
| Value::Args(..)
|
||||
| Value::Dict(..)
|
||||
| Value::Module(..)
|
||||
| Value::Func(..)
|
||||
| Value::Label(..)
|
||||
| Value::Bool(..)
|
||||
| Value::Int(..)
|
||||
| Value::Float(..)
|
||||
| Value::Decimal(..)
|
||||
| Value::Length(..)
|
||||
| Value::Angle(..)
|
||||
| Value::Ratio(..)
|
||||
| Value::Relative(..)
|
||||
| Value::Fraction(..)
|
||||
| Value::Color(..)
|
||||
| Value::Gradient(..)
|
||||
| Value::Tiling(..)
|
||||
| Value::Symbol(..)
|
||||
| Value::Version(..)
|
||||
| Value::Str(..)
|
||||
| Value::Bytes(..)
|
||||
| Value::Datetime(..)
|
||||
| Value::Duration(..)
|
||||
| Value::Content(..)
|
||||
| Value::Styles(..) => {
|
||||
if !s.is_detached() {
|
||||
Ty::Value(InsTy::new_at(value.clone(), s))
|
||||
} else {
|
||||
Ty::Value(InsTy::new(value.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1507
crates/tinymist-analysis/src/ty/def.rs
Normal file
1507
crates/tinymist-analysis/src/ty/def.rs
Normal file
File diff suppressed because it is too large
Load diff
226
crates/tinymist-analysis/src/ty/describe.rs
Normal file
226
crates/tinymist-analysis/src/ty/describe.rs
Normal file
|
@ -0,0 +1,226 @@
|
|||
use ecow::{eco_format, EcoString};
|
||||
use tinymist_std::hash::hash128;
|
||||
use tinymist_world::vfs::WorkspaceResolver;
|
||||
use typst::foundations::Repr;
|
||||
|
||||
use super::{is_plain_value, term_value};
|
||||
use crate::{ty::prelude::*, upstream::truncated_repr_};
|
||||
|
||||
impl Ty {
|
||||
/// Describe the given type.
|
||||
pub fn repr(&self) -> Option<EcoString> {
|
||||
let mut worker = TypeDescriber {
|
||||
repr: true,
|
||||
..Default::default()
|
||||
};
|
||||
worker.describe_root(self)
|
||||
}
|
||||
|
||||
/// Describe available value instances of the given type.
|
||||
pub fn value_repr(&self) -> Option<EcoString> {
|
||||
let mut worker = TypeDescriber {
|
||||
repr: true,
|
||||
value: true,
|
||||
..Default::default()
|
||||
};
|
||||
worker.describe_root(self)
|
||||
}
|
||||
|
||||
/// Describe the given type.
|
||||
pub fn describe(&self) -> Option<EcoString> {
|
||||
let mut worker = TypeDescriber::default();
|
||||
worker.describe_root(self)
|
||||
}
|
||||
|
||||
// todo: extend this cache idea for all crate?
|
||||
// #[allow(clippy::mutable_key_type)]
|
||||
// let mut describe_cache = HashMap::<Ty, String>::new();
|
||||
// let doc_ty = |ty: Option<&Ty>| {
|
||||
// let ty = ty?;
|
||||
// let short = {
|
||||
// describe_cache
|
||||
// .entry(ty.clone())
|
||||
// .or_insert_with(|| ty.describe().unwrap_or_else(||
|
||||
// "unknown".to_string())) .clone()
|
||||
// };
|
||||
|
||||
// Some((short, format!("{ty:?}")))
|
||||
// };
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TypeDescriber {
|
||||
repr: bool,
|
||||
value: bool,
|
||||
described: HashMap<u128, EcoString>,
|
||||
results: HashSet<EcoString>,
|
||||
functions: Vec<Interned<SigTy>>,
|
||||
}
|
||||
|
||||
impl TypeDescriber {
|
||||
fn describe_root(&mut self, ty: &Ty) -> Option<EcoString> {
|
||||
let _ = TypeDescriber::describe_iter;
|
||||
// recursive structure
|
||||
if let Some(t) = self.described.get(&hash128(ty)) {
|
||||
return Some(t.clone());
|
||||
}
|
||||
|
||||
let res = self.describe(ty);
|
||||
if !res.is_empty() {
|
||||
return Some(res);
|
||||
}
|
||||
self.described.insert(hash128(ty), "$self".into());
|
||||
|
||||
let mut results = std::mem::take(&mut self.results)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let functions = std::mem::take(&mut self.functions);
|
||||
if !functions.is_empty() {
|
||||
// todo: union signature
|
||||
// only first function is described
|
||||
let func = functions[0].clone();
|
||||
|
||||
let mut res = EcoString::new();
|
||||
res.push('(');
|
||||
let mut not_first = false;
|
||||
for ty in func.positional_params() {
|
||||
if not_first {
|
||||
res.push_str(", ");
|
||||
} else {
|
||||
not_first = true;
|
||||
}
|
||||
res.push_str(self.describe_root(ty).as_deref().unwrap_or("any"));
|
||||
}
|
||||
for (name, ty) in func.named_params() {
|
||||
if not_first {
|
||||
res.push_str(", ");
|
||||
} else {
|
||||
not_first = true;
|
||||
}
|
||||
res.push_str(name);
|
||||
res.push_str(": ");
|
||||
res.push_str(self.describe_root(ty).as_deref().unwrap_or("any"));
|
||||
}
|
||||
if let Some(spread_right) = func.rest_param() {
|
||||
if not_first {
|
||||
res.push_str(", ");
|
||||
}
|
||||
res.push_str("..: ");
|
||||
res.push_str(self.describe_root(spread_right).as_deref().unwrap_or("any"));
|
||||
}
|
||||
res.push_str(") => ");
|
||||
res.push_str(
|
||||
func.body
|
||||
.as_ref()
|
||||
.and_then(|ret| self.describe_root(ret))
|
||||
.as_deref()
|
||||
.unwrap_or("any"),
|
||||
);
|
||||
|
||||
results.push(res);
|
||||
}
|
||||
|
||||
if results.is_empty() {
|
||||
self.described.insert(hash128(ty), "any".into());
|
||||
return None;
|
||||
}
|
||||
|
||||
results.sort();
|
||||
results.dedup();
|
||||
let res: EcoString = results.join(" | ").into();
|
||||
self.described.insert(hash128(ty), res.clone());
|
||||
Some(res)
|
||||
}
|
||||
|
||||
fn describe_iter(&mut self, ty: &[Ty]) {
|
||||
for ty in ty.iter() {
|
||||
let desc = self.describe(ty);
|
||||
if !desc.is_empty() {
|
||||
self.results.insert(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn describe(&mut self, ty: &Ty) -> EcoString {
|
||||
match ty {
|
||||
Ty::Var(..) => {}
|
||||
Ty::Union(types) => {
|
||||
self.describe_iter(types);
|
||||
}
|
||||
Ty::Let(bounds) => {
|
||||
self.describe_iter(&bounds.lbs);
|
||||
self.describe_iter(&bounds.ubs);
|
||||
}
|
||||
Ty::Func(func) => {
|
||||
self.functions.push(func.clone());
|
||||
}
|
||||
Ty::Dict(..) => {
|
||||
return "dictionary".into();
|
||||
}
|
||||
Ty::Tuple(..) => {
|
||||
return "array".into();
|
||||
}
|
||||
Ty::Array(..) => {
|
||||
return "array".into();
|
||||
}
|
||||
// todo: sig with
|
||||
Ty::With(w) => {
|
||||
return self.describe(&w.sig);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Content(Some(elem))) => {
|
||||
return elem.name().into();
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Content(None) | BuiltinTy::Space) => {
|
||||
return "content".into();
|
||||
}
|
||||
// Doesn't provide any information, hence we doesn't describe it intermediately here.
|
||||
Ty::Any | Ty::Builtin(BuiltinTy::Clause | BuiltinTy::Undef | BuiltinTy::Infer) => {}
|
||||
Ty::Builtin(BuiltinTy::FlowNone | BuiltinTy::None) => {
|
||||
return "none".into();
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Auto) => {
|
||||
return "auto".into();
|
||||
}
|
||||
Ty::Boolean(..) if self.repr => {
|
||||
return "bool".into();
|
||||
}
|
||||
Ty::Boolean(None) => {
|
||||
return "bool".into();
|
||||
}
|
||||
Ty::Boolean(Some(b)) => {
|
||||
return eco_format!("{b}");
|
||||
}
|
||||
Ty::Builtin(b) => {
|
||||
return b.describe();
|
||||
}
|
||||
Ty::Value(v) if matches!(v.val, Value::Module(..)) => {
|
||||
let Value::Module(m) = &v.val else {
|
||||
return "module".into();
|
||||
};
|
||||
|
||||
return match (m.name(), m.file_id()) {
|
||||
(Some(name), ..) => eco_format!("module({name:?})"),
|
||||
(None, file_id) => {
|
||||
eco_format!("module({:?})", WorkspaceResolver::display(file_id))
|
||||
}
|
||||
};
|
||||
}
|
||||
Ty::Value(v) if !is_plain_value(&v.val) => return self.describe(&term_value(&v.val)),
|
||||
Ty::Value(v) if self.value => return truncated_repr_::<181>(&v.val),
|
||||
Ty::Value(v) if self.repr => return v.val.ty().short_name().into(),
|
||||
Ty::Value(v) => return v.val.repr(),
|
||||
Ty::Param(..) => {
|
||||
return "param".into();
|
||||
}
|
||||
Ty::Args(..) => {
|
||||
return "arguments".into();
|
||||
}
|
||||
Ty::Pattern(..) => {
|
||||
return "pattern".into();
|
||||
}
|
||||
Ty::Select(..) | Ty::Unary(..) | Ty::Binary(..) | Ty::If(..) => return "any".into(),
|
||||
}
|
||||
|
||||
EcoString::new()
|
||||
}
|
||||
}
|
307
crates/tinymist-analysis/src/ty/iface.rs
Normal file
307
crates/tinymist-analysis/src/ty/iface.rs
Normal file
|
@ -0,0 +1,307 @@
|
|||
use typst::foundations::{Dict, Func, Module, Scope, Type};
|
||||
use typst::syntax::FileId;
|
||||
|
||||
use super::BoundChecker;
|
||||
use crate::{syntax::Decl, ty::prelude::*};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Iface<'a> {
|
||||
Array(&'a Interned<Ty>),
|
||||
Tuple(&'a Interned<Vec<Ty>>),
|
||||
Dict(&'a Interned<RecordTy>),
|
||||
Content {
|
||||
val: &'a typst::foundations::Element,
|
||||
at: &'a Ty,
|
||||
},
|
||||
TypeType {
|
||||
val: &'a typst::foundations::Type,
|
||||
at: &'a Ty,
|
||||
},
|
||||
Type {
|
||||
val: &'a typst::foundations::Type,
|
||||
at: &'a Ty,
|
||||
},
|
||||
Func {
|
||||
val: &'a typst::foundations::Func,
|
||||
at: &'a Ty,
|
||||
},
|
||||
Value {
|
||||
val: &'a Dict,
|
||||
at: &'a Ty,
|
||||
},
|
||||
Module {
|
||||
val: FileId,
|
||||
at: &'a Ty,
|
||||
},
|
||||
ModuleVal {
|
||||
val: &'a Module,
|
||||
at: &'a Ty,
|
||||
},
|
||||
}
|
||||
|
||||
impl Iface<'_> {
|
||||
pub fn to_type(self) -> Ty {
|
||||
match self {
|
||||
Iface::Array(ty) => Ty::Array(ty.clone()),
|
||||
Iface::Tuple(tys) => Ty::Tuple(tys.clone()),
|
||||
Iface::Dict(dict) => Ty::Dict(dict.clone()),
|
||||
Iface::Content { at, .. }
|
||||
| Iface::TypeType { at, .. }
|
||||
| Iface::Type { at, .. }
|
||||
| Iface::Func { at, .. }
|
||||
| Iface::Value { at, .. }
|
||||
| Iface::Module { at, .. }
|
||||
| Iface::ModuleVal { at, .. } => at.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
// IfaceShape { iface }
|
||||
pub fn select(self, ctx: &mut impl TyCtxMut, key: &StrRef) -> Option<Ty> {
|
||||
crate::log_debug_ct!("iface shape: {self:?}");
|
||||
|
||||
match self {
|
||||
Iface::Array(..) | Iface::Tuple(..) => {
|
||||
select_scope(Some(Type::of::<typst::foundations::Array>().scope()), key)
|
||||
}
|
||||
Iface::Dict(dict) => dict.field_by_name(key).cloned(),
|
||||
Iface::Content { val, .. } => select_scope(Some(val.scope()), key),
|
||||
// todo: distinguish TypeType and Type
|
||||
Iface::TypeType { val, .. } | Iface::Type { val, .. } => {
|
||||
select_scope(Some(val.scope()), key)
|
||||
}
|
||||
Iface::Func { val, .. } => select_scope(val.scope(), key),
|
||||
Iface::Value { val, at: _ } => ctx.type_of_dict(val).field_by_name(key).cloned(),
|
||||
Iface::Module { val, at: _ } => ctx.check_module_item(val, key),
|
||||
Iface::ModuleVal { val, at: _ } => ctx.type_of_module(val).field_by_name(key).cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_scope(scope: Option<&Scope>, key: &str) -> Option<Ty> {
|
||||
let scope = scope?;
|
||||
let sub = scope.get(key)?;
|
||||
let sub_span = sub.span();
|
||||
Some(Ty::Value(InsTy::new_at(sub.read().clone(), sub_span)))
|
||||
}
|
||||
|
||||
pub trait IfaceChecker: TyCtx {
|
||||
fn check(&mut self, iface: Iface, ctx: &mut IfaceCheckContext, pol: bool) -> Option<()>;
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
/// Iterate over the signatures of the given type.
|
||||
pub fn iface_surface(
|
||||
&self,
|
||||
pol: bool,
|
||||
// iface_kind: IfaceSurfaceKind,
|
||||
checker: &mut impl IfaceChecker,
|
||||
) {
|
||||
let context = IfaceCheckContext { args: Vec::new() };
|
||||
let mut worker = IfaceCheckDriver {
|
||||
ctx: context,
|
||||
checker,
|
||||
};
|
||||
|
||||
worker.ty(self, pol);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IfaceCheckContext {
|
||||
pub args: Vec<Interned<SigTy>>,
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(checker)]
|
||||
pub struct IfaceCheckDriver<'a> {
|
||||
ctx: IfaceCheckContext,
|
||||
checker: &'a mut dyn IfaceChecker,
|
||||
}
|
||||
|
||||
impl BoundChecker for IfaceCheckDriver<'_> {
|
||||
fn collect(&mut self, ty: &Ty, pol: bool) {
|
||||
self.ty(ty, pol);
|
||||
}
|
||||
}
|
||||
|
||||
impl IfaceCheckDriver<'_> {
|
||||
fn array_as_iface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn dict_as_iface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn value_as_iface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn ty(&mut self, at: &Ty, pol: bool) {
|
||||
crate::log_debug_ct!("check iface ty: {at:?}");
|
||||
|
||||
match at {
|
||||
Ty::Builtin(BuiltinTy::Stroke) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_STROKE_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Margin) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_MARGIN_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Inset) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_INSET_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Outset) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_OUTSET_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Radius) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_RADIUS_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::TextFont) if self.dict_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Dict(&FLOW_TEXT_FONT_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Value(ins_ty) => {
|
||||
// todo: deduplicate checking early
|
||||
if self.value_as_iface() {
|
||||
match &ins_ty.val {
|
||||
Value::Module(val) => {
|
||||
self.checker
|
||||
.check(Iface::ModuleVal { val, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Value::Dict(dict) => {
|
||||
self.checker
|
||||
.check(Iface::Value { val: dict, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Value::Type(ty) => {
|
||||
self.checker
|
||||
.check(Iface::TypeType { val: ty, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Value::Func(func) => {
|
||||
self.checker
|
||||
.check(Iface::Func { val: func, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Value::None
|
||||
| Value::Auto
|
||||
| Value::Bool(_)
|
||||
| Value::Int(_)
|
||||
| Value::Float(_)
|
||||
| Value::Length(..)
|
||||
| Value::Angle(..)
|
||||
| Value::Ratio(..)
|
||||
| Value::Relative(..)
|
||||
| Value::Fraction(..)
|
||||
| Value::Color(..)
|
||||
| Value::Gradient(..)
|
||||
| Value::Tiling(..)
|
||||
| Value::Symbol(..)
|
||||
| Value::Version(..)
|
||||
| Value::Str(..)
|
||||
| Value::Bytes(..)
|
||||
| Value::Label(..)
|
||||
| Value::Datetime(..)
|
||||
| Value::Decimal(..)
|
||||
| Value::Duration(..)
|
||||
| Value::Content(..)
|
||||
| Value::Styles(..)
|
||||
| Value::Array(..)
|
||||
| Value::Args(..)
|
||||
| Value::Dyn(..) => {
|
||||
self.checker.check(
|
||||
Iface::Type {
|
||||
val: &ins_ty.val.ty(),
|
||||
at,
|
||||
},
|
||||
&mut self.ctx,
|
||||
pol,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// todo: more builtin types to check
|
||||
Ty::Builtin(BuiltinTy::Content(Some(elem))) if self.value_as_iface() => {
|
||||
self.checker
|
||||
.check(Iface::Content { val: elem, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Content(..)) if self.value_as_iface() => {
|
||||
let ty = Type::of::<typst::foundations::Content>();
|
||||
self.checker
|
||||
.check(Iface::Type { val: &ty, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Type(ty)) if self.value_as_iface() => {
|
||||
// todo: distinguish between element and function
|
||||
self.checker
|
||||
.check(Iface::Type { val: ty, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Element(elem)) if self.value_as_iface() => {
|
||||
self.checker.check(
|
||||
Iface::Func {
|
||||
val: &Func::from(*elem),
|
||||
at,
|
||||
},
|
||||
&mut self.ctx,
|
||||
pol,
|
||||
);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Module(module)) => {
|
||||
if let Decl::Module(m) = module.as_ref() {
|
||||
self.checker
|
||||
.check(Iface::Module { val: m.fid, at }, &mut self.ctx, pol);
|
||||
}
|
||||
}
|
||||
// Ty::Func(..) if self.value_as_iface() => {
|
||||
// self.checker.check(Iface::Type(sig), &mut self.ctx, pol);
|
||||
// }
|
||||
// Ty::Array(sig) if self.array_as_sig() => {
|
||||
// // let sig = FlowSignature::array_cons(*sig.clone(), true);
|
||||
// self.checker.check(Iface::ArrayCons(sig), &mut self.ctx, pol);
|
||||
// }
|
||||
// // todo: tuple
|
||||
// Ty::Tuple(_) => {}
|
||||
Ty::Dict(sig) if self.dict_as_iface() => {
|
||||
// self.check_dict_signature(sig, pol, self.checker);
|
||||
self.checker.check(Iface::Dict(sig), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Tuple(sig) if self.array_as_iface() => {
|
||||
// self.check_dict_signature(sig, pol, self.checker);
|
||||
self.checker.check(Iface::Tuple(sig), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Array(sig) if self.array_as_iface() => {
|
||||
// self.check_dict_signature(sig, pol, self.checker);
|
||||
self.checker.check(Iface::Array(sig), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Dict(..) => {
|
||||
self.checker.check(
|
||||
Iface::Type {
|
||||
val: &Type::of::<typst::foundations::Dict>(),
|
||||
at,
|
||||
},
|
||||
&mut self.ctx,
|
||||
pol,
|
||||
);
|
||||
}
|
||||
Ty::Tuple(..) | Ty::Array(..) => {
|
||||
self.checker.check(
|
||||
Iface::Type {
|
||||
val: &Type::of::<typst::foundations::Array>(),
|
||||
at,
|
||||
},
|
||||
&mut self.ctx,
|
||||
pol,
|
||||
);
|
||||
}
|
||||
Ty::Var(..) => at.bounds(pol, self),
|
||||
_ if at.has_bounds() => at.bounds(pol, self),
|
||||
_ => {}
|
||||
}
|
||||
// Ty::Select(sel) => sel.ty.bounds(pol, &mut MethodDriver(self,
|
||||
// &sel.select)), // todo: calculate these operators
|
||||
// Ty::Unary(_) => {}
|
||||
// Ty::Binary(_) => {}
|
||||
// Ty::If(_) => {}
|
||||
}
|
||||
}
|
135
crates/tinymist-analysis/src/ty/mod.rs
Normal file
135
crates/tinymist-analysis/src/ty/mod.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
//! Types and type operations for Typst.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
mod apply;
|
||||
mod bound;
|
||||
mod builtin;
|
||||
mod convert;
|
||||
mod def;
|
||||
mod describe;
|
||||
mod iface;
|
||||
mod mutate;
|
||||
mod prelude;
|
||||
mod select;
|
||||
mod sig;
|
||||
mod simplify;
|
||||
mod subst;
|
||||
|
||||
pub use apply::*;
|
||||
pub use bound::*;
|
||||
pub use builtin::*;
|
||||
pub use convert::*;
|
||||
pub use def::*;
|
||||
pub use iface::*;
|
||||
pub use mutate::*;
|
||||
pub use select::*;
|
||||
pub use sig::*;
|
||||
|
||||
use typst::foundations::{self, Func, Module, Value};
|
||||
use typst::syntax::FileId;
|
||||
|
||||
/// A type context.
|
||||
pub trait TyCtx {
|
||||
/// Get local binding of a variable.
|
||||
fn local_bind_of(&self, _var: &Interned<TypeVar>) -> Option<Ty>;
|
||||
/// Get the type of a variable.
|
||||
fn global_bounds(&self, _var: &Interned<TypeVar>, _pol: bool) -> Option<DynTypeBounds>;
|
||||
}
|
||||
|
||||
impl TyCtx for () {
|
||||
fn local_bind_of(&self, _var: &Interned<TypeVar>) -> Option<Ty> {
|
||||
None
|
||||
}
|
||||
|
||||
fn global_bounds(&self, _var: &Interned<TypeVar>, _pol: bool) -> Option<DynTypeBounds> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable type context.
|
||||
pub trait TyCtxMut: TyCtx {
|
||||
/// The type of a snapshot of the scope.
|
||||
type Snap;
|
||||
|
||||
/// Start a new scope.
|
||||
#[must_use]
|
||||
fn start_scope(&mut self) -> Self::Snap;
|
||||
/// End the current scope.
|
||||
fn end_scope(&mut self, snap: Self::Snap);
|
||||
/// Execute a function with a new scope.
|
||||
fn with_scope<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
let snap = self.start_scope();
|
||||
let res = f(self);
|
||||
self.end_scope(snap);
|
||||
res
|
||||
}
|
||||
|
||||
/// Bind a variable locally.
|
||||
fn bind_local(&mut self, var: &Interned<TypeVar>, ty: Ty);
|
||||
/// Get the type of a runtime function.
|
||||
fn type_of_func(&mut self, func: &Func) -> Option<Interned<SigTy>>;
|
||||
/// Get the type of a runtime value.
|
||||
fn type_of_value(&mut self, val: &Value) -> Ty;
|
||||
/// Get the type of a runtime dict.
|
||||
fn type_of_dict(&mut self, dict: &foundations::Dict) -> Interned<RecordTy> {
|
||||
let ty = self.type_of_value(&Value::Dict(dict.clone()));
|
||||
let Ty::Dict(ty) = ty else {
|
||||
panic!("expected dict type, found {ty:?}");
|
||||
};
|
||||
ty
|
||||
}
|
||||
/// Get the type of a runtime module.
|
||||
fn type_of_module(&mut self, module: &Module) -> Interned<RecordTy> {
|
||||
let ty = self.type_of_value(&Value::Module(module.clone()));
|
||||
let Ty::Dict(ty) = ty else {
|
||||
panic!("expected dict type, found {ty:?}");
|
||||
};
|
||||
ty
|
||||
}
|
||||
/// Check a module item.
|
||||
fn check_module_item(&mut self, module: FileId, key: &StrRef) -> Option<Ty>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::adt::interner::Interned;
|
||||
use crate::syntax::Decl;
|
||||
|
||||
pub fn var_ins(s: &str) -> Ty {
|
||||
Ty::Var(TypeVar::new(s.into(), Decl::lit(s).into()))
|
||||
}
|
||||
|
||||
pub fn str_sig(
|
||||
pos: &[&str],
|
||||
named: &[(&str, &str)],
|
||||
rest: Option<&str>,
|
||||
ret: Option<&str>,
|
||||
) -> Interned<SigTy> {
|
||||
let pos = pos.iter().map(|s| var_ins(s));
|
||||
let named = named.iter().map(|(n, t)| ((*n).into(), var_ins(t)));
|
||||
let rest = rest.map(var_ins);
|
||||
let ret = ret.map(var_ins);
|
||||
SigTy::new(pos, named, None, rest, ret).into()
|
||||
}
|
||||
|
||||
// args*, (keys: values)*, ...rest -> ret
|
||||
macro_rules! literal_sig {
|
||||
($($pos:ident),* $(!$named:ident: $named_ty:ident),* $(,)? ...$rest:ident -> $ret:ident) => {
|
||||
str_sig(&[$(stringify!($pos)),*], &[$((stringify!($named), stringify!($named_ty))),*], Some(stringify!($rest)), Some(stringify!($ret)))
|
||||
};
|
||||
($($pos:ident),* $(!$named:ident: $named_ty:ident),* $(,)? -> $ret:ident) => {
|
||||
str_sig(&[$(stringify!($pos)),*], &[$((stringify!($named), stringify!($named_ty))),*], None, Some(stringify!($ret)))
|
||||
};
|
||||
($($pos:ident),* $(!$named:ident: $named_ty:ident),* $(,)? ...$rest:ident) => {
|
||||
str_sig(&[$(stringify!($pos)),*], &[$((stringify!($named), stringify!($named_ty))),*], Some(stringify!($rest)), None)
|
||||
};
|
||||
($($pos:ident),* $(!$named:ident: $named_ty:ident),* $(,)?) => {
|
||||
str_sig(&[$(stringify!($pos)),*], &[$((stringify!($named), stringify!($named_ty))),*], None, None)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use literal_sig;
|
||||
pub(crate) use literal_sig as literal_args;
|
||||
}
|
167
crates/tinymist-analysis/src/ty/mutate.rs
Normal file
167
crates/tinymist-analysis/src/ty/mutate.rs
Normal file
|
@ -0,0 +1,167 @@
|
|||
use crate::ty::def::*;
|
||||
|
||||
pub trait TyMutator {
|
||||
fn mutate(&mut self, ty: &Ty, pol: bool) -> Option<Ty> {
|
||||
self.mutate_rec(ty, pol)
|
||||
}
|
||||
fn mutate_rec(&mut self, ty: &Ty, pol: bool) -> Option<Ty> {
|
||||
use Ty::*;
|
||||
match ty {
|
||||
Value(..) | Any | Boolean(..) | Builtin(..) => None,
|
||||
Union(v) => Some(Union(self.mutate_vec(v, pol)?)),
|
||||
Var(..) | Let(..) => None,
|
||||
Array(arr) => Some(Array(self.mutate(arr, pol)?.into())),
|
||||
Dict(dict) => Some(Dict(self.mutate_record(dict, pol)?.into())),
|
||||
Tuple(tup) => Some(Tuple(self.mutate_vec(tup, pol)?)),
|
||||
Func(func) => Some(Func(self.mutate_func(func, pol)?.into())),
|
||||
Args(args) => Some(Args(self.mutate_func(args, pol)?.into())),
|
||||
Pattern(pat) => Some(Pattern(self.mutate_func(pat, pol)?.into())),
|
||||
Param(param) => Some(Param(self.mutate_param(param, pol)?.into())),
|
||||
Select(sel) => Some(Select(self.mutate_select(sel, pol)?.into())),
|
||||
With(sig) => Some(With(self.mutate_with_sig(sig, pol)?.into())),
|
||||
Unary(unary) => Some(Unary(self.mutate_unary(unary, pol)?.into())),
|
||||
Binary(binary) => Some(Binary(self.mutate_binary(binary, pol)?.into())),
|
||||
If(if_expr) => Some(If(self.mutate_if(if_expr, pol)?.into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn mutate_vec(&mut self, ty: &[Ty], pol: bool) -> Option<Interned<Vec<Ty>>> {
|
||||
let mut mutated = false;
|
||||
|
||||
let mut types = Vec::with_capacity(ty.len());
|
||||
for ty in ty.iter() {
|
||||
match self.mutate(ty, pol) {
|
||||
Some(ty) => {
|
||||
types.push(ty);
|
||||
mutated = true;
|
||||
}
|
||||
None => types.push(ty.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
if mutated {
|
||||
Some(types.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn mutate_option(&mut self, ty: Option<&Ty>, pol: bool) -> Option<Option<Ty>> {
|
||||
match ty {
|
||||
Some(ty) => self.mutate(ty, pol).map(Some),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mutate_func(&mut self, ty: &Interned<SigTy>, pol: bool) -> Option<SigTy> {
|
||||
let types = self.mutate_vec(&ty.inputs, pol);
|
||||
let ret = self.mutate_option(ty.body.as_ref(), pol);
|
||||
|
||||
if types.is_none() && ret.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sig = ty.as_ref().clone();
|
||||
let types = types.unwrap_or_else(|| ty.inputs.clone());
|
||||
let ret = ret.unwrap_or_else(|| ty.body.clone());
|
||||
Some(SigTy {
|
||||
inputs: types,
|
||||
body: ret,
|
||||
..sig
|
||||
})
|
||||
}
|
||||
|
||||
fn mutate_param(&mut self, param: &Interned<ParamTy>, pol: bool) -> Option<ParamTy> {
|
||||
let ty = self.mutate(¶m.ty, pol)?;
|
||||
let mut param = param.as_ref().clone();
|
||||
param.ty = ty;
|
||||
Some(param)
|
||||
}
|
||||
|
||||
fn mutate_record(&mut self, record: &Interned<RecordTy>, pol: bool) -> Option<RecordTy> {
|
||||
let types = self.mutate_vec(&record.types, pol)?;
|
||||
|
||||
let rec = record.as_ref().clone();
|
||||
Some(RecordTy { types, ..rec })
|
||||
}
|
||||
|
||||
fn mutate_with_sig(&mut self, ty: &Interned<SigWithTy>, pol: bool) -> Option<SigWithTy> {
|
||||
let sig = self.mutate(ty.sig.as_ref(), pol);
|
||||
let with = self.mutate_func(&ty.with, pol);
|
||||
|
||||
if sig.is_none() && with.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sig = sig.map(Interned::new).unwrap_or_else(|| ty.sig.clone());
|
||||
let with = with.map(Interned::new).unwrap_or_else(|| ty.with.clone());
|
||||
|
||||
Some(SigWithTy { sig, with })
|
||||
}
|
||||
|
||||
fn mutate_unary(&mut self, ty: &Interned<TypeUnary>, pol: bool) -> Option<TypeUnary> {
|
||||
let lhs = self.mutate(&ty.lhs, pol)?;
|
||||
|
||||
Some(TypeUnary { lhs, op: ty.op })
|
||||
}
|
||||
|
||||
fn mutate_binary(&mut self, ty: &Interned<TypeBinary>, pol: bool) -> Option<TypeBinary> {
|
||||
let (lhs, rhs) = &ty.operands;
|
||||
|
||||
let x = self.mutate(lhs, pol);
|
||||
let y = self.mutate(rhs, pol);
|
||||
|
||||
if x.is_none() && y.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let lhs = x.unwrap_or_else(|| lhs.clone());
|
||||
let rhs = y.unwrap_or_else(|| rhs.clone());
|
||||
|
||||
Some(TypeBinary {
|
||||
operands: (lhs, rhs),
|
||||
op: ty.op,
|
||||
})
|
||||
}
|
||||
|
||||
fn mutate_if(&mut self, ty: &Interned<IfTy>, pol: bool) -> Option<IfTy> {
|
||||
let cond = self.mutate(ty.cond.as_ref(), pol);
|
||||
let then = self.mutate(ty.then.as_ref(), pol);
|
||||
let else_ = self.mutate(ty.else_.as_ref(), pol);
|
||||
|
||||
if cond.is_none() && then.is_none() && else_.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cond = cond.map(Interned::new).unwrap_or_else(|| ty.cond.clone());
|
||||
let then = then.map(Interned::new).unwrap_or_else(|| ty.then.clone());
|
||||
let else_ = else_.map(Interned::new).unwrap_or_else(|| ty.else_.clone());
|
||||
|
||||
Some(IfTy { cond, then, else_ })
|
||||
}
|
||||
|
||||
fn mutate_select(&mut self, ty: &Interned<SelectTy>, pol: bool) -> Option<SelectTy> {
|
||||
let target = self.mutate(ty.ty.as_ref(), pol)?.into();
|
||||
|
||||
Some(SelectTy {
|
||||
ty: target,
|
||||
select: ty.select.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TyMutator for T
|
||||
where
|
||||
T: FnMut(&Ty, bool) -> Option<Ty>,
|
||||
{
|
||||
fn mutate(&mut self, ty: &Ty, pol: bool) -> Option<Ty> {
|
||||
self(ty, pol)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
/// Mutate the given type.
|
||||
pub fn mutate(&self, pol: bool, checker: &mut impl TyMutator) -> Option<Ty> {
|
||||
checker.mutate(self, pol)
|
||||
}
|
||||
}
|
8
crates/tinymist-analysis/src/ty/prelude.rs
Normal file
8
crates/tinymist-analysis/src/ty/prelude.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
pub use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||
pub use tinymist_std::DefId;
|
||||
pub use typst::foundations::Value;
|
||||
|
||||
pub use super::builtin::*;
|
||||
pub use super::def::*;
|
35
crates/tinymist-analysis/src/ty/select.rs
Normal file
35
crates/tinymist-analysis/src/ty/select.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use super::{Iface, IfaceChecker};
|
||||
use crate::ty::def::*;
|
||||
|
||||
pub trait SelectChecker: TyCtx {
|
||||
fn select(&mut self, sig: Iface, key: &Interned<str>, pol: bool);
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
/// Select the given type with the given key.
|
||||
pub fn select(&self, key: &Interned<str>, pol: bool, checker: &mut impl SelectChecker) {
|
||||
SelectKeyChecker(checker, key).ty(self, pol);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(0)]
|
||||
pub struct SelectKeyChecker<'a, T: TyCtx>(&'a mut T, &'a Interned<str>);
|
||||
|
||||
impl<T: SelectChecker> SelectKeyChecker<'_, T> {
|
||||
fn ty(&mut self, ty: &Ty, pol: bool) {
|
||||
ty.iface_surface(pol, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SelectChecker> IfaceChecker for SelectKeyChecker<'_, T> {
|
||||
fn check(
|
||||
&mut self,
|
||||
iface: Iface,
|
||||
_ctx: &mut super::IfaceCheckContext,
|
||||
pol: bool,
|
||||
) -> Option<()> {
|
||||
self.0.select(iface, self.1, pol);
|
||||
Some(())
|
||||
}
|
||||
}
|
337
crates/tinymist-analysis/src/ty/sig.rs
Normal file
337
crates/tinymist-analysis/src/ty/sig.rs
Normal file
|
@ -0,0 +1,337 @@
|
|||
use typst::foundations::{Func, Value};
|
||||
|
||||
use super::BoundChecker;
|
||||
use crate::ty::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Sig<'a> {
|
||||
Builtin(BuiltinSig<'a>),
|
||||
Type(&'a Interned<SigTy>),
|
||||
TypeCons {
|
||||
val: &'a typst::foundations::Type,
|
||||
at: &'a Ty,
|
||||
},
|
||||
ArrayCons(&'a TyRef),
|
||||
TupleCons(&'a Interned<Vec<Ty>>),
|
||||
DictCons(&'a Interned<RecordTy>),
|
||||
Value {
|
||||
val: &'a Func,
|
||||
at: &'a Ty,
|
||||
},
|
||||
Partialize(&'a Sig<'a>),
|
||||
With {
|
||||
sig: &'a Sig<'a>,
|
||||
withs: &'a Vec<Interned<ArgsTy>>,
|
||||
at: &'a Ty,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct SigShape<'a> {
|
||||
pub sig: Interned<SigTy>,
|
||||
pub withs: Option<&'a Vec<Interned<SigTy>>>,
|
||||
}
|
||||
|
||||
impl<'a> Sig<'a> {
|
||||
pub fn ty(self) -> Option<Ty> {
|
||||
Some(match self {
|
||||
Sig::Builtin(_) => return None,
|
||||
Sig::Type(t) => Ty::Func(t.clone()),
|
||||
Sig::ArrayCons(t) => Ty::Array(t.clone()),
|
||||
Sig::TupleCons(t) => Ty::Tuple(t.clone()),
|
||||
Sig::DictCons(t) => Ty::Dict(t.clone()),
|
||||
Sig::TypeCons { val, .. } => Ty::Builtin(BuiltinTy::Type(*val)),
|
||||
Sig::Value { at, .. } => at.clone(),
|
||||
Sig::With { at, .. } => at.clone(),
|
||||
Sig::Partialize(..) => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shape(self, ctx: &mut impl TyCtxMut) -> Option<SigShape<'a>> {
|
||||
let (sig, _is_partialize) = match self {
|
||||
Sig::Partialize(sig) => (*sig, true),
|
||||
sig => (sig, false),
|
||||
};
|
||||
|
||||
let (cano_sig, withs) = match sig {
|
||||
Sig::With { sig, withs, .. } => (*sig, Some(withs)),
|
||||
sig => (sig, None),
|
||||
};
|
||||
|
||||
let sig = match cano_sig {
|
||||
Sig::Builtin(_) => return None,
|
||||
Sig::ArrayCons(arr) => SigTy::array_cons(arr.as_ref().clone(), false),
|
||||
Sig::TupleCons(tup) => SigTy::tuple_cons(tup.clone(), false),
|
||||
Sig::DictCons(dict) => SigTy::dict_cons(dict, false),
|
||||
Sig::TypeCons { val, .. } => ctx.type_of_func(&val.constructor().ok()?)?,
|
||||
Sig::Value { val, .. } => ctx.type_of_func(val)?,
|
||||
// todo
|
||||
Sig::Partialize(..) => return None,
|
||||
Sig::With { .. } => return None,
|
||||
Sig::Type(ty) => ty.clone(),
|
||||
};
|
||||
|
||||
Some(SigShape { sig, withs })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SigSurfaceKind {
|
||||
Call,
|
||||
Array,
|
||||
Dict,
|
||||
ArrayOrDict,
|
||||
}
|
||||
|
||||
pub trait SigChecker: TyCtx {
|
||||
fn check(&mut self, sig: Sig, args: &mut SigCheckContext, pol: bool) -> Option<()>;
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
/// Iterate over the signatures of the given type.
|
||||
pub fn sig_surface(&self, pol: bool, sig_kind: SigSurfaceKind, checker: &mut impl SigChecker) {
|
||||
let ctx = SigCheckContext {
|
||||
sig_kind,
|
||||
args: Vec::new(),
|
||||
at: TyRef::new(Ty::Any),
|
||||
};
|
||||
|
||||
SigCheckDriver { ctx, checker }.ty(self, pol);
|
||||
}
|
||||
|
||||
/// Get the signature representation of the given type.
|
||||
pub fn sig_repr(&self, pol: bool, ctx: &mut impl TyCtxMut) -> Option<Interned<SigTy>> {
|
||||
// todo: union sig
|
||||
// let mut pos = vec![];
|
||||
// let mut named = HashMap::new();
|
||||
// let mut rest = None;
|
||||
// let mut ret = None;
|
||||
|
||||
let mut primary = None;
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(0)]
|
||||
struct SigReprDriver<'a, C: TyCtxMut>(&'a mut C, &'a mut Option<Interned<SigTy>>);
|
||||
|
||||
impl<C: TyCtxMut> SigChecker for SigReprDriver<'_, C> {
|
||||
fn check(&mut self, sig: Sig, _ctx: &mut SigCheckContext, _pol: bool) -> Option<()> {
|
||||
let sig = sig.shape(self.0)?;
|
||||
*self.1 = Some(sig.sig.clone());
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
self.sig_surface(
|
||||
pol,
|
||||
SigSurfaceKind::Call,
|
||||
// todo: bind type context
|
||||
&mut SigReprDriver(ctx, &mut primary),
|
||||
);
|
||||
|
||||
primary
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SigCheckContext {
|
||||
pub sig_kind: SigSurfaceKind,
|
||||
pub args: Vec<Interned<SigTy>>,
|
||||
pub at: TyRef,
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(checker)]
|
||||
pub struct SigCheckDriver<'a> {
|
||||
ctx: SigCheckContext,
|
||||
checker: &'a mut dyn SigChecker,
|
||||
}
|
||||
|
||||
impl SigCheckDriver<'_> {
|
||||
fn func_as_sig(&self) -> bool {
|
||||
matches!(self.ctx.sig_kind, SigSurfaceKind::Call)
|
||||
}
|
||||
|
||||
fn array_as_sig(&self) -> bool {
|
||||
matches!(
|
||||
self.ctx.sig_kind,
|
||||
SigSurfaceKind::Array | SigSurfaceKind::ArrayOrDict
|
||||
)
|
||||
}
|
||||
|
||||
fn dict_as_sig(&self) -> bool {
|
||||
matches!(
|
||||
self.ctx.sig_kind,
|
||||
SigSurfaceKind::Dict | SigSurfaceKind::ArrayOrDict
|
||||
)
|
||||
}
|
||||
|
||||
fn ty(&mut self, at: &Ty, pol: bool) {
|
||||
crate::log_debug_ct!("check sig: {at:?}");
|
||||
match at {
|
||||
Ty::Builtin(BuiltinTy::Stroke) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_STROKE_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Margin) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_MARGIN_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Inset) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_INSET_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Outset) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_OUTSET_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Radius) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_RADIUS_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::TextFont) if self.dict_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::DictCons(&FLOW_TEXT_FONT_DICT), &mut self.ctx, pol);
|
||||
}
|
||||
// todo: deduplicate checking early
|
||||
Ty::Value(ins_ty) => {
|
||||
if self.func_as_sig() {
|
||||
match &ins_ty.val {
|
||||
Value::Func(func) => {
|
||||
self.checker
|
||||
.check(Sig::Value { val: func, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Value::Type(ty) => {
|
||||
self.checker
|
||||
.check(Sig::TypeCons { val: ty, at }, &mut self.ctx, pol);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Type(b_ty)) if self.func_as_sig() => {
|
||||
// todo: distinguish between element and function
|
||||
self.checker
|
||||
.check(Sig::TypeCons { val: b_ty, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Element(elem)) if self.func_as_sig() => {
|
||||
// todo: distinguish between element and function
|
||||
let func = (*elem).into();
|
||||
self.checker
|
||||
.check(Sig::Value { val: &func, at }, &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Func(sig) if self.func_as_sig() => {
|
||||
self.checker.check(Sig::Type(sig), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Array(arr) if self.array_as_sig() => {
|
||||
self.checker.check(Sig::ArrayCons(arr), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Tuple(elems) if self.array_as_sig() => {
|
||||
self.checker
|
||||
.check(Sig::TupleCons(elems), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::Dict(sig) if self.dict_as_sig() => {
|
||||
// self.check_dict_signature(sig, pol, self.checker);
|
||||
self.checker.check(Sig::DictCons(sig), &mut self.ctx, pol);
|
||||
}
|
||||
Ty::With(sig) if self.func_as_sig() => {
|
||||
self.ctx.args.push(sig.with.clone());
|
||||
self.ty(&sig.sig, pol);
|
||||
self.ctx.args.pop();
|
||||
}
|
||||
Ty::Select(sel) => sel.ty.bounds(pol, &mut MethodDriver(self, &sel.select)),
|
||||
// todo: calculate these operators
|
||||
Ty::Unary(_) => {}
|
||||
Ty::Binary(_) => {}
|
||||
Ty::If(_) => {}
|
||||
Ty::Param(param) => {
|
||||
// todo: keep type information
|
||||
self.ty(¶m.ty, pol);
|
||||
}
|
||||
_ if at.has_bounds() => at.bounds(pol, self),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundChecker for SigCheckDriver<'_> {
|
||||
fn collect(&mut self, ty: &Ty, pol: bool) {
|
||||
crate::log_debug_ct!("sig bounds: {ty:?}");
|
||||
self.ty(ty, pol);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BindTyCtx)]
|
||||
#[bind(0)]
|
||||
struct MethodDriver<'a, 'b>(&'a mut SigCheckDriver<'b>, &'a StrRef);
|
||||
|
||||
impl MethodDriver<'_, '_> {
|
||||
fn is_binder(&self) -> bool {
|
||||
matches!(self.1.as_ref(), "with" | "where")
|
||||
}
|
||||
|
||||
fn array_method(&mut self, ty: &Ty, pol: bool) {
|
||||
let method = match self.1.as_ref() {
|
||||
"map" => BuiltinSig::TupleMap(ty),
|
||||
"at" => BuiltinSig::TupleAt(ty),
|
||||
_ => return,
|
||||
};
|
||||
self.0
|
||||
.checker
|
||||
.check(Sig::Builtin(method), &mut self.0.ctx, pol);
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundChecker for MethodDriver<'_, '_> {
|
||||
fn collect(&mut self, ty: &Ty, pol: bool) {
|
||||
crate::log_debug_ct!("check method: {ty:?}.{}", self.1.as_ref());
|
||||
match ty {
|
||||
// todo: deduplicate checking early
|
||||
Ty::Value(v) => {
|
||||
match &v.val {
|
||||
Value::Func(func) => {
|
||||
if self.is_binder() {
|
||||
self.0.checker.check(
|
||||
Sig::Partialize(&Sig::Value { val: func, at: ty }),
|
||||
&mut self.0.ctx,
|
||||
pol,
|
||||
);
|
||||
} else {
|
||||
// todo: general select operator
|
||||
}
|
||||
}
|
||||
Value::Array(..) => self.array_method(ty, pol),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ty::Builtin(BuiltinTy::Element(elem)) => {
|
||||
// todo: distinguish between element and function
|
||||
if self.is_binder() {
|
||||
let func = (*elem).into();
|
||||
self.0.checker.check(
|
||||
Sig::Partialize(&Sig::Value { val: &func, at: ty }),
|
||||
&mut self.0.ctx,
|
||||
pol,
|
||||
);
|
||||
} else {
|
||||
// todo: general select operator
|
||||
}
|
||||
}
|
||||
Ty::Func(sig) => {
|
||||
if self.is_binder() {
|
||||
self.0
|
||||
.checker
|
||||
.check(Sig::Partialize(&Sig::Type(sig)), &mut self.0.ctx, pol);
|
||||
} else {
|
||||
// todo: general select operator
|
||||
}
|
||||
}
|
||||
Ty::With(w) => {
|
||||
self.0.ctx.args.push(w.with.clone());
|
||||
w.sig.bounds(pol, self);
|
||||
self.0.ctx.args.pop();
|
||||
}
|
||||
Ty::Tuple(..) => self.array_method(ty, pol),
|
||||
Ty::Array(..) => self.array_method(ty, pol),
|
||||
// todo: general select operator
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
312
crates/tinymist-analysis/src/ty/simplify.rs
Normal file
312
crates/tinymist-analysis/src/ty/simplify.rs
Normal file
|
@ -0,0 +1,312 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use ecow::EcoVec;
|
||||
|
||||
use crate::{syntax::DeclExpr, ty::prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct CompactTy {
|
||||
equiv_vars: HashSet<DefId>,
|
||||
primitives: HashSet<Ty>,
|
||||
recursives: HashMap<DefId, CompactTy>,
|
||||
signatures: Vec<Interned<SigTy>>,
|
||||
|
||||
is_final: bool,
|
||||
}
|
||||
|
||||
impl TypeInfo {
|
||||
/// Simplify (Canonicalize) the given type with the given type scheme.
|
||||
pub fn simplify(&self, ty: Ty, principal: bool) -> Ty {
|
||||
let mut cache = self.cano_cache.lock();
|
||||
let cache = &mut *cache;
|
||||
|
||||
cache.cano_local_cache.clear();
|
||||
cache.positives.clear();
|
||||
cache.negatives.clear();
|
||||
|
||||
let mut worker = TypeSimplifier {
|
||||
principal,
|
||||
vars: &self.vars,
|
||||
cano_cache: &mut cache.cano_cache,
|
||||
cano_local_cache: &mut cache.cano_local_cache,
|
||||
|
||||
positives: &mut cache.positives,
|
||||
negatives: &mut cache.negatives,
|
||||
};
|
||||
|
||||
worker.simplify(ty, principal)
|
||||
}
|
||||
}
|
||||
|
||||
struct TypeSimplifier<'a, 'b> {
|
||||
principal: bool,
|
||||
|
||||
vars: &'a FxHashMap<DeclExpr, TypeVarBounds>,
|
||||
|
||||
cano_cache: &'b mut FxHashMap<(Ty, bool), Ty>,
|
||||
cano_local_cache: &'b mut FxHashMap<(DeclExpr, bool), Ty>,
|
||||
negatives: &'b mut FxHashSet<DeclExpr>,
|
||||
positives: &'b mut FxHashSet<DeclExpr>,
|
||||
}
|
||||
|
||||
impl TypeSimplifier<'_, '_> {
|
||||
fn simplify(&mut self, ty: Ty, principal: bool) -> Ty {
|
||||
if let Some(cano) = self.cano_cache.get(&(ty.clone(), principal)) {
|
||||
return cano.clone();
|
||||
}
|
||||
|
||||
self.analyze(&ty, true);
|
||||
|
||||
self.transform(&ty, true)
|
||||
}
|
||||
|
||||
fn analyze(&mut self, ty: &Ty, pol: bool) {
|
||||
match ty {
|
||||
Ty::Var(var) => {
|
||||
let w = self.vars.get(&var.def).unwrap();
|
||||
match &w.bounds {
|
||||
FlowVarKind::Strong(w) | FlowVarKind::Weak(w) => {
|
||||
let bounds = w.read();
|
||||
let inserted = if pol {
|
||||
self.positives.insert(var.def.clone())
|
||||
} else {
|
||||
self.negatives.insert(var.def.clone())
|
||||
};
|
||||
if !inserted {
|
||||
return;
|
||||
}
|
||||
|
||||
if pol {
|
||||
for lb in bounds.lbs.iter() {
|
||||
self.analyze(lb, pol);
|
||||
}
|
||||
} else {
|
||||
for ub in bounds.ubs.iter() {
|
||||
self.analyze(ub, pol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ty::Func(func) => {
|
||||
for input_ty in func.inputs() {
|
||||
self.analyze(input_ty, !pol);
|
||||
}
|
||||
if let Some(ret_ty) = &func.body {
|
||||
self.analyze(ret_ty, pol);
|
||||
}
|
||||
}
|
||||
Ty::Dict(record) => {
|
||||
for member in record.types.iter() {
|
||||
self.analyze(member, pol);
|
||||
}
|
||||
}
|
||||
Ty::Tuple(elems) => {
|
||||
for elem in elems.iter() {
|
||||
self.analyze(elem, pol);
|
||||
}
|
||||
}
|
||||
Ty::Array(arr) => {
|
||||
self.analyze(arr, pol);
|
||||
}
|
||||
Ty::With(with) => {
|
||||
self.analyze(&with.sig, pol);
|
||||
for input in with.with.inputs() {
|
||||
self.analyze(input, pol);
|
||||
}
|
||||
}
|
||||
Ty::Args(args) => {
|
||||
for input in args.inputs() {
|
||||
self.analyze(input, pol);
|
||||
}
|
||||
}
|
||||
Ty::Pattern(pat) => {
|
||||
for input in pat.inputs() {
|
||||
self.analyze(input, pol);
|
||||
}
|
||||
}
|
||||
Ty::Unary(unary) => self.analyze(&unary.lhs, pol),
|
||||
Ty::Binary(binary) => {
|
||||
let [lhs, rhs] = binary.operands();
|
||||
self.analyze(lhs, pol);
|
||||
self.analyze(rhs, pol);
|
||||
}
|
||||
Ty::If(if_expr) => {
|
||||
self.analyze(&if_expr.cond, pol);
|
||||
self.analyze(&if_expr.then, pol);
|
||||
self.analyze(&if_expr.else_, pol);
|
||||
}
|
||||
Ty::Union(types) => {
|
||||
for ty in types.iter() {
|
||||
self.analyze(ty, pol);
|
||||
}
|
||||
}
|
||||
Ty::Select(select) => {
|
||||
self.analyze(&select.ty, pol);
|
||||
}
|
||||
Ty::Let(bounds) => {
|
||||
for lb in bounds.lbs.iter() {
|
||||
self.analyze(lb, !pol);
|
||||
}
|
||||
for ub in bounds.ubs.iter() {
|
||||
self.analyze(ub, pol);
|
||||
}
|
||||
}
|
||||
Ty::Param(param) => {
|
||||
self.analyze(¶m.ty, pol);
|
||||
}
|
||||
Ty::Value(_v) => {}
|
||||
Ty::Any => {}
|
||||
Ty::Boolean(_) => {}
|
||||
Ty::Builtin(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn transform(&mut self, ty: &Ty, pol: bool) -> Ty {
|
||||
match ty {
|
||||
Ty::Let(bounds) => self.transform_let(bounds.lbs.iter(), bounds.ubs.iter(), None, pol),
|
||||
Ty::Var(var) => {
|
||||
if let Some(cano) = self
|
||||
.cano_local_cache
|
||||
.get(&(var.def.clone(), self.principal))
|
||||
{
|
||||
return cano.clone();
|
||||
}
|
||||
// todo: avoid cycle
|
||||
self.cano_local_cache
|
||||
.insert((var.def.clone(), self.principal), Ty::Any);
|
||||
|
||||
let res = match &self.vars.get(&var.def).unwrap().bounds {
|
||||
FlowVarKind::Strong(w) | FlowVarKind::Weak(w) => {
|
||||
let w = w.read();
|
||||
|
||||
self.transform_let(w.lbs.iter(), w.ubs.iter(), Some(&var.def), pol)
|
||||
}
|
||||
};
|
||||
|
||||
self.cano_local_cache
|
||||
.insert((var.def.clone(), self.principal), res.clone());
|
||||
|
||||
res
|
||||
}
|
||||
Ty::Func(func) => Ty::Func(self.transform_sig(func, pol)),
|
||||
Ty::Dict(record) => {
|
||||
let mut mutated = record.as_ref().clone();
|
||||
mutated.types = self.transform_seq(&mutated.types, pol);
|
||||
|
||||
Ty::Dict(mutated.into())
|
||||
}
|
||||
Ty::Tuple(tup) => Ty::Tuple(self.transform_seq(tup, pol)),
|
||||
Ty::Array(arr) => Ty::Array(self.transform(arr, pol).into()),
|
||||
Ty::With(with) => {
|
||||
let sig = self.transform(&with.sig, pol).into();
|
||||
// Negate the pol to make correct covariance
|
||||
let mutated = self.transform_sig(&with.with, !pol);
|
||||
|
||||
Ty::With(SigWithTy::new(sig, mutated))
|
||||
}
|
||||
// Negate the pol to make correct covariance
|
||||
// todo: negate?
|
||||
Ty::Args(args) => Ty::Args(self.transform_sig(args, !pol)),
|
||||
Ty::Pattern(pat) => Ty::Pattern(self.transform_sig(pat, !pol)),
|
||||
Ty::Unary(unary) => {
|
||||
Ty::Unary(TypeUnary::new(unary.op, self.transform(&unary.lhs, pol)))
|
||||
}
|
||||
Ty::Binary(binary) => {
|
||||
let [lhs, rhs] = binary.operands();
|
||||
let lhs = self.transform(lhs, pol);
|
||||
let rhs = self.transform(rhs, pol);
|
||||
|
||||
Ty::Binary(TypeBinary::new(binary.op, lhs, rhs))
|
||||
}
|
||||
Ty::If(if_ty) => Ty::If(IfTy::new(
|
||||
self.transform(&if_ty.cond, pol).into(),
|
||||
self.transform(&if_ty.then, pol).into(),
|
||||
self.transform(&if_ty.else_, pol).into(),
|
||||
)),
|
||||
Ty::Union(types) => {
|
||||
let seq = types.iter().map(|ty| self.transform(ty, pol));
|
||||
let seq_no_any = seq.filter(|ty| !matches!(ty, Ty::Any));
|
||||
let seq = seq_no_any.collect::<Vec<_>>();
|
||||
Ty::from_types(seq.into_iter())
|
||||
}
|
||||
Ty::Param(param) => {
|
||||
let mut param = param.as_ref().clone();
|
||||
param.ty = self.transform(¶m.ty, pol);
|
||||
|
||||
Ty::Param(param.into())
|
||||
}
|
||||
Ty::Select(sel) => {
|
||||
let mut sel = sel.as_ref().clone();
|
||||
sel.ty = self.transform(&sel.ty, pol).into();
|
||||
|
||||
Ty::Select(sel.into())
|
||||
}
|
||||
|
||||
Ty::Value(ins_ty) => Ty::Value(ins_ty.clone()),
|
||||
Ty::Any => Ty::Any,
|
||||
Ty::Boolean(truthiness) => Ty::Boolean(*truthiness),
|
||||
Ty::Builtin(ty) => Ty::Builtin(ty.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_seq(&mut self, types: &[Ty], pol: bool) -> Interned<Vec<Ty>> {
|
||||
let seq = types.iter().map(|ty| self.transform(ty, pol));
|
||||
seq.collect::<Vec<_>>().into()
|
||||
}
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
fn transform_let<'a>(
|
||||
&mut self,
|
||||
lbs_iter: impl ExactSizeIterator<Item = &'a Ty>,
|
||||
ubs_iter: impl ExactSizeIterator<Item = &'a Ty>,
|
||||
decl: Option<&DeclExpr>,
|
||||
pol: bool,
|
||||
) -> Ty {
|
||||
let mut lbs = HashSet::with_capacity(lbs_iter.len());
|
||||
let mut ubs = HashSet::with_capacity(ubs_iter.len());
|
||||
|
||||
crate::log_debug_ct!("transform let [principal={}]", self.principal);
|
||||
|
||||
if !self.principal || ((pol) && !decl.is_some_and(|decl| self.negatives.contains(decl))) {
|
||||
for lb in lbs_iter {
|
||||
lbs.insert(self.transform(lb, pol));
|
||||
}
|
||||
}
|
||||
if !self.principal || ((!pol) && !decl.is_some_and(|decl| self.positives.contains(decl))) {
|
||||
for ub in ubs_iter {
|
||||
ubs.insert(self.transform(ub, !pol));
|
||||
}
|
||||
}
|
||||
|
||||
if ubs.is_empty() {
|
||||
if lbs.len() == 1 {
|
||||
return lbs.into_iter().next().unwrap();
|
||||
}
|
||||
if lbs.is_empty() {
|
||||
return Ty::Any;
|
||||
}
|
||||
} else if lbs.is_empty() && ubs.len() == 1 {
|
||||
return ubs.into_iter().next().unwrap();
|
||||
}
|
||||
|
||||
// todo: bad performance
|
||||
let mut lbs: Vec<_> = lbs.into_iter().collect();
|
||||
lbs.sort();
|
||||
let mut ubs: Vec<_> = ubs.into_iter().collect();
|
||||
ubs.sort();
|
||||
|
||||
Ty::Let(TypeBounds { lbs, ubs }.into())
|
||||
}
|
||||
|
||||
fn transform_sig(&mut self, sig: &SigTy, pol: bool) -> Interned<SigTy> {
|
||||
let mut sig = sig.clone();
|
||||
sig.inputs = self.transform_seq(&sig.inputs, !pol);
|
||||
if let Some(ret) = &sig.body {
|
||||
sig.body = Some(self.transform(ret, pol));
|
||||
}
|
||||
|
||||
// todo: we can reduce one clone by early compare on sig.types
|
||||
sig.into()
|
||||
}
|
||||
}
|
112
crates/tinymist-analysis/src/ty/subst.rs
Normal file
112
crates/tinymist-analysis/src/ty/subst.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use super::{Sig, SigShape, TyMutator};
|
||||
use crate::ty::prelude::*;
|
||||
|
||||
impl Sig<'_> {
|
||||
pub fn call(&self, args: &Interned<ArgsTy>, pol: bool, ctx: &mut impl TyCtxMut) -> Option<Ty> {
|
||||
crate::log_debug_ct!("call {self:?} {args:?} {pol:?}");
|
||||
ctx.with_scope(|ctx| {
|
||||
let body = self.check_bind(args, ctx)?;
|
||||
|
||||
// Substitute the bound variables in the body or just body
|
||||
let mut checker = SubstituteChecker { ctx };
|
||||
Some(checker.ty(&body, pol).unwrap_or(body))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_bind(&self, args: &Interned<ArgsTy>, ctx: &mut impl TyCtxMut) -> Option<Ty> {
|
||||
let SigShape { sig, withs } = self.shape(ctx)?;
|
||||
|
||||
// todo: check if the signature has free variables
|
||||
// let has_free_vars = sig.has_free_variables;
|
||||
|
||||
for (arg_recv, arg_ins) in sig.matches(args, withs) {
|
||||
if let Ty::Var(arg_recv) = arg_recv {
|
||||
crate::log_debug_ct!("bind {arg_recv:?} {arg_ins:?}");
|
||||
ctx.bind_local(arg_recv, arg_ins.clone());
|
||||
}
|
||||
}
|
||||
|
||||
sig.body.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct SubstituteChecker<'a, T: TyCtxMut> {
|
||||
ctx: &'a mut T,
|
||||
}
|
||||
|
||||
impl<T: TyCtxMut> SubstituteChecker<'_, T> {
|
||||
fn ty(&mut self, body: &Ty, pol: bool) -> Option<Ty> {
|
||||
body.mutate(pol, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TyCtxMut> TyMutator for SubstituteChecker<'_, T> {
|
||||
fn mutate(&mut self, ty: &Ty, pol: bool) -> Option<Ty> {
|
||||
// todo: extrude the type into a polarized type
|
||||
let _ = pol;
|
||||
|
||||
if let Ty::Var(v) = ty {
|
||||
self.ctx.local_bind_of(v)
|
||||
} else {
|
||||
self.mutate_rec(ty, pol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::{assert_debug_snapshot, assert_snapshot};
|
||||
use tinymist_derive::BindTyCtx;
|
||||
|
||||
use super::{DynTypeBounds, Interned, Ty, TyCtx, TypeInfo, TypeVar};
|
||||
use crate::ty::tests::*;
|
||||
use crate::ty::ApplyChecker;
|
||||
#[test]
|
||||
fn test_ty() {
|
||||
use super::*;
|
||||
let ty = Ty::Builtin(BuiltinTy::Clause);
|
||||
let ty_ref = TyRef::new(ty.clone());
|
||||
assert_debug_snapshot!(ty_ref, @"Clause");
|
||||
}
|
||||
|
||||
#[derive(Default, BindTyCtx)]
|
||||
#[bind(0)]
|
||||
struct CallCollector(TypeInfo, Vec<Ty>);
|
||||
|
||||
impl ApplyChecker for CallCollector {
|
||||
fn apply(
|
||||
&mut self,
|
||||
sig: super::Sig,
|
||||
arguments: &crate::adt::interner::Interned<super::ArgsTy>,
|
||||
pol: bool,
|
||||
) {
|
||||
let ty = sig.call(arguments, pol, &mut self.0);
|
||||
if let Some(ty) = ty {
|
||||
self.1.push(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sig_call() {
|
||||
use super::*;
|
||||
|
||||
fn call(sig: Interned<SigTy>, args: Interned<SigTy>) -> String {
|
||||
let sig_ty = Ty::Func(sig);
|
||||
let mut collector = CallCollector::default();
|
||||
sig_ty.call(&args, false, &mut collector);
|
||||
|
||||
collector.1.iter().fold(String::new(), |mut acc, ty| {
|
||||
if !acc.is_empty() {
|
||||
acc.push_str(", ");
|
||||
}
|
||||
|
||||
acc.push_str(&format!("{ty:?}"));
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
assert_snapshot!(call(literal_sig!(p1 -> p1), literal_args!(q1)), @"@q1");
|
||||
assert_snapshot!(call(literal_sig!(!u1: w1 -> w1), literal_args!(!u1: w2)), @"@w2");
|
||||
}
|
||||
}
|
177
crates/tinymist-analysis/src/upstream/groups.yml
Normal file
177
crates/tinymist-analysis/src/upstream/groups.yml
Normal file
|
@ -0,0 +1,177 @@
|
|||
# This is responsible for the fact that certain math functions are grouped
|
||||
# together into one documentation page although they are not part of any scope.
|
||||
|
||||
- name: variants
|
||||
title: Variants
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["serif", "sans", "frak", "mono", "bb", "cal"]
|
||||
details: |
|
||||
Alternate typefaces within formulas.
|
||||
|
||||
These functions are distinct from the [`text`] function because math fonts
|
||||
contain multiple variants of each letter.
|
||||
|
||||
- name: styles
|
||||
title: Styles
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["upright", "italic", "bold"]
|
||||
details: |
|
||||
Alternate letterforms within formulas.
|
||||
|
||||
These functions are distinct from the [`text`] function because math fonts
|
||||
contain multiple variants of each letter.
|
||||
|
||||
- name: sizes
|
||||
title: Sizes
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["display", "inline", "script", "sscript"]
|
||||
details: |
|
||||
Forced size styles for expressions within formulas.
|
||||
|
||||
These functions allow manual configuration of the size of equation elements
|
||||
to make them look as in a display/inline equation or as if used in a root or
|
||||
sub/superscripts.
|
||||
|
||||
- name: underover
|
||||
title: Under/Over
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: [
|
||||
"underline",
|
||||
"overline",
|
||||
"underbrace",
|
||||
"overbrace",
|
||||
"underbracket",
|
||||
"overbracket",
|
||||
"underparen",
|
||||
"overparen",
|
||||
"undershell",
|
||||
"overshell",
|
||||
]
|
||||
details: |
|
||||
Delimiters above or below parts of an equation.
|
||||
|
||||
The braces and brackets further allow you to add an optional annotation
|
||||
below or above themselves.
|
||||
|
||||
- name: roots
|
||||
title: Roots
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["root", "sqrt"]
|
||||
details: |
|
||||
Square and non-square roots.
|
||||
|
||||
# Example
|
||||
```example
|
||||
$ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
|
||||
$ root(3, x) $
|
||||
```
|
||||
|
||||
- name: attach
|
||||
title: Attach
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["attach", "scripts", "limits"]
|
||||
details: |
|
||||
Subscript, superscripts, and limits.
|
||||
|
||||
Attachments can be displayed either as sub/superscripts, or limits. Typst
|
||||
automatically decides which is more suitable depending on the base, but you
|
||||
can also control this manually with the `scripts` and `limits` functions.
|
||||
|
||||
If you want the base to stretch to fit long top and bottom attachments (for
|
||||
example, an arrow with text above it), use the [`stretch`]($math.stretch)
|
||||
function.
|
||||
|
||||
# Example
|
||||
```example
|
||||
$ sum_(i=0)^n a_i = 2^(1+i) $
|
||||
```
|
||||
|
||||
# Syntax
|
||||
This function also has dedicated syntax for attachments after the base: Use
|
||||
the underscore (`_`) to indicate a subscript i.e. bottom attachment and the
|
||||
hat (`^`) to indicate a superscript i.e. top attachment.
|
||||
|
||||
- name: lr
|
||||
title: Left/Right
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["lr", "mid", "abs", "norm", "floor", "ceil", "round"]
|
||||
details: |
|
||||
Delimiter matching.
|
||||
|
||||
The `lr` function allows you to match two delimiters and scale them with the
|
||||
content they contain. While this also happens automatically for delimiters
|
||||
that match syntactically, `lr` allows you to match two arbitrary delimiters
|
||||
and control their size exactly. Apart from the `lr` function, Typst provides
|
||||
a few more functions that create delimiter pairings for absolute, ceiled,
|
||||
and floored values as well as norms.
|
||||
|
||||
# Example
|
||||
```example
|
||||
$ [a, b/2] $
|
||||
$ lr(]sum_(x=1)^n], size: #50%) x $
|
||||
$ abs((x + y) / 2) $
|
||||
```
|
||||
|
||||
- name: calc
|
||||
title: Calculation
|
||||
category: foundations
|
||||
path: ["calc"]
|
||||
details: |
|
||||
Module for calculations and processing of numeric values.
|
||||
|
||||
These definitions are part of the `calc` module and not imported by default.
|
||||
In addition to the functions listed below, the `calc` module also defines
|
||||
the constants `pi`, `tau`, `e`, and `inf`.
|
||||
|
||||
- name: sys
|
||||
title: System
|
||||
category: foundations
|
||||
path: ["sys"]
|
||||
details: |
|
||||
Module for system interactions.
|
||||
|
||||
This module defines the following items:
|
||||
|
||||
- The `sys.version` constant (of type [`version`]) that specifies
|
||||
the currently active Typst compiler version.
|
||||
|
||||
- The `sys.inputs` [dictionary], which makes external inputs
|
||||
available to the project. An input specified in the command line as
|
||||
`--input key=value` becomes available under `sys.inputs.key` as
|
||||
`{"value"}`. To include spaces in the value, it may be enclosed with
|
||||
single or double quotes.
|
||||
|
||||
The value is always of type [string]($str). More complex data
|
||||
may be parsed manually using functions like [`json.decode`]($json.decode).
|
||||
|
||||
- name: sym
|
||||
title: General
|
||||
category: symbols
|
||||
path: ["sym"]
|
||||
details: |
|
||||
Named general symbols.
|
||||
|
||||
For example, `#sym.arrow` produces the → symbol. Within
|
||||
[formulas]($category/math), these symbols can be used without the `#sym.`
|
||||
prefix.
|
||||
|
||||
The `d` in an integral's `dx` can be written as `[$dif x$]`.
|
||||
Outside math formulas, `dif` can be accessed as `math.dif`.
|
||||
|
||||
- name: emoji
|
||||
title: Emoji
|
||||
category: symbols
|
||||
path: ["emoji"]
|
||||
details: |
|
||||
Named emoji.
|
||||
|
||||
For example, `#emoji.face` produces the 😀 emoji. If you frequently use
|
||||
certain emojis, you can also import them from the `emoji` module (`[#import
|
||||
emoji: face]`) to use them without the `#emoji.` prefix.
|
488
crates/tinymist-analysis/src/upstream/mod.rs
Normal file
488
crates/tinymist-analysis/src/upstream/mod.rs
Normal file
|
@ -0,0 +1,488 @@
|
|||
//! Functions from typst-ide
|
||||
|
||||
use std::{collections::HashMap, fmt::Write, sync::LazyLock};
|
||||
|
||||
use comemo::Tracked;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::Deserialize;
|
||||
use serde_yaml as yaml;
|
||||
use typst::{
|
||||
diag::{bail, StrResult},
|
||||
foundations::{Binding, Content, Func, Module, Type, Value},
|
||||
introspection::MetadataElem,
|
||||
syntax::Span,
|
||||
text::{FontInfo, FontStyle},
|
||||
Library, World,
|
||||
};
|
||||
|
||||
mod tooltip;
|
||||
pub use tooltip::*;
|
||||
|
||||
/// Extract the first sentence of plain text of a piece of documentation.
|
||||
///
|
||||
/// Removes Markdown formatting.
|
||||
pub fn plain_docs_sentence(docs: &str) -> EcoString {
|
||||
crate::log_debug_ct!("plain docs {docs:?}");
|
||||
let docs = docs.replace("```example", "```typ");
|
||||
let mut scanner = unscanny::Scanner::new(&docs);
|
||||
let mut output = EcoString::new();
|
||||
let mut link = false;
|
||||
while let Some(ch) = scanner.eat() {
|
||||
match ch {
|
||||
'`' => {
|
||||
let mut raw = scanner.eat_until('`');
|
||||
if (raw.starts_with('{') && raw.ends_with('}'))
|
||||
|| (raw.starts_with('[') && raw.ends_with(']'))
|
||||
{
|
||||
raw = &raw[1..raw.len() - 1];
|
||||
}
|
||||
|
||||
scanner.eat();
|
||||
output.push('`');
|
||||
output.push_str(raw);
|
||||
output.push('`');
|
||||
}
|
||||
'[' => {
|
||||
link = true;
|
||||
output.push('[');
|
||||
}
|
||||
']' if link => {
|
||||
output.push(']');
|
||||
let cursor = scanner.cursor();
|
||||
if scanner.eat_if('(') {
|
||||
scanner.eat_until(')');
|
||||
let link_content = scanner.from(cursor + 1);
|
||||
scanner.eat();
|
||||
|
||||
crate::log_debug_ct!("Intra Link: {link_content}");
|
||||
let link = resolve(link_content, "https://typst.app/docs/").ok();
|
||||
let link = link.unwrap_or_else(|| {
|
||||
log::warn!("Failed to resolve link: {link_content}");
|
||||
"https://typst.app/docs/404.html".to_string()
|
||||
});
|
||||
|
||||
output.push('(');
|
||||
output.push_str(&link);
|
||||
output.push(')');
|
||||
} else if scanner.eat_if('[') {
|
||||
scanner.eat_until(']');
|
||||
scanner.eat();
|
||||
output.push_str(scanner.from(cursor));
|
||||
}
|
||||
link = false
|
||||
}
|
||||
// '*' | '_' => {}
|
||||
// '.' => {
|
||||
// output.push('.');
|
||||
// break;
|
||||
// }
|
||||
_ => output.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Data about a collection of functions.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct GroupData {
|
||||
name: EcoString,
|
||||
// title: EcoString,
|
||||
category: EcoString,
|
||||
#[serde(default)]
|
||||
path: Vec<EcoString>,
|
||||
#[serde(default)]
|
||||
filter: Vec<EcoString>,
|
||||
// details: EcoString,
|
||||
}
|
||||
|
||||
impl GroupData {
|
||||
fn module(&self) -> &'static Module {
|
||||
let mut focus = &LIBRARY.global;
|
||||
for path in &self.path {
|
||||
focus = get_module(focus, path).unwrap();
|
||||
}
|
||||
focus
|
||||
}
|
||||
}
|
||||
|
||||
static GROUPS: LazyLock<Vec<GroupData>> = LazyLock::new(|| {
|
||||
let mut groups: Vec<GroupData> = yaml::from_str(include_str!("groups.yml")).unwrap();
|
||||
for group in &mut groups {
|
||||
if group.filter.is_empty() {
|
||||
group.filter = group
|
||||
.module()
|
||||
.scope()
|
||||
.iter()
|
||||
.filter(|(_, v)| matches!(v.read(), Value::Func(_)))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
groups
|
||||
});
|
||||
|
||||
/// Resolve an intra-doc link.
|
||||
pub fn resolve(link: &str, base: &str) -> StrResult<String> {
|
||||
if link.starts_with('#') || link.starts_with("http") {
|
||||
return Ok(link.to_string());
|
||||
}
|
||||
|
||||
let (head, tail) = split_link(link)?;
|
||||
let mut route = match resolve_known(head, base) {
|
||||
Some(route) => route,
|
||||
None => resolve_definition(head, base)?,
|
||||
};
|
||||
|
||||
if !tail.is_empty() {
|
||||
route.push('/');
|
||||
route.push_str(tail);
|
||||
}
|
||||
|
||||
if !route.contains(['#', '?']) && !route.ends_with('/') {
|
||||
route.push('/');
|
||||
}
|
||||
|
||||
Ok(route)
|
||||
}
|
||||
|
||||
/// Split a link at the first slash.
|
||||
fn split_link(link: &str) -> StrResult<(&str, &str)> {
|
||||
let first = link.split('/').next().unwrap_or(link);
|
||||
let rest = link[first.len()..].trim_start_matches('/');
|
||||
Ok((first, rest))
|
||||
}
|
||||
|
||||
/// Resolve a `$` link head to a known destination.
|
||||
fn resolve_known(head: &str, base: &str) -> Option<String> {
|
||||
Some(match head {
|
||||
"$tutorial" => format!("{base}tutorial"),
|
||||
"$reference" => format!("{base}reference"),
|
||||
"$category" => format!("{base}reference"),
|
||||
"$syntax" => format!("{base}reference/syntax"),
|
||||
"$styling" => format!("{base}reference/styling"),
|
||||
"$scripting" => format!("{base}reference/scripting"),
|
||||
"$context" => format!("{base}reference/context"),
|
||||
"$guides" => format!("{base}guides"),
|
||||
"$changelog" => format!("{base}changelog"),
|
||||
"$community" => format!("{base}community"),
|
||||
"$universe" => "https://typst.app/universe".into(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
static LIBRARY: LazyLock<Library> = LazyLock::new(Library::default);
|
||||
|
||||
/// Extract a module from another module.
|
||||
#[track_caller]
|
||||
fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> {
|
||||
match parent.scope().get(name).map(|x| x.read()) {
|
||||
Some(Value::Module(module)) => Ok(module),
|
||||
_ => bail!("module doesn't contain module `{name}`"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a `$` link to a global definition.
|
||||
fn resolve_definition(head: &str, base: &str) -> StrResult<String> {
|
||||
let mut parts = head.trim_start_matches('$').split('.').peekable();
|
||||
let mut focus = &LIBRARY.global;
|
||||
let mut category = None;
|
||||
|
||||
while let Some(name) = parts.peek() {
|
||||
if category.is_none() {
|
||||
category = focus.scope().get(name).and_then(Binding::category);
|
||||
}
|
||||
let Ok(module) = get_module(focus, name) else {
|
||||
break;
|
||||
};
|
||||
focus = module;
|
||||
parts.next();
|
||||
}
|
||||
|
||||
let Some(category) = category else {
|
||||
bail!("{head} has no category")
|
||||
};
|
||||
|
||||
let name = parts.next().ok_or("link is missing first part")?;
|
||||
let value = focus.field(name, ())?;
|
||||
|
||||
// Handle grouped functions.
|
||||
if let Some(group) = GROUPS.iter().find(|group| {
|
||||
group.category == category.name() && group.filter.iter().any(|func| func == name)
|
||||
}) {
|
||||
let mut route = format!(
|
||||
"{}reference/{}/{}/#functions-{}",
|
||||
base, group.category, group.name, name
|
||||
);
|
||||
if let Some(param) = parts.next() {
|
||||
route.push('-');
|
||||
route.push_str(param);
|
||||
}
|
||||
return Ok(route);
|
||||
}
|
||||
|
||||
let mut route = format!("{}reference/{}/{name}", base, category.name());
|
||||
if let Some(next) = parts.next() {
|
||||
if let Ok(field) = value.field(next, ()) {
|
||||
route.push_str("/#definitions-");
|
||||
route.push_str(next);
|
||||
if let Some(next) = parts.next() {
|
||||
if field
|
||||
.cast::<Func>()
|
||||
.is_ok_and(|func| func.param(next).is_some())
|
||||
{
|
||||
route.push('-');
|
||||
route.push_str(next);
|
||||
}
|
||||
}
|
||||
} else if value
|
||||
.clone()
|
||||
.cast::<Func>()
|
||||
.is_ok_and(|func| func.param(next).is_some())
|
||||
{
|
||||
route.push_str("/#parameters-");
|
||||
route.push_str(next);
|
||||
} else {
|
||||
bail!("field {next} not found");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(route)
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
enum CatKey {
|
||||
Func(Func),
|
||||
Type(Type),
|
||||
}
|
||||
|
||||
impl PartialEq for CatKey {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use typst::foundations::func::Repr::*;
|
||||
match (self, other) {
|
||||
(CatKey::Func(a), CatKey::Func(b)) => match (a.inner(), b.inner()) {
|
||||
(Native(a), Native(b)) => a == b,
|
||||
(Element(a), Element(b)) => a == b,
|
||||
_ => false,
|
||||
},
|
||||
(CatKey::Type(a), CatKey::Type(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for CatKey {}
|
||||
|
||||
// todo: category of types
|
||||
static ROUTE_MAPS: LazyLock<HashMap<CatKey, String>> = LazyLock::new(|| {
|
||||
// todo: this is a false positive for clippy on LazyHash
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut map = HashMap::new();
|
||||
let mut scope_to_finds = vec![
|
||||
(LIBRARY.global.scope(), None, None),
|
||||
(LIBRARY.math.scope(), None, None),
|
||||
];
|
||||
while let Some((scope, parent_name, cat)) = scope_to_finds.pop() {
|
||||
for (name, bind) in scope.iter() {
|
||||
let cat = cat.or_else(|| bind.category());
|
||||
let name = urlify(name);
|
||||
match bind.read() {
|
||||
Value::Func(func) => {
|
||||
if let Some(cat) = cat {
|
||||
let Some(name) = func.name() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Handle grouped functions.
|
||||
if let Some(group) = GROUPS.iter().find(|group| {
|
||||
group.category == cat.name()
|
||||
&& group.filter.iter().any(|func| func == name)
|
||||
}) {
|
||||
let route = format!(
|
||||
"reference/{}/{}/#functions-{name}",
|
||||
group.category, group.name
|
||||
);
|
||||
map.insert(CatKey::Func(func.clone()), route);
|
||||
continue;
|
||||
}
|
||||
|
||||
crate::log_debug_ct!("func: {func:?} -> {cat:?}");
|
||||
|
||||
let route = if let Some(parent_name) = &parent_name {
|
||||
format!("reference/{}/{parent_name}/#definitions-{name}", cat.name())
|
||||
} else {
|
||||
format!("reference/{}/{name}/", cat.name())
|
||||
};
|
||||
|
||||
map.insert(CatKey::Func(func.clone()), route);
|
||||
}
|
||||
if let Some(s) = func.scope() {
|
||||
scope_to_finds.push((s, Some(name), cat));
|
||||
}
|
||||
}
|
||||
Value::Type(t) => {
|
||||
if let Some(cat) = cat {
|
||||
crate::log_debug_ct!("type: {t:?} -> {cat:?}");
|
||||
|
||||
let route = if let Some(parent_name) = &parent_name {
|
||||
format!("reference/{}/{parent_name}/#definitions-{name}", cat.name())
|
||||
} else {
|
||||
format!("reference/{}/{name}/", cat.name())
|
||||
};
|
||||
map.insert(CatKey::Type(*t), route);
|
||||
}
|
||||
scope_to_finds.push((t.scope(), Some(name), cat));
|
||||
}
|
||||
Value::Module(module) => {
|
||||
scope_to_finds.push((module.scope(), Some(name), cat));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
/// Turn a title into an URL fragment.
|
||||
pub(crate) fn urlify(title: &str) -> EcoString {
|
||||
title
|
||||
.chars()
|
||||
.map(|ch| ch.to_ascii_lowercase())
|
||||
.map(|ch| match ch {
|
||||
'a'..='z' | '0'..='9' => ch,
|
||||
_ => '-',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the route of a value.
|
||||
pub fn route_of_value(val: &Value) -> Option<&'static String> {
|
||||
// ROUTE_MAPS.get(&CatKey::Func(k.clone()))
|
||||
let key = match val {
|
||||
Value::Func(func) => CatKey::Func(func.clone()),
|
||||
Value::Type(ty) => CatKey::Type(*ty),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
ROUTE_MAPS.get(&key)
|
||||
}
|
||||
|
||||
/// Create a short description of a font family.
|
||||
pub fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString {
|
||||
let mut infos: Vec<_> = variants.collect();
|
||||
infos.sort_by_key(|info: &&FontInfo| info.variant);
|
||||
|
||||
let mut has_italic = false;
|
||||
let mut min_weight = u16::MAX;
|
||||
let mut max_weight = 0;
|
||||
for info in &infos {
|
||||
let weight = info.variant.weight.to_number();
|
||||
has_italic |= info.variant.style == FontStyle::Italic;
|
||||
min_weight = min_weight.min(weight);
|
||||
max_weight = min_weight.max(weight);
|
||||
}
|
||||
|
||||
let count = infos.len();
|
||||
let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" });
|
||||
|
||||
if min_weight == max_weight {
|
||||
write!(detail, " Weight {min_weight}.").unwrap();
|
||||
} else {
|
||||
write!(detail, " Weights {min_weight}–{max_weight}.").unwrap();
|
||||
}
|
||||
|
||||
if has_italic {
|
||||
detail.push_str(" Has italics.");
|
||||
}
|
||||
|
||||
detail
|
||||
}
|
||||
|
||||
/// Get the representation but truncated to a certain size.
|
||||
pub fn truncated_repr_<const SZ_LIMIT: usize>(value: &Value) -> EcoString {
|
||||
use typst::foundations::Repr;
|
||||
|
||||
let data: Option<Content> = value.clone().cast().ok();
|
||||
let metadata: Option<MetadataElem> = data.and_then(|content| content.unpack().ok());
|
||||
|
||||
// todo: early truncation
|
||||
let repr = if let Some(metadata) = metadata {
|
||||
metadata.value.repr()
|
||||
} else {
|
||||
value.repr()
|
||||
};
|
||||
|
||||
if repr.len() > SZ_LIMIT {
|
||||
eco_format!("[truncated-repr: {} bytes]", repr.len())
|
||||
} else {
|
||||
repr
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the representation but truncated to a certain size.
|
||||
pub fn truncated_repr(value: &Value) -> EcoString {
|
||||
const _10MB: usize = 100 * 1024 * 1024;
|
||||
truncated_repr_::<_10MB>(value)
|
||||
}
|
||||
|
||||
/// Run a function with a VM instance in the world
|
||||
pub fn with_vm<T>(
|
||||
world: Tracked<dyn World + '_>,
|
||||
f: impl FnOnce(&mut typst_shim::eval::Vm) -> T,
|
||||
) -> T {
|
||||
use comemo::Track;
|
||||
use typst::engine::*;
|
||||
use typst::foundations::*;
|
||||
use typst::introspection::*;
|
||||
use typst_shim::eval::*;
|
||||
|
||||
let introspector = Introspector::default();
|
||||
let traced = Traced::default();
|
||||
let mut sink = Sink::new();
|
||||
let engine = Engine {
|
||||
routines: &typst::ROUTINES,
|
||||
world,
|
||||
route: Route::default(),
|
||||
introspector: introspector.track(),
|
||||
traced: traced.track(),
|
||||
sink: sink.track_mut(),
|
||||
};
|
||||
|
||||
let context = Context::none();
|
||||
let mut vm = Vm::new(
|
||||
engine,
|
||||
context.track(),
|
||||
Scopes::new(Some(world.library())),
|
||||
Span::detached(),
|
||||
);
|
||||
|
||||
f(&mut vm)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn docs_test() {
|
||||
assert_eq!(
|
||||
"[citation](https://typst.app/docs/reference/model/cite/)",
|
||||
super::plain_docs_sentence("[citation]($cite)")
|
||||
);
|
||||
assert_eq!(
|
||||
"[citation][cite]",
|
||||
super::plain_docs_sentence("[citation][cite]")
|
||||
);
|
||||
assert_eq!(
|
||||
"[citation](https://typst.app/docs/reference/model/cite/)",
|
||||
super::plain_docs_sentence("[citation]($cite)")
|
||||
);
|
||||
assert_eq!(
|
||||
"[citation][cite][cite2]",
|
||||
super::plain_docs_sentence("[citation][cite][cite2]")
|
||||
);
|
||||
assert_eq!(
|
||||
"[citation][cite](test)[cite2]",
|
||||
super::plain_docs_sentence("[citation][cite](test)[cite2]")
|
||||
);
|
||||
}
|
||||
}
|
260
crates/tinymist-analysis/src/upstream/tooltip.rs
Normal file
260
crates/tinymist-analysis/src/upstream/tooltip.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use if_chain::if_chain;
|
||||
use typst::engine::Sink;
|
||||
use typst::foundations::{repr, Capturer, CastInfo, Value};
|
||||
use typst::layout::Length;
|
||||
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
|
||||
use typst::World;
|
||||
use typst_shim::eval::CapturesVisitor;
|
||||
use typst_shim::syntax::LinkedNodeExt;
|
||||
use typst_shim::utils::{round_2, Numeric};
|
||||
|
||||
use super::{plain_docs_sentence, summarize_font_family, truncated_repr};
|
||||
use crate::analyze_expr;
|
||||
|
||||
/// Describe the item under the cursor.
|
||||
///
|
||||
/// Passing a `document` (from a previous compilation) is optional, but enhances
|
||||
/// the autocompletions. Label completions, for instance, are only generated
|
||||
/// when the document is available.
|
||||
pub fn tooltip_(world: &dyn World, source: &Source, cursor: usize) -> Option<Tooltip> {
|
||||
let leaf = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
|
||||
if leaf.kind().is_trivia() {
|
||||
return None;
|
||||
}
|
||||
|
||||
named_param_tooltip(world, &leaf)
|
||||
.or_else(|| font_tooltip(world, &leaf))
|
||||
// todo: test that label_tooltip can be removed safely
|
||||
// .or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
|
||||
.or_else(|| expr_tooltip(world, &leaf))
|
||||
.or_else(|| closure_tooltip(&leaf))
|
||||
}
|
||||
|
||||
/// A hover tooltip.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Tooltip {
|
||||
/// A string of text.
|
||||
Text(EcoString),
|
||||
/// A string of Typst code.
|
||||
Code(EcoString),
|
||||
}
|
||||
|
||||
/// Tooltip for a hovered expression.
|
||||
pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
let mut ancestor = leaf;
|
||||
while !ancestor.is::<ast::Expr>() {
|
||||
ancestor = ancestor.parent()?;
|
||||
}
|
||||
|
||||
let expr = ancestor.cast::<ast::Expr>()?;
|
||||
if !expr.hash() && !matches!(expr, ast::Expr::MathIdent(_)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let values = analyze_expr(world, ancestor);
|
||||
|
||||
if let [(value, _)] = values.as_slice() {
|
||||
if let Some(docs) = value.docs() {
|
||||
return Some(Tooltip::Text(plain_docs_sentence(docs)));
|
||||
}
|
||||
|
||||
if let &Value::Length(length) = value {
|
||||
if let Some(tooltip) = length_tooltip(length) {
|
||||
return Some(tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expr.is_literal() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut last = None;
|
||||
let mut pieces: Vec<EcoString> = vec![];
|
||||
let mut unique_func: Option<Value> = None;
|
||||
let mut unique = true;
|
||||
let mut iter = values.iter();
|
||||
for (value, _) in (&mut iter).take(Sink::MAX_VALUES - 1) {
|
||||
if let Some((prev, count)) = &mut last {
|
||||
if *prev == value {
|
||||
*count += 1;
|
||||
continue;
|
||||
} else if *count > 1 {
|
||||
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(value, Value::Func(..) | Value::Type(..)) {
|
||||
match &unique_func {
|
||||
Some(unique_func) if unique => {
|
||||
unique = unique_func == value;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
unique_func = Some(value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unique = false;
|
||||
}
|
||||
|
||||
pieces.push(truncated_repr(value));
|
||||
last = Some((value, 1));
|
||||
}
|
||||
|
||||
// Don't report the only function reference...
|
||||
// Note we usually expect the `definition` analyzer work in this case, otherwise
|
||||
// please open an issue for this.
|
||||
if unique_func.is_some() && unique {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((_, count)) = last {
|
||||
if count > 1 {
|
||||
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if iter.next().is_some() {
|
||||
pieces.push("...".into());
|
||||
}
|
||||
|
||||
let tooltip = repr::pretty_comma_list(&pieces, false);
|
||||
// todo: check sensible length, value highlighting
|
||||
(!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
|
||||
}
|
||||
|
||||
/// Tooltip for a hovered closure.
|
||||
fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
// Only show this tooltip when hovering over the equals sign or arrow of
|
||||
// the closure. Showing it across the whole subtree is too noisy.
|
||||
if !matches!(leaf.kind(), SyntaxKind::Eq | SyntaxKind::Arrow) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the closure to analyze.
|
||||
let parent = leaf.parent()?;
|
||||
if parent.kind() != SyntaxKind::Closure {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Analyze the closure's captures.
|
||||
let mut visitor = CapturesVisitor::new(None, Capturer::Function);
|
||||
visitor.visit(parent);
|
||||
|
||||
let captures = visitor.finish();
|
||||
let mut names: Vec<_> = captures
|
||||
.iter()
|
||||
.map(|(name, _)| eco_format!("`{name}`"))
|
||||
.collect();
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
let tooltip = repr::separated_list(&names, "and");
|
||||
Some(Tooltip::Text(eco_format!(
|
||||
"This closure captures {tooltip}."
|
||||
)))
|
||||
}
|
||||
|
||||
/// Tooltip text for a hovered length.
|
||||
fn length_tooltip(length: Length) -> Option<Tooltip> {
|
||||
length.em.is_zero().then(|| {
|
||||
Tooltip::Code(eco_format!(
|
||||
"{}pt = {}mm = {}cm = {}in",
|
||||
round_2(length.abs.to_pt()),
|
||||
round_2(length.abs.to_mm()),
|
||||
round_2(length.abs.to_cm()),
|
||||
round_2(length.abs.to_inches())
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Tooltips for components of a named parameter.
|
||||
fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
let (func, named) = if_chain! {
|
||||
// Ensure that we are in a named pair in the arguments to a function
|
||||
// call or set rule.
|
||||
if let Some(parent) = leaf.parent();
|
||||
if let Some(named) = parent.cast::<ast::Named>();
|
||||
if let Some(grand) = parent.parent();
|
||||
if matches!(grand.kind(), SyntaxKind::Args);
|
||||
if let Some(grand_grand) = grand.parent();
|
||||
if let Some(expr) = grand_grand.cast::<ast::Expr>();
|
||||
if let Some(ast::Expr::Ident(callee)) = match expr {
|
||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||
ast::Expr::Set(set) => Some(set.target()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Find metadata about the function.
|
||||
if let Some(Value::Func(func)) = world.library().global.scope().get(&callee).map(|x| x.read());
|
||||
then { (func, named) }
|
||||
else { return None; }
|
||||
};
|
||||
|
||||
// Hovering over the parameter name.
|
||||
if_chain! {
|
||||
if leaf.index() == 0;
|
||||
if let Some(ident) = leaf.cast::<ast::Ident>();
|
||||
if let Some(param) = func.param(&ident);
|
||||
then {
|
||||
return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
|
||||
}
|
||||
}
|
||||
|
||||
// Hovering over a string parameter value.
|
||||
if_chain! {
|
||||
if let Some(string) = leaf.cast::<ast::Str>();
|
||||
if let Some(param) = func.param(&named.name());
|
||||
if let Some(docs) = find_string_doc(¶m.input, &string.get());
|
||||
then {
|
||||
return Some(Tooltip::Text(docs.into()));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find documentation for a castable string.
|
||||
fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> {
|
||||
match info {
|
||||
CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs),
|
||||
CastInfo::Union(options) => options
|
||||
.iter()
|
||||
.find_map(|option| find_string_doc(option, string)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tooltip for font.
|
||||
fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
if_chain! {
|
||||
// Ensure that we are on top of a string.
|
||||
if let Some(string) = leaf.cast::<ast::Str>();
|
||||
let lower = string.get().to_lowercase();
|
||||
|
||||
// Ensure that we are in the arguments to the text function.
|
||||
if let Some(parent) = leaf.parent();
|
||||
if let Some(named) = parent.cast::<ast::Named>();
|
||||
if named.name().as_str() == "font";
|
||||
|
||||
// Find the font family.
|
||||
if let Some((_, iter)) = world
|
||||
.book()
|
||||
.families()
|
||||
.find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
|
||||
|
||||
then {
|
||||
let detail = summarize_font_family(iter);
|
||||
return Some(Tooltip::Text(detail));
|
||||
}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue