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