Skip to main content

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