tiger_lib/
datatype.rs

1//! Validator for the `[ ... ]` code blocks in localization and gui files.
2//! The main entry points are the [`validate_datatypes`] function and the [`Datatype`] enum.
3
4use std::borrow::Cow;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7use std::sync::LazyLock;
8
9use phf::phf_map;
10use strum_macros::{Display, EnumString};
11
12#[cfg(feature = "ck3")]
13use crate::ck3::data::religions::CUSTOM_RELIGION_LOCAS;
14use crate::context::ScopeContext;
15#[cfg(feature = "jomini")]
16use crate::data::customloca::CustomLocalization;
17use crate::data::localization::Language;
18#[cfg(feature = "jomini")]
19use crate::data::scripted_guis::ScriptedGui;
20use crate::datacontext::DataContext;
21use crate::everything::Everything;
22use crate::game::Game;
23use crate::helpers::BiTigerHashMap;
24#[cfg(feature = "hoi4")]
25use crate::helpers::is_country_tag;
26#[cfg(feature = "hoi4")]
27use crate::hoi4::data::scripted_localisation::ScriptedLocalisation;
28use crate::item::Item;
29#[cfg(feature = "hoi4")]
30use crate::report::Severity;
31#[cfg(feature = "jomini")]
32use crate::report::err;
33use crate::report::{ErrorKey, warn};
34use crate::scopes::Scopes;
35use crate::token::Token;
36
37// Load the game-specific datatype definitions
38#[cfg(feature = "ck3")]
39include!("ck3/tables/include/datatypes.rs");
40#[cfg(feature = "vic3")]
41include!("vic3/tables/include/datatypes.rs");
42#[cfg(feature = "imperator")]
43include!("imperator/tables/include/datatypes.rs");
44#[cfg(feature = "eu5")]
45include!("eu5/tables/include/datatypes.rs");
46#[cfg(feature = "hoi4")]
47include!("hoi4/tables/include/datatypes.rs");
48
49/// All the object types used in `[...]` code in localization and gui files.
50///
51/// The names exactly match the ones in the `data_types` logs from the games,
52/// which is why some of them are lowercase.
53/// Most of the variants are generated directly from those logs.
54///
55/// The enum is divided into the "generic" datatypes, which are valid for all games and which can
56/// be referenced directly in code, and the per-game lists of datatypes which are in game-specific
57/// wrappers. With a few exceptions, the per-game datatypes are only referenced in the per-game tables
58/// of datafunctions and promotes.
59///
60/// The game-specific datatypes are wrapped because otherwise they would still have name
61/// collisions. This is because the list of generic datatypes is only a small selection; there are
62/// many more datatypes that are in effect generic but separating them out would be pointless work.
63/// (Separating them out would be made harder because the lists of variants are generated from the docs).
64#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
65#[allow(non_camel_case_types)]
66pub enum Datatype {
67    // Synthetic datatypes for our typechecking
68    Unknown,
69    AnyScope,
70
71    // The generic datatypes
72    CFixedPoint,
73    CString,
74    CUTF8String,
75    CVector2f,
76    CVector2i,
77    CVector3f,
78    CVector3i,
79    CVector4f,
80    CVector4i,
81    Date,
82    Scope,
83    TopScope,
84    bool,
85    double,
86    float,
87    int16,
88    int32,
89    int64,
90    int8,
91    uint16,
92    uint32,
93    uint64,
94    uint8,
95    void,
96
97    // Wrappers for the per-game datatypes
98    #[cfg(feature = "ck3")]
99    Ck3(Ck3Datatype),
100    #[cfg(feature = "vic3")]
101    Vic3(Vic3Datatype),
102    #[cfg(feature = "imperator")]
103    Imperator(ImperatorDatatype),
104    #[cfg(feature = "eu5")]
105    Eu5(Eu5Datatype),
106    #[cfg(feature = "hoi4")]
107    Hoi4(Hoi4Datatype),
108}
109
110static STR_DATATYPE_MAP: phf::Map<&'static str, Datatype> = phf_map! {
111    "Unknown" => Datatype::Unknown,
112    "AnyScope" => Datatype::AnyScope,
113    "CFixedPoint" => Datatype::CFixedPoint,
114    "CString" => Datatype::CString,
115    "CUTF8String" => Datatype::CUTF8String,
116    "CVector2f" => Datatype::CVector2f,
117    "CVector2i" => Datatype::CVector2i,
118    "CVector3f" => Datatype::CVector3f,
119    "CVector3i" => Datatype::CVector3i,
120    "CVector4f" => Datatype::CVector4f,
121    "CVector4i" => Datatype::CVector4i,
122    "Date" => Datatype::Date,
123    "Scope" => Datatype::Scope,
124    "TopScope" => Datatype::TopScope,
125    "bool" => Datatype::bool,
126    "double" => Datatype::double,
127    "float" => Datatype::float,
128    "int16" => Datatype::int16,
129    "int32" => Datatype::int32,
130    "int64" => Datatype::int64,
131    "int8" => Datatype::int8,
132    "uint16" => Datatype::uint16,
133    "uint32" => Datatype::uint32,
134    "uint64" => Datatype::uint64,
135    "uint8" => Datatype::uint8,
136    "void" => Datatype::void,
137};
138
139impl FromStr for Datatype {
140    type Err = strum::ParseError;
141    /// Read a Datatype from a string, without requiring the string to use the game-specific wrappers.
142    fn from_str(s: &str) -> Result<Self, strum::ParseError> {
143        STR_DATATYPE_MAP.get(s).copied().ok_or(strum::ParseError::VariantNotFound).or_else(|_| {
144            match Game::game() {
145                #[cfg(feature = "ck3")]
146                Game::Ck3 => Ck3Datatype::from_str(s).map(Datatype::Ck3),
147                #[cfg(feature = "vic3")]
148                Game::Vic3 => Vic3Datatype::from_str(s).map(Datatype::Vic3),
149                #[cfg(feature = "imperator")]
150                Game::Imperator => ImperatorDatatype::from_str(s).map(Datatype::Imperator),
151                #[cfg(feature = "eu5")]
152                Game::Eu5 => Eu5Datatype::from_str(s).map(Datatype::Eu5),
153                #[cfg(feature = "hoi4")]
154                Game::Hoi4 => Hoi4Datatype::from_str(s).map(Datatype::Hoi4),
155            }
156        })
157    }
158}
159
160impl Display for Datatype {
161    /// Convert a `Datatype` to string format, while leaving out the game-specific wrappers.
162    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
163        // Have to do the generic variants by hand, so that the per-game variants can be done with the macro.
164        match *self {
165            Datatype::Unknown => write!(f, "Unknown"),
166            Datatype::AnyScope => write!(f, "AnyScope"),
167            Datatype::CFixedPoint => write!(f, "CFixedPoint"),
168            Datatype::CString => write!(f, "CString"),
169            Datatype::CUTF8String => write!(f, "CUTF8String"),
170            Datatype::CVector2f => write!(f, "CVector2f"),
171            Datatype::CVector2i => write!(f, "CVector2i"),
172            Datatype::CVector3f => write!(f, "CVector3f"),
173            Datatype::CVector3i => write!(f, "CVector3i"),
174            Datatype::CVector4f => write!(f, "CVector4f"),
175            Datatype::CVector4i => write!(f, "CVector4i"),
176            Datatype::Date => write!(f, "Date"),
177            Datatype::Scope => write!(f, "Scope"),
178            Datatype::TopScope => write!(f, "TopScope"),
179            Datatype::bool => write!(f, "bool"),
180            Datatype::double => write!(f, "double"),
181            Datatype::float => write!(f, "float"),
182            Datatype::int16 => write!(f, "int16"),
183            Datatype::int32 => write!(f, "int32"),
184            Datatype::int64 => write!(f, "int64"),
185            Datatype::int8 => write!(f, "int8"),
186            Datatype::uint16 => write!(f, "uint16"),
187            Datatype::uint32 => write!(f, "uint32"),
188            Datatype::uint64 => write!(f, "uint64"),
189            Datatype::uint8 => write!(f, "uint8"),
190            Datatype::void => write!(f, "void"),
191            #[cfg(feature = "ck3")]
192            Datatype::Ck3(dt) => dt.fmt(f),
193            #[cfg(feature = "vic3")]
194            Datatype::Vic3(dt) => dt.fmt(f),
195            #[cfg(feature = "imperator")]
196            Datatype::Imperator(dt) => dt.fmt(f),
197            #[cfg(feature = "eu5")]
198            Datatype::Eu5(dt) => dt.fmt(f),
199            #[cfg(feature = "hoi4")]
200            Datatype::Hoi4(dt) => dt.fmt(f),
201        }
202    }
203}
204
205/// A [`CodeChain`] represents the full string between `[` and `]` in gui and localization (except for
206/// the trailing format).
207/// It consists of a series of codes separated by dots.
208///
209/// "code" is my name for the things separated by dots. They don't have an official name.
210/// They should be a series of "promotes" followed by a final "function",
211/// each of which can possibly take arguments. The first code should be "global", meaning it
212/// doesn't need a [`Datatype`] from the previous code as input.
213///
214/// There are a few exceptions that don't take a "function" at the end and are just a list of "promotes".
215///
216/// A `CodeChain` can also be very simple and consist of a single identifier, which should be a
217/// global function because it both starts and ends the chain.
218#[derive(Clone, Debug, Default)]
219pub struct CodeChain {
220    pub codes: Box<[Code]>,
221}
222
223/// Most codes are just a name followed by another dot or by the end of the code chain.
224/// Some have comma-separated arguments between parentheses.
225/// Those arguments can be single-quoted strings or other code chains.
226#[derive(Clone, Debug)]
227pub struct Code {
228    pub name: Token,
229    pub arguments: Vec<CodeArg>,
230}
231
232/// `CodeArg` represents a single argument of a [`Code`].
233#[derive(Clone, Debug)]
234#[allow(dead_code)] // hoi4 does not use CodeChain
235pub enum CodeArg {
236    /// An argument that is itself a [`CodeChain`], though it doesn't need the `[` `]` around it.
237    Chain(CodeChain),
238    /// An argument that is a literal string between single quotes. The literal can start with a
239    /// datatype in front of it between parentheses, such as `'(int32)0'`. If it doesn't start
240    /// with a datatype, the literal's type will be `CString`.
241    Literal(Token),
242}
243
244impl CodeChain {
245    #[cfg(feature = "ck3")]
246    pub fn as_gameconcept(&self) -> Option<&Token> {
247        if self.codes.len() == 1 && self.codes[0].arguments.is_empty() {
248            Some(&self.codes[0].name)
249        } else if self.codes.len() == 1
250            && self.codes[0].name.is("Concept")
251            && self.codes[0].arguments.len() == 2
252        {
253            if let CodeArg::Literal(token) = &self.codes[0].arguments[0] {
254                Some(token)
255            } else {
256                None
257            }
258        } else {
259            None
260        }
261    }
262
263    #[cfg(feature = "jomini")]
264    pub fn without_last(&self) -> Self {
265        if self.codes.is_empty() {
266            CodeChain { codes: Box::new([]) }
267        } else {
268            CodeChain { codes: Box::from(&self.codes[..self.codes.len() - 1]) }
269        }
270    }
271}
272
273/// [`Arg`] is the counterpart to [`CodeArg`]. Where `CodeArg` represents an actual argument given
274/// in a codechain string, the `Arg` represents what kind of argument is expected by a promote or
275/// function.
276#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
277pub enum Arg {
278    /// The argument is expected to be a code chain whose final function returns this [`Datatype`],
279    /// or a literal that is encoded to be of the expected type.
280    #[cfg(feature = "jomini")]
281    DType(Datatype),
282    /// The argument is expected to be a literal containing a key to this [`Item`] type, or a code
283    /// chain that returns a `CString` (in which case the `Item` lookup is not checked).
284    #[cfg(feature = "jomini")]
285    IType(Item),
286}
287
288/// [`Args`] is the list of arguments expected by a given promote or function. The actual arguments
289/// from a [`Code`] can be checked against this. The special value `Args::Unknown` means that all
290/// arguments are accepted.
291#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
292pub enum Args {
293    Unknown,
294    Args(&'static [Arg]),
295}
296
297/// Result from looking up a name in the promotes or functions tables.
298#[derive(Copy, Clone, Debug)]
299enum LookupResult {
300    /// The name didn't occur in the table at all.
301    NotFound,
302    /// The name was in the table, but not associated with the given [`Datatype`].
303    WrongType,
304    /// Found a matching entry.
305    /// Returns the expected arguments for this promote or function, and its return type.
306    Found(Args, Datatype),
307}
308
309/// Internal function for validating a reference to a custom localization.
310///
311/// * `token`: The name of the localization.
312/// * `scopes`: The scope type of the value being passed in to the custom localization.
313/// * `lang`: The language being validated, can be `None` when not applicable (such as in gui files).
314///   Many custom localizations are only meant for one language, and the keys they use only need
315///   to exist in that language.
316#[cfg(feature = "jomini")]
317fn validate_custom(token: &Token, data: &Everything, scopes: Scopes, lang: Option<Language>) {
318    data.verify_exists(Item::CustomLocalization, token);
319    if let Some((key, block)) = data.get_key_block(Item::CustomLocalization, token.as_str()) {
320        CustomLocalization::validate_custom_call(key, block, data, token, scopes, lang, "", None);
321    }
322}
323
324/// Internal function for validating an argument to a datatype code.
325/// If the argument is iself a code chain, this will end up calling `validate_datatypes` recursively.
326///
327/// * `arg`: The actual argument being supplied.
328/// * `sc`: The available named scopes.
329/// * `expect_arg`: The form of argument expected by the promote or function.
330/// * `lang`: The language of the localization file in which this code appears. This is just passed through.
331/// * `format`: The formatting code for this code chain. This just passed through.
332#[cfg(feature = "jomini")]
333fn validate_argument(
334    arg: &CodeArg,
335    data: &Everything,
336    sc: &mut ScopeContext,
337    dc: &DataContext,
338    expect_arg: Arg,
339    lang: Option<Language>,
340    format: Option<&Token>,
341) {
342    match expect_arg {
343        Arg::DType(expect_type) => {
344            match arg {
345                CodeArg::Chain(chain) => {
346                    validate_datatypes(chain, data, sc, dc, expect_type, lang, format, false);
347                }
348                CodeArg::Literal(token) => {
349                    if token.as_str().starts_with('(') && token.as_str().contains(')') {
350                        // These unwraps are safe because of the checks in the if condition
351                        let dtype =
352                            token.as_str().split(')').next().unwrap().strip_prefix('(').unwrap();
353                        if dtype == "hex" {
354                            if expect_type != Datatype::Unknown && expect_type != Datatype::int32 {
355                                let msg = format!("expected {expect_type}, got {dtype}");
356                                warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
357                            }
358                        } else if let Ok(dtype) = Datatype::from_str(dtype) {
359                            if expect_type != Datatype::Unknown && expect_type != dtype {
360                                let msg = format!("expected {expect_type}, got {dtype}");
361                                warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
362                            }
363                        } else {
364                            let msg = format!("unrecognized datatype {dtype}");
365                            warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
366                        }
367                    } else if expect_type != Datatype::Unknown && expect_type != Datatype::CString {
368                        let msg = format!("expected {expect_type}, got CString");
369                        warn(ErrorKey::Datafunctions).msg(msg).loc(token).push();
370                    }
371                }
372            }
373        }
374        Arg::IType(itype) => match arg {
375            CodeArg::Chain(chain) => {
376                validate_datatypes(chain, data, sc, dc, Datatype::CString, lang, format, false);
377            }
378            CodeArg::Literal(token) => {
379                data.verify_exists(itype, token);
380            }
381        },
382    }
383}
384
385/// Validate a datafunction chain, which is the stuff between [ ] in localization.
386/// * `chain` is the parsed datafunction structure.
387/// * `sc` is a `ScopeContext` used to evaluate scope references in the datafunctions.
388///   If nothing is known about the scope, just pass an empty `ScopeContext` with `set_strict_types(false)`.
389/// * `expect_type` is the datatype that should be returned by this chain, can be `Datatype::Unknown` in many cases.
390/// * `lang` is set to a specific language if `Custom` references in this chain only need to be defined for one language.
391///   It can just be `None` otherwise.
392/// * `format` is the formatting code given after `|` in the datatype expression. It's used for
393///   checking that game concepts in ck3 have `|E` formats.
394/// * `expect_promote` is true iff the chain is expected to end on a promote rather than on a function.
395///   Promotes and functions are very similar but they are defined separately in the datafunction tables
396///   and usually only a function can end a chain.
397#[allow(unused_variables)] // TODO HOI4: use `format`
398#[allow(clippy::too_many_arguments)] // Can't really cut anything
399pub fn validate_datatypes(
400    chain: &CodeChain,
401    data: &Everything,
402    sc: &mut ScopeContext,
403    dc: &DataContext,
404    expect_type: Datatype,
405    lang: Option<Language>,
406    format: Option<&Token>,
407    expect_promote: bool,
408) -> Datatype {
409    let mut curtype = Datatype::Unknown;
410    #[allow(unused_mut)] // imperator does not need the mut
411    let mut codes = Cow::from(&chain.codes[..]);
412    #[cfg(any(feature = "ck3", feature = "vic3"))]
413    let mut macro_count = 0;
414    // Have to loop with `while` instead of `for` because the array can mutate during the loop because of macro substitution
415    let mut i = 0;
416    let mut in_variable = false;
417    while i < codes.len() {
418        #[cfg(any(feature = "ck3", feature = "vic3"))]
419        if Game::is_ck3() || Game::is_vic3() {
420            while let Some(binding) = data.data_bindings.get(codes[i].name.as_str()) {
421                if let Some(replacement) = binding.replace(&codes[i]) {
422                    macro_count += 1;
423                    if macro_count > 255 {
424                        let msg =
425                            format!("substituted data bindings {macro_count} times, giving up");
426                        err(ErrorKey::Macro).msg(msg).loc(&codes[i].name).push();
427                        return Datatype::Unknown;
428                    }
429                    codes.to_mut().splice(i..=i, replacement.codes);
430                } else {
431                    return Datatype::Unknown;
432                }
433            }
434        }
435
436        let code = &codes[i];
437        let is_first = i == 0;
438        let is_last = i == codes.len() - 1;
439        let mut args = Args::Args(&[]);
440        let mut rtype = Datatype::Unknown;
441
442        if code.name.is("") {
443            // TODO: verify if the game engine is okay with this
444            warn(ErrorKey::Datafunctions).msg("empty fragment").loc(&code.name).push();
445            return Datatype::Unknown;
446        }
447
448        let lookup_gf = lookup_global_function(code.name.as_str());
449        let lookup_gp = lookup_global_promote(code.name.as_str());
450        let lookup_f = lookup_function(code.name.as_str(), curtype);
451        let lookup_p = lookup_promote(code.name.as_str(), curtype);
452
453        let gf_found = lookup_gf.is_some();
454        let gp_found = lookup_gp.is_some();
455        let f_found = !matches!(lookup_f, LookupResult::NotFound);
456        let p_found = !matches!(lookup_p, LookupResult::NotFound);
457
458        let mut found = false;
459
460        if is_first && is_last && !expect_promote {
461            if let Some((xargs, xrtype)) = lookup_gf {
462                found = true;
463                args = xargs;
464                rtype = xrtype;
465            }
466        } else if is_first && (!is_last || expect_promote) {
467            if let Some((xargs, xrtype)) = lookup_gp {
468                found = true;
469                args = xargs;
470                rtype = xrtype;
471            }
472        } else if !is_first && (!is_last || expect_promote) {
473            match lookup_p {
474                LookupResult::Found(xargs, xrtype) => {
475                    found = true;
476                    args = xargs;
477                    rtype = xrtype;
478                }
479                LookupResult::WrongType => {
480                    let msg = format!("{} cannot follow a {curtype} promote", code.name);
481                    warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
482                    return Datatype::Unknown;
483                }
484                LookupResult::NotFound => (),
485            }
486        } else if !is_first && is_last && !expect_promote {
487            match lookup_f {
488                LookupResult::Found(xargs, xrtype) => {
489                    found = true;
490                    args = xargs;
491                    rtype = xrtype;
492                }
493                LookupResult::WrongType => {
494                    let msg = format!("{} cannot follow a {curtype} promote", code.name);
495                    warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
496                    return Datatype::Unknown;
497                }
498                LookupResult::NotFound => (),
499            }
500        }
501
502        if Game::is_hoi4() && !found && !is_first && code.name.is("FROM") {
503            // FROM can be chained, regardless of datatype
504            found = true;
505            // TODO HOI4: this could be just the scope types.
506            rtype = Datatype::Unknown;
507        } else if Game::is_hoi4() && !found && !is_first && code.name.is("OWNER") {
508            // OWNER can be chained off of FROM
509            found = true;
510            // TODO HOI4: this could be just the scope types.
511            rtype = Datatype::Unknown;
512        }
513
514        if !found {
515            // Properly reporting these errors is tricky because `code.name`
516            // might be found in any or all of the functions and promotes tables.
517            if is_first && (p_found || f_found) && !gp_found && !gf_found {
518                let msg = format!("{} cannot be the first in a chain", code.name);
519                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
520                return Datatype::Unknown;
521            }
522            if is_last && (gp_found || p_found) && !gf_found && !f_found && !expect_promote {
523                let msg = format!("{} cannot be last in a chain", code.name);
524                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
525                return Datatype::Unknown;
526            }
527            if expect_promote && (gf_found || f_found) {
528                let msg = format!("{} cannot be used in this field", code.name);
529                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
530                return Datatype::Unknown;
531            }
532            if !is_first && (gp_found || gf_found) && !p_found && !f_found {
533                let msg = format!("{} must be the first in a chain", code.name);
534                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
535                return Datatype::Unknown;
536            }
537            if !is_last && (gf_found || f_found) && !gp_found && !p_found {
538                let msg = format!("{} must be last in the chain", code.name);
539                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
540                return Datatype::Unknown;
541            }
542            // A catch-all condition if none of the above match
543            if gp_found || gf_found || p_found || f_found {
544                let msg = format!("{} is improperly used here", code.name);
545                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
546                return Datatype::Unknown;
547            }
548        }
549
550        #[cfg(feature = "vic3")]
551        // Vic3 allows the three-letter country codes to be used unadorned as datatypes.
552        if Game::is_vic3()
553            && !found
554            && is_first
555            && data.item_exists(Item::Country, code.name.as_str())
556        {
557            found = true;
558            args = Args::Args(&[]);
559            rtype = Datatype::Vic3(Vic3Datatype::Country);
560        }
561
562        #[cfg(feature = "imperator")]
563        if Game::is_imperator()
564            && !found
565            && is_first
566            && data.item_exists(Item::Country, code.name.as_str())
567        {
568            found = true;
569            args = Args::Args(&[]);
570            rtype = Datatype::Imperator(ImperatorDatatype::Country);
571        }
572
573        // In vic3, game concepts are unadorned, like [concept_ideology]
574        // Each concept also generates a [concept_ideology_desc]
575        #[cfg(feature = "vic3")]
576        if Game::is_vic3()
577            && !found
578            && is_first
579            && is_last
580            && code.name.as_str().starts_with("concept_")
581        {
582            found = true;
583            if let Some(concept) = code.name.as_str().strip_suffix("_desc") {
584                data.verify_exists_implied(Item::GameConcept, concept, &code.name);
585            } else {
586                data.verify_exists(Item::GameConcept, &code.name);
587            }
588            args = Args::Args(&[]);
589            rtype = Datatype::CString;
590        }
591
592        // In eu5, game concepts are unadorned, like [manpower]
593        // Each concept also generates a [manpower_icon] and [manpower_with_icon]
594        #[cfg(feature = "eu5")]
595        if Game::is_eu5() && !found && is_first && is_last {
596            found = true;
597            if let Some(concept) = code.name.as_str().strip_suffix("_with_icon") {
598                data.verify_exists_implied(Item::GameConcept, concept, &code.name);
599            } else if let Some(concept) = code.name.as_str().strip_suffix("_icon") {
600                data.verify_exists_implied(Item::GameConcept, concept, &code.name);
601            } else {
602                data.verify_exists(Item::GameConcept, &code.name);
603            }
604            args = Args::Args(&[]);
605            rtype = Datatype::CString;
606        }
607
608        #[cfg(feature = "ck3")]
609        if Game::is_ck3()
610            && !found
611            && is_first
612            && is_last
613            && data.item_exists(Item::GameConcept, code.name.as_str())
614        {
615            let game_concept_formatting =
616                format.is_some_and(|fmt| fmt.as_str().contains('E') || fmt.as_str().contains('e'));
617            // In ck3, allow unadorned game concepts as long as they end with _i
618            // (which means they are just an icon). This is a heuristic.
619            // TODO: should also allow unadorned game concepts if inside another format
620            // Many strings leave out the |E from flavor text and the like.
621            // if !code.name.as_str().ends_with("_i") && !game_concept_formatting {
622            //     let msg = "game concept should have |E formatting";
623            //     warn(ErrorKey::Localization).weak().msg(msg).loc(&code.name).push();
624            // }
625
626            // If the game concept is also a passed-in scope, the game concept takes precedence.
627            // This is worth warning about.
628            // Real life example: [ROOT.Char.Custom2('RelationToMeShort', schemer)]
629            if sc.is_name_defined(code.name.as_str(), data).is_some() && !game_concept_formatting {
630                let msg = format!("`{}` is both a named scope and a game concept here", &code.name);
631                let info = format!(
632                    "The game concept will take precedence. Do `{}.Self` if you want the named scope.",
633                    &code.name
634                );
635                warn(ErrorKey::Datafunctions).msg(msg).info(info).loc(&code.name).push();
636            }
637
638            found = true;
639            args = Args::Args(&[]);
640            rtype = Datatype::CString;
641        }
642
643        if Game::is_hoi4() && !found && in_variable {
644            // The second part of a variable reference. We don't validate variable names yet.
645            in_variable = false;
646            found = true;
647            // TODO HOI4: this could be just the scope types.
648            rtype = Datatype::Unknown;
649        }
650
651        // TODO HOI4: see about disabling the scope-related logic below.
652
653        // See if it's a passed-in scope.
654        // It may still be a passed-in scope even if this check doesn't pass, because sc might be a non-strict scope
655        // where the scope names are not known. That's handled heuristically below.
656        if !found && is_first {
657            if let Some(scopes) = sc.is_name_defined(code.name.as_str(), data) {
658                found = true;
659                args = Args::Args(&[]);
660                rtype = datatype_from_scopes(scopes);
661            }
662        }
663
664        // If `code.name` is not found yet, then it can be some passed-in scope we don't know about.
665        // Unfortunately we don't have a complete list of those, so accept any id that starts
666        // with a lowercase letter or a number. This is not a foolproof check though.
667        // TODO: it's in theory possible to build a complete list of possible scope variable names
668        let first_char = code.name.as_str().chars().next().unwrap();
669        if !found
670            && is_first
671            && !sc.is_strict()
672            && (first_char.is_lowercase() || first_char.is_ascii_digit())
673        {
674            found = true;
675            args = Args::Args(&[]);
676            // TODO: this could in theory be reduced to just the scope types.
677            // That would be valuable for checks because it will find
678            // the common mistake of using .Var directly after one.
679            rtype = Datatype::Unknown;
680        }
681
682        #[cfg(feature = "hoi4")]
683        if Game::is_hoi4() && !found && is_country_tag(code.name.as_str()) {
684            found = true;
685            data.verify_exists_max_sev(Item::CountryTag, &code.name, Severity::Warning);
686            rtype = Datatype::Hoi4(Hoi4Datatype::Country);
687        }
688
689        #[cfg(feature = "hoi4")]
690        if Game::is_hoi4()
691            && !found
692            && data.item_exists(Item::ScriptedLocalisation, code.name.as_str())
693        {
694            found = true;
695            rtype = Datatype::CString;
696            if let Some((_, block)) =
697                data.get_key_block(Item::ScriptedLocalisation, code.name.as_str())
698            {
699                ScriptedLocalisation::validate_loca_call(block, data, lang);
700            }
701        }
702
703        #[cfg(feature = "hoi4")]
704        if Game::is_hoi4() && !found && code.name.starts_with("?") {
705            // It's a variable reference
706            // TODO HOI4: validate the variable reference
707            found = true;
708            // TODO HOI4: this could be just the scope types.
709            rtype = Datatype::Unknown;
710            let reference = code.name.strip_prefix("?").unwrap();
711            // Is it a two-part reference?
712            if reference.lowercase_is("global") || reference.is("FROM") || reference.is("PREV") {
713                in_variable = true;
714            } else if is_country_tag(reference.as_str()) {
715                in_variable = true;
716                data.verify_exists_max_sev(Item::CountryTag, &reference, Severity::Warning);
717            } else if reference.is_integer() {
718                // Literal numbers are allowed after `?`, and if they have a decimal part they will
719                // be split at the `.` by the loop we're in.
720                in_variable = true;
721            }
722        }
723
724        // If it's still not found, warn and exit.
725        if !found {
726            // TODO: If there is a Custom of the same name, suggest that
727            let msg = format!("unknown datafunction {}", &code.name);
728            if let Some(alternative) = lookup_alternative(code.name.as_str()) {
729                let info = format!("did you mean {alternative}?");
730                warn(ErrorKey::Datafunctions).msg(msg).info(info).loc(&code.name).push();
731            } else {
732                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
733            }
734            return Datatype::Unknown;
735        }
736
737        // This `if let` skips this check if args is `Args::Unknown`
738        if let Args::Args(a) = args {
739            if a.len() != code.arguments.len() {
740                let msg = format!(
741                    "{} takes {} arguments but was given {} here",
742                    code.name,
743                    a.len(),
744                    code.arguments.len()
745                );
746                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
747                return Datatype::Unknown;
748            }
749        }
750
751        #[cfg(feature = "jomini")]
752        // TODO: handle the case where there is a previous `datacontext = [GetScriptedGui(...)]`
753        if Game::is_jomini() && is_first {
754            let name = if code.name.is("GetScriptedGui") {
755                // Get the name from GetScriptedGui('name')
756                if let Some(CodeArg::Literal(name)) = code.arguments.first() {
757                    Some(name)
758                } else {
759                    None
760                }
761            } else if code.name.is("ScriptedGui") {
762                // Get the name from a previously declared datacontext property
763                dc.sgui_name()
764            } else {
765                None
766            };
767            if let Some(name) = name {
768                // Get the operation on the scriptedgui ScriptedGui.Execute(...) or similar.
769                if let Some(code) = codes.get(1) {
770                    if let Some((key, block, kind)) =
771                        data.get_item::<ScriptedGui>(Item::ScriptedGui, name.as_str())
772                    {
773                        kind.validate_guicall(key, block, data, sc, dc, code);
774                    }
775                }
776            }
777        }
778
779        // TODO: validate the Faith customs
780        #[cfg(feature = "ck3")]
781        if Game::is_ck3()
782            && curtype != Datatype::Ck3(Ck3Datatype::Faith)
783            && (code.name.is("Custom") && code.arguments.len() == 1)
784            || (code.name.is("Custom2") && code.arguments.len() == 2)
785        {
786            // TODO: for Custom2, get the datatype of the second argument and use it to initialize scope:second
787            if let CodeArg::Literal(ref token) = code.arguments[0] {
788                if let Some(scopes) = scope_from_datatype(curtype) {
789                    validate_custom(token, data, scopes, lang);
790                } else if (curtype == Datatype::Unknown
791                    || curtype == Datatype::AnyScope
792                    || curtype == Datatype::TopScope)
793                    && !CUSTOM_RELIGION_LOCAS.contains(&token.as_str())
794                {
795                    // TODO: is a TopScope even valid to pass to .Custom? verify
796                    validate_custom(token, data, Scopes::all(), lang);
797                }
798            }
799        }
800
801        #[cfg(feature = "vic3")]
802        if Game::is_vic3() && code.name.is("GetCustom") && code.arguments.len() == 1 {
803            if let CodeArg::Literal(ref token) = code.arguments[0] {
804                if let Some(scopes) = scope_from_datatype(curtype) {
805                    validate_custom(token, data, scopes, lang);
806                } else if curtype == Datatype::Unknown
807                    || curtype == Datatype::AnyScope
808                    || curtype == Datatype::TopScope
809                {
810                    // TODO: is a TopScope even valid to pass to .GetCustom? verify
811                    validate_custom(token, data, Scopes::all(), lang);
812                }
813            }
814        }
815
816        #[cfg(feature = "imperator")]
817        if Game::is_imperator() && code.name.is("Custom") && code.arguments.len() == 1 {
818            if let CodeArg::Literal(ref token) = code.arguments[0] {
819                if let Some(scopes) = scope_from_datatype(curtype) {
820                    validate_custom(token, data, scopes, lang);
821                } else if curtype == Datatype::Unknown
822                    || curtype == Datatype::AnyScope
823                    || curtype == Datatype::TopScope
824                {
825                    // TODO: is a TopScope even valid to pass to .Custom? verify
826                    validate_custom(token, data, Scopes::all(), lang);
827                }
828            }
829        }
830
831        // TODO: handle GetDefineAtIndex too. No examples in vanilla.
832        #[cfg(feature = "jomini")]
833        if code.name.is("GetDefine") && code.arguments.len() == 2 {
834            if let CodeArg::Literal(ref token1) = code.arguments[0] {
835                if let CodeArg::Literal(ref token2) = code.arguments[1] {
836                    let key = format!("{token1}|{token2}");
837                    if data.defines.get_bv(&key).is_none() {
838                        let msg = format!("{key} not defined in common/defines/");
839                        err(ErrorKey::MissingItem).msg(msg).loc(token2).push();
840                    }
841                }
842            }
843        }
844
845        // TODO: vic3 docs say that `Localize` can take a `CustomLocalization` as well
846        if code.name.is("Localize") && code.arguments.len() == 1 {
847            if let CodeArg::Literal(ref token) = code.arguments[0] {
848                // The is_ascii check is to weed out some localizations (looking at you, Russian)
849                // that do a lot of Localize on already localized strings. There's no reason for
850                // it, but I guess it makes them happy.
851                if token.as_str().is_ascii() {
852                    data.localization.verify_exists_lang(token, lang);
853                }
854            }
855        }
856
857        #[cfg(feature = "jomini")]
858        if let Args::Args(a) = args {
859            for (i, arg) in a.iter().enumerate() {
860                // Handle |E that contain a SelectLocalization that chooses between two gameconcepts
861                if Game::is_jomini() && code.name.is("SelectLocalization") && i > 0 {
862                    if let CodeArg::Chain(chain) = &code.arguments[i] {
863                        if chain.codes.len() == 1
864                            && chain.codes[0].arguments.is_empty()
865                            && data.item_exists(Item::GameConcept, chain.codes[0].name.as_str())
866                        {
867                            continue;
868                        }
869                    }
870                }
871                validate_argument(&code.arguments[i], data, sc, dc, *arg, lang, format);
872            }
873        }
874
875        curtype = rtype;
876
877        if is_last
878            && curtype != Datatype::Unknown
879            && expect_type != Datatype::Unknown
880            && curtype != expect_type
881        {
882            if expect_type == Datatype::AnyScope {
883                if scope_from_datatype(curtype).is_none() {
884                    let msg =
885                        format!("{} returns {curtype} but a scope type is needed here", code.name);
886                    warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
887                    return Datatype::Unknown;
888                }
889            } else {
890                let msg =
891                    format!("{} returns {curtype} but a {expect_type} is needed here", code.name);
892                warn(ErrorKey::Datafunctions).msg(msg).loc(&code.name).push();
893                return Datatype::Unknown;
894            }
895        }
896
897        i += 1;
898    }
899    curtype
900}
901
902fn lookup_global_promote(lookup_name: &str) -> Option<(Args, Datatype)> {
903    let global_promotes_map = match Game::game() {
904        #[cfg(feature = "ck3")]
905        Game::Ck3 => &crate::ck3::tables::datafunctions::GLOBAL_PROMOTES_MAP,
906        #[cfg(feature = "vic3")]
907        Game::Vic3 => &crate::vic3::tables::datafunctions::GLOBAL_PROMOTES_MAP,
908        #[cfg(feature = "imperator")]
909        Game::Imperator => &crate::imperator::tables::datafunctions::GLOBAL_PROMOTES_MAP,
910        #[cfg(feature = "eu5")]
911        Game::Eu5 => &crate::eu5::tables::datafunctions::GLOBAL_PROMOTES_MAP,
912        #[cfg(feature = "hoi4")]
913        Game::Hoi4 => &crate::hoi4::tables::datafunctions::GLOBAL_PROMOTES_MAP,
914    };
915
916    if let result @ Some(_) = global_promotes_map.get(lookup_name).copied() {
917        return result;
918    }
919
920    // Datatypes can be used directly as global promotes, taking their value from the gui context.
921    if let Ok(dtype) = Datatype::from_str(lookup_name) {
922        return Some((Args::Args(&[]), dtype));
923    }
924
925    None
926}
927
928fn lookup_global_function(lookup_name: &str) -> Option<(Args, Datatype)> {
929    let global_functions_map = match Game::game() {
930        #[cfg(feature = "ck3")]
931        Game::Ck3 => &crate::ck3::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
932        #[cfg(feature = "vic3")]
933        Game::Vic3 => &crate::vic3::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
934        #[cfg(feature = "imperator")]
935        Game::Imperator => &crate::imperator::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
936        #[cfg(feature = "eu5")]
937        Game::Eu5 => &crate::eu5::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
938        #[cfg(feature = "hoi4")]
939        Game::Hoi4 => &crate::hoi4::tables::datafunctions::GLOBAL_FUNCTIONS_MAP,
940    };
941    global_functions_map.get(lookup_name).copied()
942}
943
944fn lookup_promote_or_function(ltype: Datatype, vec: &[(Datatype, Args, Datatype)]) -> LookupResult {
945    let mut possible_args = None;
946    let mut possible_rtype = None;
947
948    for (intype, args, rtype) in vec.iter().copied() {
949        if ltype == Datatype::Unknown {
950            if possible_rtype.is_none() {
951                possible_args = Some(args);
952                possible_rtype = Some(rtype);
953            } else {
954                if possible_rtype != Some(rtype) {
955                    possible_rtype = Some(Datatype::Unknown);
956                }
957                if possible_args != Some(args) {
958                    possible_args = Some(Args::Unknown);
959                }
960            }
961        } else if ltype == intype {
962            return LookupResult::Found(args, rtype);
963        }
964    }
965
966    if ltype == Datatype::Unknown {
967        LookupResult::Found(possible_args.unwrap(), possible_rtype.unwrap())
968    } else {
969        // If it was the right type, it would already have been returned as `Found`, above.
970        LookupResult::WrongType
971    }
972}
973
974fn lookup_promote(lookup_name: &str, ltype: Datatype) -> LookupResult {
975    let promotes_map = match Game::game() {
976        #[cfg(feature = "ck3")]
977        Game::Ck3 => &crate::ck3::tables::datafunctions::PROMOTES_MAP,
978        #[cfg(feature = "vic3")]
979        Game::Vic3 => &crate::vic3::tables::datafunctions::PROMOTES_MAP,
980        #[cfg(feature = "imperator")]
981        Game::Imperator => &crate::imperator::tables::datafunctions::PROMOTES_MAP,
982        #[cfg(feature = "eu5")]
983        Game::Eu5 => &crate::eu5::tables::datafunctions::PROMOTES_MAP,
984        #[cfg(feature = "hoi4")]
985        Game::Hoi4 => &crate::hoi4::tables::datafunctions::PROMOTES_MAP,
986    };
987
988    promotes_map
989        .get(lookup_name)
990        .map_or(LookupResult::NotFound, |x| lookup_promote_or_function(ltype, x))
991}
992
993fn lookup_function(lookup_name: &str, ltype: Datatype) -> LookupResult {
994    let functions_map = match Game::game() {
995        #[cfg(feature = "ck3")]
996        Game::Ck3 => &crate::ck3::tables::datafunctions::FUNCTIONS_MAP,
997        #[cfg(feature = "vic3")]
998        Game::Vic3 => &crate::vic3::tables::datafunctions::FUNCTIONS_MAP,
999        #[cfg(feature = "imperator")]
1000        Game::Imperator => &crate::imperator::tables::datafunctions::FUNCTIONS_MAP,
1001        #[cfg(feature = "eu5")]
1002        Game::Eu5 => &crate::eu5::tables::datafunctions::FUNCTIONS_MAP,
1003        #[cfg(feature = "hoi4")]
1004        Game::Hoi4 => &crate::hoi4::tables::datafunctions::FUNCTIONS_MAP,
1005    };
1006
1007    functions_map
1008        .get(lookup_name)
1009        .map_or(LookupResult::NotFound, |x| lookup_promote_or_function(ltype, x))
1010}
1011
1012pub struct CaseInsensitiveStr(pub(crate) &'static str);
1013
1014impl PartialEq for CaseInsensitiveStr {
1015    fn eq(&self, other: &Self) -> bool {
1016        self.0.eq_ignore_ascii_case(other.0)
1017    }
1018}
1019
1020impl Eq for CaseInsensitiveStr {}
1021
1022impl std::hash::Hash for CaseInsensitiveStr {
1023    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1024        self.0.to_ascii_lowercase().hash(state);
1025    }
1026}
1027
1028/// Find an alternative datafunction to suggest when `lookup_name` has not been found.
1029/// This is a fairly expensive lookup.
1030/// Currently it only looks for different-case variants.
1031// TODO: make it consider misspellings as well
1032fn lookup_alternative(lookup_name: &'static str) -> Option<&'static str> {
1033    let lowercase_datatype_set = match Game::game() {
1034        #[cfg(feature = "ck3")]
1035        Game::Ck3 => &crate::ck3::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1036        #[cfg(feature = "vic3")]
1037        Game::Vic3 => &crate::vic3::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1038        #[cfg(feature = "imperator")]
1039        Game::Imperator => &crate::imperator::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1040        #[cfg(feature = "eu5")]
1041        Game::Eu5 => &crate::eu5::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1042        #[cfg(feature = "hoi4")]
1043        Game::Hoi4 => &crate::hoi4::tables::datafunctions::LOWERCASE_DATATYPE_SET,
1044    };
1045
1046    lowercase_datatype_set.get(&CaseInsensitiveStr(lookup_name)).map(|x| x.0)
1047}
1048
1049fn datatype_and_scope_map() -> &'static LazyLock<BiTigerHashMap<Datatype, Scopes>> {
1050    match Game::game() {
1051        #[cfg(feature = "ck3")]
1052        Game::Ck3 => &crate::ck3::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1053        #[cfg(feature = "vic3")]
1054        Game::Vic3 => &crate::vic3::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1055        #[cfg(feature = "imperator")]
1056        Game::Imperator => &crate::imperator::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1057        #[cfg(feature = "eu5")]
1058        Game::Eu5 => &crate::eu5::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1059        #[cfg(feature = "hoi4")]
1060        Game::Hoi4 => &crate::hoi4::tables::datafunctions::DATATYPE_AND_SCOPE_MAP,
1061    }
1062}
1063
1064/// Return the scope type that best matches `dtype`, or `None` if there is no match.
1065/// Nearly every scope type has a matching datatype, but there are far more datatypes than scope types.
1066pub fn scope_from_datatype(dtype: Datatype) -> Option<Scopes> {
1067    datatype_and_scope_map().get_by_left(&dtype).copied()
1068}
1069
1070/// Return the datatype that best matches `scopes`, or `Datatype::Unknown` if there is no match.
1071/// Nearly every scope type has a matching datatype, but there are far more datatypes than scope types.
1072/// Note that only `Scopes` values that are narrowed down to a single scope type can be matched.
1073fn datatype_from_scopes(scopes: Scopes) -> Datatype {
1074    datatype_and_scope_map().get_by_right(&scopes).copied().unwrap_or(Datatype::Unknown)
1075}