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