refactor: move expr and ty defs to analysis crate (#1633)

This commit is contained in:
Myriad-Dreamin 2025-04-08 05:50:55 +08:00 committed by GitHub
parent 72e33e461d
commit ac506dcc31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 968 additions and 890 deletions

View file

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

View 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,);

View file

@ -0,0 +1,4 @@
#![allow(missing_docs)]
pub mod interner;
pub mod snapshot_map;

View 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 => {}
}
}
}

View file

@ -0,0 +1,6 @@
#![allow(missing_docs)]
mod def;
pub use def::*;
mod tidy;
pub use tidy::*;

View 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(&param.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))
}

View 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
");
}
}

View file

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

View 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
}

View 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])
}

View file

@ -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::*;

View 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,
);

View 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)
}
}

View 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)
}

View 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(())
}
}

View 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),
}
}
}

View 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(&param.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(&param.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(&param.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:?}");
}
}

View 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()))
}
}
}
}

File diff suppressed because it is too large Load diff

View 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()
}
}

View 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(_) => {}
}
}

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

View 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(&param.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)
}
}

View 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::*;

View 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(())
}
}

View 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(&param.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
_ => {}
}
}
}

View 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(&param.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(&param.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()
}
}

View 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");
}
}

View 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.

View 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]")
);
}
}

View 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(&param.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
}