tinymist_analysis/ty/
builtin.rs

1use core::fmt;
2use std::path::Path;
3use std::sync::LazyLock;
4
5use ecow::{EcoString, eco_format};
6use regex::RegexSet;
7use strum::{EnumIter, IntoEnumIterator};
8use typst::foundations::{CastInfo, Regex};
9use typst::layout::Ratio;
10use typst::syntax::FileId;
11use typst::{
12    foundations::{AutoValue, Content, Func, NoneValue, ParamInfo, Type, Value},
13    layout::Length,
14};
15use typst_shim::syntax::RootedPathExt;
16
17use crate::syntax::Decl;
18use crate::ty::*;
19
20/// A kind of path recognized by the analyzer.
21#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, EnumIter)]
22pub enum PathKind {
23    /// A source path: `import "foo.typ"`.
24    Source {
25        /// Whether to allow package imports.
26        allow_package: bool,
27    },
28    /// A WASM path: `plugin("foo.wasm")`.
29    Wasm,
30    /// A CSV path: `csv("foo.csv")`.
31    Csv,
32    /// An image path: `image("foo.png")`.
33    Image,
34    /// A JSON path: `json("foo.json")`.
35    Json,
36    /// A YAML path: `yaml("foo.yml")`.
37    Yaml,
38    /// A XML path: `xml("foo.xml")`.
39    Xml,
40    /// A TOML path: `toml("foo.toml")`.
41    Toml,
42    /// A CSL path: `bibliography(csl: "foo.csl")`.
43    Csl,
44    /// A bibliography path: `bibliography("foo.bib")`.
45    Bibliography,
46    /// A raw theme path: `raw(theme: "foo.tmTheme")`.
47    RawTheme,
48    /// A raw syntaxes path: `raw(syntaxes: "foo.tmLanguage")`.
49    RawSyntax,
50    /// All of the above kinds.
51    Special,
52    /// Merely known as a path.
53    None,
54}
55
56impl PathKind {
57    /// Matches the extension of the path by kind.
58    pub fn ext_matcher(&self) -> &'static RegexSet {
59        type RegSet = LazyLock<RegexSet>;
60
61        fn make_regex(patterns: &[&str]) -> RegexSet {
62            let patterns = patterns.iter().map(|pattern| format!("(?i)^{pattern}$"));
63            RegexSet::new(patterns).unwrap()
64        }
65
66        static SOURCE_REGSET: RegSet = RegSet::new(|| make_regex(&["typ", "typc"]));
67        static WASM_REGSET: RegSet = RegSet::new(|| make_regex(&["wasm"]));
68        static IMAGE_REGSET: RegSet = RegSet::new(|| {
69            make_regex(&[
70                "ico", "bmp", "png", "webp", "jpg", "jpeg", "jfif", "tiff", "gif", "svg", "svgz",
71                "pdf",
72            ])
73        });
74        static JSON_REGSET: RegSet = RegSet::new(|| make_regex(&["json", "jsonc", "json5"]));
75        static YAML_REGSET: RegSet = RegSet::new(|| make_regex(&["yaml", "yml"]));
76        static XML_REGSET: RegSet = RegSet::new(|| make_regex(&["xml"]));
77        static TOML_REGSET: RegSet = RegSet::new(|| make_regex(&["toml"]));
78        static CSV_REGSET: RegSet = RegSet::new(|| make_regex(&["csv"]));
79        static BIB_REGSET: RegSet = RegSet::new(|| make_regex(&["yaml", "yml", "bib"]));
80        static CSL_REGSET: RegSet = RegSet::new(|| make_regex(&["csl"]));
81        static RAW_THEME_REGSET: RegSet = RegSet::new(|| make_regex(&["tmTheme", "xml"]));
82        static RAW_SYNTAX_REGSET: RegSet =
83            RegSet::new(|| make_regex(&["tmLanguage", "sublime-syntax"]));
84
85        static ALL_REGSET: RegSet = RegSet::new(|| RegexSet::new([r".*"]).unwrap());
86        static ALL_SPECIAL_REGSET: RegSet = RegSet::new(|| {
87            RegexSet::new({
88                let patterns = SOURCE_REGSET.patterns();
89                let patterns = patterns.iter().chain(WASM_REGSET.patterns());
90                let patterns = patterns.chain(IMAGE_REGSET.patterns());
91                let patterns = patterns.chain(JSON_REGSET.patterns());
92                let patterns = patterns.chain(YAML_REGSET.patterns());
93                let patterns = patterns.chain(XML_REGSET.patterns());
94                let patterns = patterns.chain(TOML_REGSET.patterns());
95                let patterns = patterns.chain(CSV_REGSET.patterns());
96                let patterns = patterns.chain(BIB_REGSET.patterns());
97                let patterns = patterns.chain(CSL_REGSET.patterns());
98                let patterns = patterns.chain(RAW_THEME_REGSET.patterns());
99                patterns.chain(RAW_SYNTAX_REGSET.patterns())
100            })
101            .unwrap()
102        });
103
104        match self {
105            PathKind::Source { .. } => &SOURCE_REGSET,
106            PathKind::Wasm => &WASM_REGSET,
107            PathKind::Csv => &CSV_REGSET,
108            PathKind::Image => &IMAGE_REGSET,
109            PathKind::Json => &JSON_REGSET,
110            PathKind::Yaml => &YAML_REGSET,
111            PathKind::Xml => &XML_REGSET,
112            PathKind::Toml => &TOML_REGSET,
113            PathKind::Csl => &CSL_REGSET,
114            PathKind::Bibliography => &BIB_REGSET,
115            PathKind::RawTheme => &RAW_THEME_REGSET,
116            PathKind::RawSyntax => &RAW_SYNTAX_REGSET,
117            PathKind::Special => &ALL_SPECIAL_REGSET,
118            PathKind::None => &ALL_REGSET,
119        }
120    }
121
122    /// Checks if the path matches the kind.
123    pub fn is_match(&self, path: &Path) -> bool {
124        let ext = path.extension().and_then(|ext| ext.to_str());
125        ext.is_some_and(|ext| self.ext_matcher().is_match(ext))
126    }
127
128    /// Gets the kind of the path by extension.
129    pub fn from_ext(path: &str) -> Option<Self> {
130        PathKind::iter().find(|preference| preference.is_match(std::path::Path::new(path)))
131    }
132}
133
134impl Ty {
135    /// Converts a cast info to a type.
136    pub fn from_cast_info(ty: &CastInfo) -> Ty {
137        match &ty {
138            CastInfo::Any => Ty::Any,
139            CastInfo::Value(val, doc) => Ty::Value(InsTy::new_doc(val.clone(), *doc)),
140            CastInfo::Type(ty) => Ty::Builtin(BuiltinTy::Type(*ty)),
141            CastInfo::Union(types) => {
142                Ty::iter_union(UnionIter(vec![types.as_slice().iter()]).map(Self::from_cast_info))
143            }
144        }
145    }
146
147    /// Converts a parameter site to a type.
148    pub fn from_param_site(func: &Func, param: &ParamInfo) -> Ty {
149        use typst::foundations::FuncInner;
150        match func.inner() {
151            FuncInner::Element(..) | FuncInner::Native(..) | FuncInner::Plugin(..) => {
152                if let Some(ty) = param_mapping(func, param) {
153                    return ty;
154                }
155            }
156            FuncInner::Closure(_) => {}
157            FuncInner::With(w) => return Ty::from_param_site(&w.0, param),
158        };
159
160        param
161            .to_native()
162            .map(|native| Self::from_cast_info(&native.input))
163            .unwrap_or(Ty::Any)
164    }
165
166    /// Converts a return site to a type.
167    pub(crate) fn from_return_site(func: &Func, ty: &'_ CastInfo) -> Self {
168        use typst::foundations::FuncInner;
169        match func.inner() {
170            FuncInner::Element(elem) => return Ty::Builtin(BuiltinTy::Content(Some(*elem))),
171            FuncInner::Closure(_) | FuncInner::Plugin(_) => {}
172            FuncInner::With(w) => return Ty::from_return_site(&w.0, ty),
173            FuncInner::Native(_) => {}
174        };
175
176        Self::from_cast_info(ty)
177    }
178}
179
180/// An iterator over a union of cast infos.
181struct UnionIter<'a>(Vec<std::slice::Iter<'a, CastInfo>>);
182
183impl<'a> Iterator for UnionIter<'a> {
184    type Item = &'a CastInfo;
185
186    fn next(&mut self) -> Option<Self::Item> {
187        loop {
188            let iter = self.0.last_mut()?;
189            if let Some(ty) = iter.next() {
190                match ty {
191                    CastInfo::Union(types) => {
192                        self.0.push(types.as_slice().iter());
193                    }
194                    _ => return Some(ty),
195                }
196            } else {
197                self.0.pop();
198            }
199        }
200    }
201}
202
203// todo: we can write some proto files for builtin sigs
204/// A builtin signature.
205#[derive(Debug, Clone, Copy)]
206pub enum BuiltinSig<'a> {
207    /// Maps a function over a tuple: `(a, b, c).map`
208    TupleMap(&'a Ty),
209    /// Gets element of a tuple: `(a, b, c).at`
210    TupleAt(&'a Ty),
211    /// Gets the positional values of arguments: `arguments.pos`
212    ArgumentsPos(&'a Ty),
213}
214
215impl<'a> BuiltinSig<'a> {
216    /// Gets a dependent method signature for an arguments receiver.
217    pub fn arguments_method(receiver: &'a Ty, method: &str) -> Option<Self> {
218        (method == "pos").then_some(Self::ArgumentsPos(receiver))
219    }
220}
221
222/// A package identifier.
223#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
224pub struct PackageId {
225    /// The namespace of the package.
226    pub namespace: StrRef,
227    /// The name of the package.
228    pub name: StrRef,
229}
230
231impl fmt::Debug for PackageId {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(f, "@{}/{}", self.namespace, self.name)
234    }
235}
236
237impl TryFrom<FileId> for PackageId {
238    type Error = ();
239
240    fn try_from(value: FileId) -> Result<Self, Self::Error> {
241        let spec = value.package_compat().ok_or(())?;
242        Ok(PackageId {
243            namespace: spec.namespace.as_str().into(),
244            name: spec.name.as_str().into(),
245        })
246    }
247}
248
249/// A builtin type.
250#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
251pub enum BuiltinTy {
252    /// A clause type.
253    Clause,
254    /// An undefined type.
255    Undef,
256    /// A space type: `[ ]`
257    Space,
258    /// A none type: `none`
259    None,
260    /// A break type: `break`
261    Break,
262    /// A continue type: `continue`
263    Continue,
264    /// A never type for expressions that do not continue normally.
265    Never,
266    /// An infer type: `any`
267    Infer,
268    /// A flow none type: `none`
269    FlowNone,
270    /// An auto type: `auto`
271    Auto,
272
273    /// Arguments: `arguments(a, b: c, ..d)`
274    Args,
275    /// A color type: `rgb(r, g, b)`
276    Color,
277    /// A text size type: `text.size`
278    TextSize,
279    /// A text font type: `text.font`
280    TextFont,
281    /// A text feature type: `text.feature`
282    TextFeature,
283    /// A text language type: `text.lang`
284    TextLang,
285    /// A text region type: `text.region`
286    TextRegion,
287    /// A dir type: `left`
288    Dir,
289    /// A label type: `<label>`
290    Label,
291    /// A cite label type: `#cite(<label>)`
292    CiteLabel,
293    /// A ref label type: `@label`
294    RefLabel,
295    /// A length type: `10pt`
296    Length,
297    /// A float type: `1.0`
298    Float,
299    /// A stroke type: `stroke(paint: red)`
300    Stroke,
301    /// A margin type: `page(margin: 10pt)`
302    Margin,
303    /// An inset type: `box(inset: 10pt)`
304    Inset,
305    /// An outset type: `box(outset: 10pt)`
306    Outset,
307    /// A radius type: `box(radius: 10pt)`
308    Radius,
309
310    /// A tag type: `tag`
311    Tag(Box<(StrRef, Option<Interned<PackageId>>)>),
312
313    /// The type of a value: `int` of `10`
314    Type(typst::foundations::Type),
315    /// The type of a type: `type(int)`
316    TypeType(typst::foundations::Type),
317    /// The element type of a content value. For example, `#[text]` has
318    /// element type `text`.
319    ///
320    /// If the element is not specified, the element type is `content`.
321    Content(Option<typst::foundations::Element>),
322    /// The type of an element: `text`
323    Element(typst::foundations::Element),
324
325    /// A module type: `module(foo)`
326    Module(Interned<Decl>),
327    /// A path type: `import "foo.typ"`
328    Path(PathKind),
329}
330
331impl fmt::Debug for BuiltinTy {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        match self {
334            BuiltinTy::Clause => f.write_str("Clause"),
335            BuiltinTy::Undef => f.write_str("Undef"),
336            BuiltinTy::Content(ty) => {
337                if let Some(ty) = ty {
338                    write!(f, "Content({})", ty.name())
339                } else {
340                    f.write_str("Content")
341                }
342            }
343            BuiltinTy::Space => f.write_str("Space"),
344            BuiltinTy::None => f.write_str("None"),
345            BuiltinTy::Break => f.write_str("Break"),
346            BuiltinTy::Continue => f.write_str("Continue"),
347            BuiltinTy::Never => f.write_str("Never"),
348            BuiltinTy::Infer => f.write_str("Infer"),
349            BuiltinTy::FlowNone => f.write_str("FlowNone"),
350            BuiltinTy::Auto => f.write_str("Auto"),
351
352            BuiltinTy::Args => write!(f, "Args"),
353            BuiltinTy::Color => write!(f, "Color"),
354            BuiltinTy::TextSize => write!(f, "TextSize"),
355            BuiltinTy::TextFont => write!(f, "TextFont"),
356            BuiltinTy::TextFeature => write!(f, "TextFeature"),
357            BuiltinTy::TextLang => write!(f, "TextLang"),
358            BuiltinTy::TextRegion => write!(f, "TextRegion"),
359            BuiltinTy::Dir => write!(f, "Dir"),
360            BuiltinTy::Length => write!(f, "Length"),
361            BuiltinTy::Label => write!(f, "Label"),
362            BuiltinTy::CiteLabel => write!(f, "CiteLabel"),
363            BuiltinTy::RefLabel => write!(f, "RefLabel"),
364            BuiltinTy::Float => write!(f, "Float"),
365            BuiltinTy::Stroke => write!(f, "Stroke"),
366            BuiltinTy::Margin => write!(f, "Margin"),
367            BuiltinTy::Inset => write!(f, "Inset"),
368            BuiltinTy::Outset => write!(f, "Outset"),
369            BuiltinTy::Radius => write!(f, "Radius"),
370            BuiltinTy::TypeType(ty) => write!(f, "TypeType({})", ty.short_name()),
371            BuiltinTy::Type(ty) => write!(f, "Type({})", ty.short_name()),
372            BuiltinTy::Element(elem) => elem.fmt(f),
373            BuiltinTy::Tag(tag) => {
374                let (name, id) = tag.as_ref();
375                if let Some(id) = id {
376                    write!(f, "Tag({name:?}) of {id:?}")
377                } else {
378                    write!(f, "Tag({name:?})")
379                }
380            }
381            BuiltinTy::Module(decl) => write!(f, "{decl:?}"),
382            BuiltinTy::Path(preference) => write!(f, "Path({preference:?})"),
383        }
384    }
385}
386
387impl BuiltinTy {
388    /// Converts a value to a type.
389    pub fn from_value(builtin: &Value) -> Ty {
390        if let Value::Bool(v) = builtin {
391            return Ty::Boolean(Some(*v));
392        }
393
394        Self::from_builtin(builtin.ty())
395    }
396
397    /// Converts a builtin type to a type.
398    pub fn from_builtin(builtin: Type) -> Ty {
399        if builtin == Type::of::<AutoValue>() {
400            return Ty::Builtin(BuiltinTy::Auto);
401        }
402        if builtin == Type::of::<NoneValue>() {
403            return Ty::Builtin(BuiltinTy::None);
404        }
405        if builtin == Type::of::<typst::visualize::Color>() {
406            return Color.literally();
407        }
408        if builtin == Type::of::<bool>() {
409            return Ty::Builtin(BuiltinTy::None);
410        }
411        if builtin == Type::of::<f64>() {
412            return Float.literally();
413        }
414        if builtin == Type::of::<Length>() {
415            return Length.literally();
416        }
417        if builtin == Type::of::<Content>() {
418            return Ty::Builtin(BuiltinTy::Content(Option::None));
419        }
420
421        BuiltinTy::Type(builtin).literally()
422    }
423
424    /// Describes the builtin type.
425    pub(crate) fn describe(&self) -> EcoString {
426        let res = match self {
427            BuiltinTy::Clause => "any",
428            BuiltinTy::Undef => "any",
429            BuiltinTy::Content(ty) => {
430                return if let Some(ty) = ty {
431                    eco_format!("content({})", ty.name())
432                } else {
433                    "content".into()
434                };
435            }
436            BuiltinTy::Space => "content",
437            BuiltinTy::None => "none",
438            BuiltinTy::Break => "break",
439            BuiltinTy::Continue => "continue",
440            BuiltinTy::Never => "never",
441            BuiltinTy::Infer => "any",
442            BuiltinTy::FlowNone => "none",
443            BuiltinTy::Auto => "auto",
444
445            BuiltinTy::Args => "arguments",
446            BuiltinTy::Color => "color",
447            BuiltinTy::TextSize => "text.size",
448            BuiltinTy::TextFont => "text.font",
449            BuiltinTy::TextFeature => "text.feature",
450            BuiltinTy::TextLang => "text.lang",
451            BuiltinTy::TextRegion => "text.region",
452            BuiltinTy::Dir => "dir",
453            BuiltinTy::Length => "length",
454            BuiltinTy::Float => "float",
455            BuiltinTy::Label => "label",
456            BuiltinTy::CiteLabel => "cite-label",
457            BuiltinTy::RefLabel => "ref-label",
458            BuiltinTy::Stroke => "stroke",
459            BuiltinTy::Margin => "margin",
460            BuiltinTy::Inset => "inset",
461            BuiltinTy::Outset => "outset",
462            BuiltinTy::Radius => "radius",
463            BuiltinTy::TypeType(..) => "type",
464            BuiltinTy::Type(ty) => ty.short_name(),
465            BuiltinTy::Element(ty) => ty.name(),
466            BuiltinTy::Tag(tag) => {
467                let (name, id) = tag.as_ref();
468                return if let Some(id) = id {
469                    eco_format!("tag {name} of {id:?}")
470                } else {
471                    eco_format!("tag {name}")
472                };
473            }
474            BuiltinTy::Module(m) => return eco_format!("module({})", m.name()),
475            BuiltinTy::Path(s) => match s {
476                PathKind::None => "[any]",
477                PathKind::Special => "[any]",
478                PathKind::Source { .. } => "[source]",
479                PathKind::Wasm => "[wasm]",
480                PathKind::Csv => "[csv]",
481                PathKind::Image => "[image]",
482                PathKind::Json => "[json]",
483                PathKind::Yaml => "[yaml]",
484                PathKind::Xml => "[xml]",
485                PathKind::Toml => "[toml]",
486                PathKind::Csl => "[csl]",
487                PathKind::Bibliography => "[bib]",
488                PathKind::RawTheme => "[theme]",
489                PathKind::RawSyntax => "[syntax]",
490            },
491        };
492
493        res.into()
494    }
495}
496
497use BuiltinTy::*;
498
499/// Converts a flow builtin to a type.
500fn literally(s: impl FlowBuiltinLiterally) -> Ty {
501    s.literally()
502}
503
504/// A trait for converting a flow builtin to a type.
505trait FlowBuiltinLiterally {
506    fn literally(self) -> Ty;
507}
508
509impl FlowBuiltinLiterally for &str {
510    fn literally(self) -> Ty {
511        Ty::Value(InsTy::new(Value::Str(self.into())))
512    }
513}
514
515impl FlowBuiltinLiterally for BuiltinTy {
516    fn literally(self) -> Ty {
517        Ty::Builtin(self.clone())
518    }
519}
520
521impl FlowBuiltinLiterally for Ty {
522    fn literally(self) -> Ty {
523        self
524    }
525}
526
527/// A macro for converting a flow builtin to a type.
528macro_rules! flow_builtin_union_inner {
529    ($literal_kind:expr) => {
530        literally($literal_kind)
531    };
532    ($($x:expr),+ $(,)?) => {
533        Vec::from_iter([
534            $(flow_builtin_union_inner!($x)),*
535        ])
536    };
537}
538
539/// A macro for converting a flow builtin to a type.
540macro_rules! flow_union {
541    // the first one is string
542    ($($b:tt)*) => {
543        Ty::iter_union(flow_builtin_union_inner!( $($b)* ).into_iter())
544    };
545
546}
547
548/// A macro for converting a flow builtin to a type.
549macro_rules! flow_record {
550    ($($name:expr => $ty:expr),* $(,)?) => {
551        RecordTy::new(vec![
552            $(
553                (
554                    $name.into(),
555                    $ty,
556                ),
557            )*
558        ])
559    };
560}
561
562/// Maps a function parameter to a type.
563pub(super) fn param_mapping(func: &Func, param: &ParamInfo) -> Option<Ty> {
564    // todo: remove path params which is compatible with 0.12.0
565    let input_ty = || {
566        param
567            .to_native()
568            .map(|native| Ty::from_cast_info(&native.input))
569            .unwrap_or(Ty::Any)
570    };
571
572    match (func.name()?, param.name()?) {
573        // todo: pdf.embed
574        ("embed", "path") => Some(literally(Path(PathKind::None))),
575        ("cbor", "path" | "source") => Some(literally(Path(PathKind::None))),
576        ("plugin", "source") => Some(literally(Path(PathKind::Wasm))),
577        ("csv", "path" | "source") => Some(literally(Path(PathKind::Csv))),
578        ("image", "path" | "source") => Some(literally(Path(PathKind::Image))),
579        ("read", "path" | "source") => Some(literally(Path(PathKind::None))),
580        ("json", "path" | "source") => Some(literally(Path(PathKind::Json))),
581        ("yaml", "path" | "source") => Some(literally(Path(PathKind::Yaml))),
582        ("xml", "path" | "source") => Some(literally(Path(PathKind::Xml))),
583        ("toml", "path" | "source") => Some(literally(Path(PathKind::Toml))),
584        ("raw", "theme") => Some(literally(Path(PathKind::RawTheme))),
585        ("raw", "syntaxes") => Some(literally(Path(PathKind::RawSyntax))),
586        ("bibliography" | "cite", "style") => {
587            Some(Ty::iter_union([literally(Path(PathKind::Csl)), input_ty()]))
588        }
589        ("cite", "key") => Some(Ty::iter_union([literally(CiteLabel)])),
590        ("ref", "target") => Some(Ty::iter_union([literally(RefLabel)])),
591        ("footnote", "body") => Some(Ty::iter_union([literally(RefLabel), input_ty()])),
592        ("link", "dest") => {
593            static LINK_DEST_TYPE: LazyLock<Ty> = LazyLock::new(|| {
594                flow_union!(
595                    literally(RefLabel),
596                    Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Str>())),
597                    Ty::Builtin(BuiltinTy::Type(Type::of::<typst::introspection::Location>())),
598                    Ty::Dict(RecordTy::new(vec![
599                        ("x".into(), literally(Length)),
600                        ("y".into(), literally(Length)),
601                    ])),
602                )
603            });
604            Some(LINK_DEST_TYPE.clone())
605        }
606        ("bibliography", "path" | "sources") => {
607            static BIB_PATH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
608                let bib_path_ty = literally(Path(PathKind::Bibliography));
609                Ty::iter_union([bib_path_ty.clone(), Ty::Array(bib_path_ty.into())])
610            });
611            Some(BIB_PATH_TYPE.clone())
612        }
613        ("text", "size") => Some(literally(TextSize)),
614        ("text", "font") => {
615            // todo: the dict can be completed, but we have bugs...
616            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
617                Ty::iter_union([literally(TextFont), Ty::Array(literally(TextFont).into())])
618            });
619            Some(FONT_TYPE.clone())
620        }
621        ("text", "feature") => {
622            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
623                Ty::iter_union([
624                    // todo: the key can only be the text feature
625                    Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Dict>())),
626                    Ty::Array(literally(TextFeature).into()),
627                ])
628            });
629            Some(FONT_TYPE.clone())
630        }
631        ("text", "costs") => {
632            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
633                Ty::Dict(flow_record!(
634                    "hyphenation" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
635                    "runt" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
636                    "widow" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
637                    "orphan" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
638                ))
639            });
640            Some(FONT_TYPE.clone())
641        }
642        ("text", "lang") => Some(literally(TextLang)),
643        ("text", "region") => Some(literally(TextRegion)),
644        ("text" | "stack", "dir") => Some(literally(Dir)),
645        ("par", "first-line-indent") => {
646            static FIRST_LINE_INDENT: LazyLock<Ty> = LazyLock::new(|| {
647                Ty::iter_union([
648                    literally(Length),
649                    Ty::Dict(RecordTy::new(vec![
650                        ("amount".into(), literally(Length)),
651                        ("all".into(), Ty::Boolean(Option::None)),
652                    ])),
653                ])
654            });
655            Some(FIRST_LINE_INDENT.clone())
656        }
657        (
658            // todo: polygon.regular
659            "page" | "highlight" | "text" | "path" | "curve" | "rect" | "ellipse" | "circle"
660            | "polygon" | "box" | "block" | "table" | "regular",
661            "fill",
662        ) => Some(literally(Color)),
663        (
664            // todo: table.cell
665            "table" | "cell" | "block" | "box" | "circle" | "ellipse" | "rect" | "square",
666            "inset",
667        ) => Some(literally(Inset)),
668        ("block" | "box" | "circle" | "ellipse" | "rect" | "square", "outset") => {
669            Some(literally(Outset))
670        }
671        ("block" | "box" | "rect" | "square" | "highlight", "radius") => Some(literally(Radius)),
672        ("grid" | "table", "columns" | "rows" | "gutter" | "column-gutter" | "row-gutter") => {
673            static COLUMN_TYPE: LazyLock<Ty> = LazyLock::new(|| {
674                flow_union!(
675                    Ty::Value(InsTy::new(Value::Auto)),
676                    Ty::Value(InsTy::new(Value::Type(Type::of::<i64>()))),
677                    literally(Length),
678                    Ty::Array(literally(Length).into()),
679                )
680            });
681            Some(COLUMN_TYPE.clone())
682        }
683        ("pattern" | "tiling", "size") => {
684            static PATTERN_SIZE_TYPE: LazyLock<Ty> = LazyLock::new(|| {
685                flow_union!(
686                    Ty::Value(InsTy::new(Value::Auto)),
687                    Ty::Array(Ty::Builtin(Length).into()),
688                )
689            });
690            Some(PATTERN_SIZE_TYPE.clone())
691        }
692        ("stroke", "dash") => Some(FLOW_STROKE_DASH_TYPE.clone()),
693        (
694            //todo: table.cell, table.hline, table.vline, math.cancel, grid.cell, polygon.regular
695            "cancel" | "highlight" | "overline" | "strike" | "underline" | "text" | "path"
696            | "curve" | "rect" | "ellipse" | "circle" | "polygon" | "box" | "block" | "table"
697            | "line" | "cell" | "hline" | "vline" | "regular",
698            "stroke",
699        ) => Some(Ty::Builtin(Stroke)),
700        ("page", "margin") => Some(Ty::Builtin(Margin)),
701        _ => Option::None,
702    }
703}
704
705/// The record component of a stroke type.
706static FLOW_STROKE_DASH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
707    flow_union!(
708        "solid",
709        "dotted",
710        "densely-dotted",
711        "loosely-dotted",
712        "dashed",
713        "densely-dashed",
714        "loosely-dashed",
715        "dash-dotted",
716        "densely-dash-dotted",
717        "loosely-dash-dotted",
718        Ty::Array(flow_union!("dot", literally(Float)).into()),
719        Ty::Dict(flow_record!(
720            "array" => Ty::Array(flow_union!("dot", literally(Float)).into()),
721            "phase" => literally(Length),
722        ))
723    )
724});
725
726/// The record component of a stroke type.
727pub static FLOW_STROKE_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
728    flow_record!(
729        "paint" => literally(Color),
730        "thickness" => literally(Length),
731        "cap" => flow_union!("butt", "round", "square"),
732        "join" => flow_union!("miter", "round", "bevel"),
733        "dash" => FLOW_STROKE_DASH_TYPE.clone(),
734        "miter-limit" => literally(Float),
735    )
736});
737
738/// The record component of a margin type.
739pub static FLOW_MARGIN_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
740    flow_record!(
741        "top" => literally(Length),
742        "right" => literally(Length),
743        "bottom" => literally(Length),
744        "left" => literally(Length),
745        "inside" => literally(Length),
746        "outside" => literally(Length),
747        "x" => literally(Length),
748        "y" => literally(Length),
749        "rest" => literally(Length),
750    )
751});
752
753/// The record component of an inset type.
754pub static FLOW_INSET_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
755    flow_record!(
756        "top" => literally(Length),
757        "right" => literally(Length),
758        "bottom" => literally(Length),
759        "left" => literally(Length),
760        "x" => literally(Length),
761        "y" => literally(Length),
762        "rest" => literally(Length),
763    )
764});
765
766/// The record component of an outset type.
767pub static FLOW_OUTSET_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
768    flow_record!(
769        "top" => literally(Length),
770        "right" => literally(Length),
771        "bottom" => literally(Length),
772        "left" => literally(Length),
773        "x" => literally(Length),
774        "y" => literally(Length),
775        "rest" => literally(Length),
776    )
777});
778
779/// The record component of a radius type.
780pub static FLOW_RADIUS_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
781    flow_record!(
782        "top" => literally(Length),
783        "right" => literally(Length),
784        "bottom" => literally(Length),
785        "left" => literally(Length),
786        "top-left" => literally(Length),
787        "top-right" => literally(Length),
788        "bottom-left" => literally(Length),
789        "bottom-right" => literally(Length),
790        "rest" => literally(Length),
791    )
792});
793
794/// The record component of a text font type.
795pub static FLOW_TEXT_FONT_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
796    flow_record!(
797        "name" => literally(TextFont),
798        "covers" => flow_union!("latin-in-cjk", BuiltinTy::Type(Type::of::<Regex>())),
799    )
800});
801
802// todo bad case: array.fold
803// todo bad case: datetime
804// todo bad case: selector
805// todo: function signatures, for example: `locate(loc => ...)`
806
807// todo: numbering/supplement
808// todo: grid/table.fill/align/stroke/inset can be a function
809// todo: math.cancel.angle can be a function
810// todo: math.mat.augment
811// todo: csv.row-type can be an array or a dictionary
812// todo: text.stylistic-set is an array of integer
813// todo: raw.lang can be completed
814// todo: smartquote.quotes can be an array or a dictionary
815// todo: mat.augment can be a dictionary
816// todo: pdf.embed mime-type can be special
817
818// ISO 639
819
820#[cfg(test)]
821mod tests {
822
823    use crate::syntax::Decl;
824
825    use super::{SigTy, Ty, TypeVar};
826
827    #[test]
828    fn test_image_extension() {
829        let path = "test.png";
830        let preference = super::PathKind::from_ext(path).unwrap();
831        assert_eq!(preference, super::PathKind::Image);
832    }
833
834    #[test]
835    fn test_image_extension_uppercase() {
836        let path = "TEST.PNG";
837        let preference = super::PathKind::from_ext(path).unwrap();
838        assert_eq!(preference, super::PathKind::Image);
839    }
840
841    // todo: map function
842    // Technical Note for implementing a map function:
843    // `u`, `v` is in level 2
844    // instantiate a `v` as the return type of the map function.
845    #[test]
846    fn test_map() {
847        let u = Ty::Var(TypeVar::new("u".into(), Decl::lit("u").into()));
848        let v = Ty::Var(TypeVar::new("v".into(), Decl::lit("v").into()));
849        let mapper_fn =
850            Ty::Func(SigTy::new([u].into_iter(), None, None, None, Some(v.clone())).into());
851        let map_fn =
852            Ty::Func(SigTy::new([mapper_fn].into_iter(), None, None, None, Some(v)).into());
853        let _ = map_fn;
854        // println!("{map_fn:?}");
855    }
856}