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}
212
213/// A package identifier.
214#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
215pub struct PackageId {
216    /// The namespace of the package.
217    pub namespace: StrRef,
218    /// The name of the package.
219    pub name: StrRef,
220}
221
222impl fmt::Debug for PackageId {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "@{}/{}", self.namespace, self.name)
225    }
226}
227
228impl TryFrom<FileId> for PackageId {
229    type Error = ();
230
231    fn try_from(value: FileId) -> Result<Self, Self::Error> {
232        let spec = value.package_compat().ok_or(())?;
233        Ok(PackageId {
234            namespace: spec.namespace.as_str().into(),
235            name: spec.name.as_str().into(),
236        })
237    }
238}
239
240/// A builtin type.
241#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
242pub enum BuiltinTy {
243    /// A clause type.
244    Clause,
245    /// An undefined type.
246    Undef,
247    /// A space type: `[ ]`
248    Space,
249    /// A none type: `none`
250    None,
251    /// A break type: `break`
252    Break,
253    /// A continue type: `continue`
254    Continue,
255    /// An infer type: `any`
256    Infer,
257    /// A flow none type: `none`
258    FlowNone,
259    /// An auto type: `auto`
260    Auto,
261
262    /// Arguments: `arguments(a, b: c, ..d)`
263    Args,
264    /// A color type: `rgb(r, g, b)`
265    Color,
266    /// A text size type: `text.size`
267    TextSize,
268    /// A text font type: `text.font`
269    TextFont,
270    /// A text feature type: `text.feature`
271    TextFeature,
272    /// A text language type: `text.lang`
273    TextLang,
274    /// A text region type: `text.region`
275    TextRegion,
276    /// A dir type: `left`
277    Dir,
278    /// A label type: `<label>`
279    Label,
280    /// A cite label type: `#cite(<label>)`
281    CiteLabel,
282    /// A ref label type: `@label`
283    RefLabel,
284    /// A length type: `10pt`
285    Length,
286    /// A float type: `1.0`
287    Float,
288    /// A stroke type: `stroke(paint: red)`
289    Stroke,
290    /// A margin type: `page(margin: 10pt)`
291    Margin,
292    /// An inset type: `box(inset: 10pt)`
293    Inset,
294    /// An outset type: `box(outset: 10pt)`
295    Outset,
296    /// A radius type: `box(radius: 10pt)`
297    Radius,
298
299    /// A tag type: `tag`
300    Tag(Box<(StrRef, Option<Interned<PackageId>>)>),
301
302    /// The type of a value: `int` of `10`
303    Type(typst::foundations::Type),
304    /// The type of a type: `type(int)`
305    TypeType(typst::foundations::Type),
306    /// The element type of a content value. For example, `#[text]` has
307    /// element type `text`.
308    ///
309    /// If the element is not specified, the element type is `content`.
310    Content(Option<typst::foundations::Element>),
311    /// The type of an element: `text`
312    Element(typst::foundations::Element),
313
314    /// A module type: `module(foo)`
315    Module(Interned<Decl>),
316    /// A path type: `import "foo.typ"`
317    Path(PathKind),
318}
319
320impl fmt::Debug for BuiltinTy {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        match self {
323            BuiltinTy::Clause => f.write_str("Clause"),
324            BuiltinTy::Undef => f.write_str("Undef"),
325            BuiltinTy::Content(ty) => {
326                if let Some(ty) = ty {
327                    write!(f, "Content({})", ty.name())
328                } else {
329                    f.write_str("Content")
330                }
331            }
332            BuiltinTy::Space => f.write_str("Space"),
333            BuiltinTy::None => f.write_str("None"),
334            BuiltinTy::Break => f.write_str("Break"),
335            BuiltinTy::Continue => f.write_str("Continue"),
336            BuiltinTy::Infer => f.write_str("Infer"),
337            BuiltinTy::FlowNone => f.write_str("FlowNone"),
338            BuiltinTy::Auto => f.write_str("Auto"),
339
340            BuiltinTy::Args => write!(f, "Args"),
341            BuiltinTy::Color => write!(f, "Color"),
342            BuiltinTy::TextSize => write!(f, "TextSize"),
343            BuiltinTy::TextFont => write!(f, "TextFont"),
344            BuiltinTy::TextFeature => write!(f, "TextFeature"),
345            BuiltinTy::TextLang => write!(f, "TextLang"),
346            BuiltinTy::TextRegion => write!(f, "TextRegion"),
347            BuiltinTy::Dir => write!(f, "Dir"),
348            BuiltinTy::Length => write!(f, "Length"),
349            BuiltinTy::Label => write!(f, "Label"),
350            BuiltinTy::CiteLabel => write!(f, "CiteLabel"),
351            BuiltinTy::RefLabel => write!(f, "RefLabel"),
352            BuiltinTy::Float => write!(f, "Float"),
353            BuiltinTy::Stroke => write!(f, "Stroke"),
354            BuiltinTy::Margin => write!(f, "Margin"),
355            BuiltinTy::Inset => write!(f, "Inset"),
356            BuiltinTy::Outset => write!(f, "Outset"),
357            BuiltinTy::Radius => write!(f, "Radius"),
358            BuiltinTy::TypeType(ty) => write!(f, "TypeType({})", ty.short_name()),
359            BuiltinTy::Type(ty) => write!(f, "Type({})", ty.short_name()),
360            BuiltinTy::Element(elem) => elem.fmt(f),
361            BuiltinTy::Tag(tag) => {
362                let (name, id) = tag.as_ref();
363                if let Some(id) = id {
364                    write!(f, "Tag({name:?}) of {id:?}")
365                } else {
366                    write!(f, "Tag({name:?})")
367                }
368            }
369            BuiltinTy::Module(decl) => write!(f, "{decl:?}"),
370            BuiltinTy::Path(preference) => write!(f, "Path({preference:?})"),
371        }
372    }
373}
374
375impl BuiltinTy {
376    /// Converts a value to a type.
377    pub fn from_value(builtin: &Value) -> Ty {
378        if let Value::Bool(v) = builtin {
379            return Ty::Boolean(Some(*v));
380        }
381
382        Self::from_builtin(builtin.ty())
383    }
384
385    /// Converts a builtin type to a type.
386    pub fn from_builtin(builtin: Type) -> Ty {
387        if builtin == Type::of::<AutoValue>() {
388            return Ty::Builtin(BuiltinTy::Auto);
389        }
390        if builtin == Type::of::<NoneValue>() {
391            return Ty::Builtin(BuiltinTy::None);
392        }
393        if builtin == Type::of::<typst::visualize::Color>() {
394            return Color.literally();
395        }
396        if builtin == Type::of::<bool>() {
397            return Ty::Builtin(BuiltinTy::None);
398        }
399        if builtin == Type::of::<f64>() {
400            return Float.literally();
401        }
402        if builtin == Type::of::<Length>() {
403            return Length.literally();
404        }
405        if builtin == Type::of::<Content>() {
406            return Ty::Builtin(BuiltinTy::Content(Option::None));
407        }
408
409        BuiltinTy::Type(builtin).literally()
410    }
411
412    /// Describes the builtin type.
413    pub(crate) fn describe(&self) -> EcoString {
414        let res = match self {
415            BuiltinTy::Clause => "any",
416            BuiltinTy::Undef => "any",
417            BuiltinTy::Content(ty) => {
418                return if let Some(ty) = ty {
419                    eco_format!("content({})", ty.name())
420                } else {
421                    "content".into()
422                };
423            }
424            BuiltinTy::Space => "content",
425            BuiltinTy::None => "none",
426            BuiltinTy::Break => "break",
427            BuiltinTy::Continue => "continue",
428            BuiltinTy::Infer => "any",
429            BuiltinTy::FlowNone => "none",
430            BuiltinTy::Auto => "auto",
431
432            BuiltinTy::Args => "arguments",
433            BuiltinTy::Color => "color",
434            BuiltinTy::TextSize => "text.size",
435            BuiltinTy::TextFont => "text.font",
436            BuiltinTy::TextFeature => "text.feature",
437            BuiltinTy::TextLang => "text.lang",
438            BuiltinTy::TextRegion => "text.region",
439            BuiltinTy::Dir => "dir",
440            BuiltinTy::Length => "length",
441            BuiltinTy::Float => "float",
442            BuiltinTy::Label => "label",
443            BuiltinTy::CiteLabel => "cite-label",
444            BuiltinTy::RefLabel => "ref-label",
445            BuiltinTy::Stroke => "stroke",
446            BuiltinTy::Margin => "margin",
447            BuiltinTy::Inset => "inset",
448            BuiltinTy::Outset => "outset",
449            BuiltinTy::Radius => "radius",
450            BuiltinTy::TypeType(..) => "type",
451            BuiltinTy::Type(ty) => ty.short_name(),
452            BuiltinTy::Element(ty) => ty.name(),
453            BuiltinTy::Tag(tag) => {
454                let (name, id) = tag.as_ref();
455                return if let Some(id) = id {
456                    eco_format!("tag {name} of {id:?}")
457                } else {
458                    eco_format!("tag {name}")
459                };
460            }
461            BuiltinTy::Module(m) => return eco_format!("module({})", m.name()),
462            BuiltinTy::Path(s) => match s {
463                PathKind::None => "[any]",
464                PathKind::Special => "[any]",
465                PathKind::Source { .. } => "[source]",
466                PathKind::Wasm => "[wasm]",
467                PathKind::Csv => "[csv]",
468                PathKind::Image => "[image]",
469                PathKind::Json => "[json]",
470                PathKind::Yaml => "[yaml]",
471                PathKind::Xml => "[xml]",
472                PathKind::Toml => "[toml]",
473                PathKind::Csl => "[csl]",
474                PathKind::Bibliography => "[bib]",
475                PathKind::RawTheme => "[theme]",
476                PathKind::RawSyntax => "[syntax]",
477            },
478        };
479
480        res.into()
481    }
482}
483
484use BuiltinTy::*;
485
486/// Converts a flow builtin to a type.
487fn literally(s: impl FlowBuiltinLiterally) -> Ty {
488    s.literally()
489}
490
491/// A trait for converting a flow builtin to a type.
492trait FlowBuiltinLiterally {
493    fn literally(self) -> Ty;
494}
495
496impl FlowBuiltinLiterally for &str {
497    fn literally(self) -> Ty {
498        Ty::Value(InsTy::new(Value::Str(self.into())))
499    }
500}
501
502impl FlowBuiltinLiterally for BuiltinTy {
503    fn literally(self) -> Ty {
504        Ty::Builtin(self.clone())
505    }
506}
507
508impl FlowBuiltinLiterally for Ty {
509    fn literally(self) -> Ty {
510        self
511    }
512}
513
514/// A macro for converting a flow builtin to a type.
515macro_rules! flow_builtin_union_inner {
516    ($literal_kind:expr) => {
517        literally($literal_kind)
518    };
519    ($($x:expr),+ $(,)?) => {
520        Vec::from_iter([
521            $(flow_builtin_union_inner!($x)),*
522        ])
523    };
524}
525
526/// A macro for converting a flow builtin to a type.
527macro_rules! flow_union {
528    // the first one is string
529    ($($b:tt)*) => {
530        Ty::iter_union(flow_builtin_union_inner!( $($b)* ).into_iter())
531    };
532
533}
534
535/// A macro for converting a flow builtin to a type.
536macro_rules! flow_record {
537    ($($name:expr => $ty:expr),* $(,)?) => {
538        RecordTy::new(vec![
539            $(
540                (
541                    $name.into(),
542                    $ty,
543                ),
544            )*
545        ])
546    };
547}
548
549/// Maps a function parameter to a type.
550pub(super) fn param_mapping(func: &Func, param: &ParamInfo) -> Option<Ty> {
551    // todo: remove path params which is compatible with 0.12.0
552    let input_ty = || {
553        param
554            .to_native()
555            .map(|native| Ty::from_cast_info(&native.input))
556            .unwrap_or(Ty::Any)
557    };
558
559    match (func.name()?, param.name()?) {
560        // todo: pdf.embed
561        ("embed", "path") => Some(literally(Path(PathKind::None))),
562        ("cbor", "path" | "source") => Some(literally(Path(PathKind::None))),
563        ("plugin", "source") => Some(literally(Path(PathKind::Wasm))),
564        ("csv", "path" | "source") => Some(literally(Path(PathKind::Csv))),
565        ("image", "path" | "source") => Some(literally(Path(PathKind::Image))),
566        ("read", "path" | "source") => Some(literally(Path(PathKind::None))),
567        ("json", "path" | "source") => Some(literally(Path(PathKind::Json))),
568        ("yaml", "path" | "source") => Some(literally(Path(PathKind::Yaml))),
569        ("xml", "path" | "source") => Some(literally(Path(PathKind::Xml))),
570        ("toml", "path" | "source") => Some(literally(Path(PathKind::Toml))),
571        ("raw", "theme") => Some(literally(Path(PathKind::RawTheme))),
572        ("raw", "syntaxes") => Some(literally(Path(PathKind::RawSyntax))),
573        ("bibliography" | "cite", "style") => {
574            Some(Ty::iter_union([literally(Path(PathKind::Csl)), input_ty()]))
575        }
576        ("cite", "key") => Some(Ty::iter_union([literally(CiteLabel)])),
577        ("ref", "target") => Some(Ty::iter_union([literally(RefLabel)])),
578        ("footnote", "body") => Some(Ty::iter_union([literally(RefLabel), input_ty()])),
579        ("link", "dest") => {
580            static LINK_DEST_TYPE: LazyLock<Ty> = LazyLock::new(|| {
581                flow_union!(
582                    literally(RefLabel),
583                    Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Str>())),
584                    Ty::Builtin(BuiltinTy::Type(Type::of::<typst::introspection::Location>())),
585                    Ty::Dict(RecordTy::new(vec![
586                        ("x".into(), literally(Length)),
587                        ("y".into(), literally(Length)),
588                    ])),
589                )
590            });
591            Some(LINK_DEST_TYPE.clone())
592        }
593        ("bibliography", "path" | "sources") => {
594            static BIB_PATH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
595                let bib_path_ty = literally(Path(PathKind::Bibliography));
596                Ty::iter_union([bib_path_ty.clone(), Ty::Array(bib_path_ty.into())])
597            });
598            Some(BIB_PATH_TYPE.clone())
599        }
600        ("text", "size") => Some(literally(TextSize)),
601        ("text", "font") => {
602            // todo: the dict can be completed, but we have bugs...
603            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
604                Ty::iter_union([literally(TextFont), Ty::Array(literally(TextFont).into())])
605            });
606            Some(FONT_TYPE.clone())
607        }
608        ("text", "feature") => {
609            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
610                Ty::iter_union([
611                    // todo: the key can only be the text feature
612                    Ty::Builtin(BuiltinTy::Type(Type::of::<foundations::Dict>())),
613                    Ty::Array(literally(TextFeature).into()),
614                ])
615            });
616            Some(FONT_TYPE.clone())
617        }
618        ("text", "costs") => {
619            static FONT_TYPE: LazyLock<Ty> = LazyLock::new(|| {
620                Ty::Dict(flow_record!(
621                    "hyphenation" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
622                    "runt" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
623                    "widow" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
624                    "orphan" => literally(BuiltinTy::Type(Type::of::<Ratio>())),
625                ))
626            });
627            Some(FONT_TYPE.clone())
628        }
629        ("text", "lang") => Some(literally(TextLang)),
630        ("text", "region") => Some(literally(TextRegion)),
631        ("text" | "stack", "dir") => Some(literally(Dir)),
632        ("par", "first-line-indent") => {
633            static FIRST_LINE_INDENT: LazyLock<Ty> = LazyLock::new(|| {
634                Ty::iter_union([
635                    literally(Length),
636                    Ty::Dict(RecordTy::new(vec![
637                        ("amount".into(), literally(Length)),
638                        ("all".into(), Ty::Boolean(Option::None)),
639                    ])),
640                ])
641            });
642            Some(FIRST_LINE_INDENT.clone())
643        }
644        (
645            // todo: polygon.regular
646            "page" | "highlight" | "text" | "path" | "curve" | "rect" | "ellipse" | "circle"
647            | "polygon" | "box" | "block" | "table" | "regular",
648            "fill",
649        ) => Some(literally(Color)),
650        (
651            // todo: table.cell
652            "table" | "cell" | "block" | "box" | "circle" | "ellipse" | "rect" | "square",
653            "inset",
654        ) => Some(literally(Inset)),
655        ("block" | "box" | "circle" | "ellipse" | "rect" | "square", "outset") => {
656            Some(literally(Outset))
657        }
658        ("block" | "box" | "rect" | "square" | "highlight", "radius") => Some(literally(Radius)),
659        ("grid" | "table", "columns" | "rows" | "gutter" | "column-gutter" | "row-gutter") => {
660            static COLUMN_TYPE: LazyLock<Ty> = LazyLock::new(|| {
661                flow_union!(
662                    Ty::Value(InsTy::new(Value::Auto)),
663                    Ty::Value(InsTy::new(Value::Type(Type::of::<i64>()))),
664                    literally(Length),
665                    Ty::Array(literally(Length).into()),
666                )
667            });
668            Some(COLUMN_TYPE.clone())
669        }
670        ("pattern" | "tiling", "size") => {
671            static PATTERN_SIZE_TYPE: LazyLock<Ty> = LazyLock::new(|| {
672                flow_union!(
673                    Ty::Value(InsTy::new(Value::Auto)),
674                    Ty::Array(Ty::Builtin(Length).into()),
675                )
676            });
677            Some(PATTERN_SIZE_TYPE.clone())
678        }
679        ("stroke", "dash") => Some(FLOW_STROKE_DASH_TYPE.clone()),
680        (
681            //todo: table.cell, table.hline, table.vline, math.cancel, grid.cell, polygon.regular
682            "cancel" | "highlight" | "overline" | "strike" | "underline" | "text" | "path"
683            | "curve" | "rect" | "ellipse" | "circle" | "polygon" | "box" | "block" | "table"
684            | "line" | "cell" | "hline" | "vline" | "regular",
685            "stroke",
686        ) => Some(Ty::Builtin(Stroke)),
687        ("page", "margin") => Some(Ty::Builtin(Margin)),
688        _ => Option::None,
689    }
690}
691
692/// The record component of a stroke type.
693static FLOW_STROKE_DASH_TYPE: LazyLock<Ty> = LazyLock::new(|| {
694    flow_union!(
695        "solid",
696        "dotted",
697        "densely-dotted",
698        "loosely-dotted",
699        "dashed",
700        "densely-dashed",
701        "loosely-dashed",
702        "dash-dotted",
703        "densely-dash-dotted",
704        "loosely-dash-dotted",
705        Ty::Array(flow_union!("dot", literally(Float)).into()),
706        Ty::Dict(flow_record!(
707            "array" => Ty::Array(flow_union!("dot", literally(Float)).into()),
708            "phase" => literally(Length),
709        ))
710    )
711});
712
713/// The record component of a stroke type.
714pub static FLOW_STROKE_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
715    flow_record!(
716        "paint" => literally(Color),
717        "thickness" => literally(Length),
718        "cap" => flow_union!("butt", "round", "square"),
719        "join" => flow_union!("miter", "round", "bevel"),
720        "dash" => FLOW_STROKE_DASH_TYPE.clone(),
721        "miter-limit" => literally(Float),
722    )
723});
724
725/// The record component of a margin type.
726pub static FLOW_MARGIN_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
727    flow_record!(
728        "top" => literally(Length),
729        "right" => literally(Length),
730        "bottom" => literally(Length),
731        "left" => literally(Length),
732        "inside" => literally(Length),
733        "outside" => literally(Length),
734        "x" => literally(Length),
735        "y" => literally(Length),
736        "rest" => literally(Length),
737    )
738});
739
740/// The record component of an inset type.
741pub static FLOW_INSET_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
742    flow_record!(
743        "top" => literally(Length),
744        "right" => literally(Length),
745        "bottom" => literally(Length),
746        "left" => literally(Length),
747        "x" => literally(Length),
748        "y" => literally(Length),
749        "rest" => literally(Length),
750    )
751});
752
753/// The record component of an outset type.
754pub static FLOW_OUTSET_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 a radius type.
767pub static FLOW_RADIUS_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        "top-left" => literally(Length),
774        "top-right" => literally(Length),
775        "bottom-left" => literally(Length),
776        "bottom-right" => literally(Length),
777        "rest" => literally(Length),
778    )
779});
780
781/// The record component of a text font type.
782pub static FLOW_TEXT_FONT_DICT: LazyLock<Interned<RecordTy>> = LazyLock::new(|| {
783    flow_record!(
784        "name" => literally(TextFont),
785        "covers" => flow_union!("latin-in-cjk", BuiltinTy::Type(Type::of::<Regex>())),
786    )
787});
788
789// todo bad case: array.fold
790// todo bad case: datetime
791// todo bad case: selector
792// todo: function signatures, for example: `locate(loc => ...)`
793
794// todo: numbering/supplement
795// todo: grid/table.fill/align/stroke/inset can be a function
796// todo: math.cancel.angle can be a function
797// todo: math.mat.augment
798// todo: csv.row-type can be an array or a dictionary
799// todo: text.stylistic-set is an array of integer
800// todo: raw.lang can be completed
801// todo: smartquote.quotes can be an array or a dictionary
802// todo: mat.augment can be a dictionary
803// todo: pdf.embed mime-type can be special
804
805// ISO 639
806
807#[cfg(test)]
808mod tests {
809
810    use crate::syntax::Decl;
811
812    use super::{SigTy, Ty, TypeVar};
813
814    #[test]
815    fn test_image_extension() {
816        let path = "test.png";
817        let preference = super::PathKind::from_ext(path).unwrap();
818        assert_eq!(preference, super::PathKind::Image);
819    }
820
821    #[test]
822    fn test_image_extension_uppercase() {
823        let path = "TEST.PNG";
824        let preference = super::PathKind::from_ext(path).unwrap();
825        assert_eq!(preference, super::PathKind::Image);
826    }
827
828    // todo: map function
829    // Technical Note for implementing a map function:
830    // `u`, `v` is in level 2
831    // instantiate a `v` as the return type of the map function.
832    #[test]
833    fn test_map() {
834        let u = Ty::Var(TypeVar::new("u".into(), Decl::lit("u").into()));
835        let v = Ty::Var(TypeVar::new("v".into(), Decl::lit("v").into()));
836        let mapper_fn =
837            Ty::Func(SigTy::new([u].into_iter(), None, None, None, Some(v.clone())).into());
838        let map_fn =
839            Ty::Func(SigTy::new([mapper_fn].into_iter(), None, None, None, Some(v)).into());
840        let _ = map_fn;
841        // println!("{map_fn:?}");
842    }
843}