tiger-lib 1.17.0

Library used by the tools ck3-tiger, vic3-tiger, and imperator-tiger. This library holds the bulk of the code for them. It can be built either for ck3-tiger with the feature ck3, or for vic3-tiger with the feature vic3, or for imperator-tiger with the feature imperator, but not both at the same time.
Documentation
use lalrpop_util::ParseError;

use crate::block::{Block, BV, BlockItem, Comparator, Eq, Field};
use crate::parse::pdxfile::lexer::{Directive, Lexeme, LexError};
use crate::parse::pdxfile::memory::CombinedMemory;
use crate::parse::pdxfile::{HasMacroParams, define_var, warn_macros, split_macros, report_error, get_numeric_var};
use crate::report::{err, warn, ErrorKey};
use crate::token::Token;

grammar<'memory>(inputs: &[Token], memory: &mut CombinedMemory<'memory>);

/// A file is parsed as an undelimited [`Block`].
pub File: Block = {
    <FileContents>,
}

pub Block: (Block, HasMacroParams) = {
    // The normal case
    <loc:"{"> <mut block:BlockContents> "}" => {
        block.0.loc = loc.get_loc();
        block
    },
    // Error handling: an unterminated field
    "{" <block:BlockContents> Key <cmp:cmp> "}" => {
        let msg = format!("expected block or value after {cmp}");
        err(ErrorKey::ParseError).msg(msg).loc(cmp.get_loc()).push();
        block
    },
    // Error handling: an unterminated @var definition
    "{" <block:BlockContents> var <cmp:cmp> "}" => {
        let msg = format!("expected value after {cmp}");
        err(ErrorKey::ParseError).msg(msg).loc(cmp.get_loc()).push();
        block
    },
    // Error handling: generic
    <loc:"{"> <mut block:BlockContents> <error:!> => {
        if let ParseError::UnrecognizedEof { .. } = error.error {
            let msg = "opening { was never closed";
            err(ErrorKey::BraceError).msg(msg).loc(loc.get_loc()).push();
        } else {
            report_error(error.error, inputs[0].loc);
        }
        block.0.loc = loc.get_loc();
        block
    },
}

/// Zero-or-more `FileItem`s form the top-level block.
FileContents: Block = {
    // Start empty
    => Block::new(inputs[0].loc),
    // Add an item
    <mut block:FileContents> <opt_item:FileItem> => {
        if let Some(blockitem) = opt_item {
            block.add_item_check_tag(blockitem);
        }
        block
    },
    <mut block:FileContents> <items:MacroInsert> => {
        for blockitem in items {
            block.add_item(blockitem);
        }
        block
    },
    // Error handling
    <block:FileContents> <error:!> => {
        report_error(error.error, inputs[0].loc);
        block
    },
}

/// A block contains zero or more `BlockItem`s
BlockContents: (Block, HasMacroParams) = {
    // Start empty
    => (Block::new(inputs[0].loc), false), // dummy loc, replaced later
    // Add an item. Macro handling is deferred to the top level block.
    <mut block:BlockContents> <opt_item:BlockItem> => {
        if let Some((blockitem, has_macro_params)) = opt_item {
            block.0.add_item_check_tag(blockitem);
            (block.0, block.1 || has_macro_params)
        } else {
            block
        }
    },
    <mut block:BlockContents> <items:MacroInsert> => {
        for blockitem in items {
            block.0.add_item(blockitem);
        }
        block
    },
}

/// A top-level `BlockItem`. Blocks with macro parameters are handled here.
FileItem: Option<BlockItem> = {
    // key = value assignment
    <key:Key> <cmp:cmp> <value:Value> => {
        warn_macros(&key.0, key.1);
        warn_macros(&value.0, value.1);
        Some(BlockItem::Field(Field(key.0, cmp.get_cmp(), BV::Value(value.0))))
    },
    // key = { block } definition
    <key:Key> <cmp:cmp> <start:@L> <mut block:Block> <end:@R> => {
        warn_macros(&key.0, key.1);
        if block.1 && inputs.len() == 1 {
            // Handle a block with macro parameters
            let s = &inputs[0].as_str()[start + 1..end - 1];
            let mut loc = block.0.loc;
            loc.column += 1;
            let token = Token::from_static_str(s, loc);
            block.0.source = Some(Box::new((split_macros(&token), memory.get_local())));
        }
        Some(BlockItem::Field(Field(key.0, cmp.get_cmp(), BV::Block(block.0))))
    },
    // loose value
    <value:Value> => {
        warn_macros(&value.0, value.1);
        Some(BlockItem::Value(value.0))
    },
    // loose block
    <start:@L> <mut block:Block> <end:@R> => {
        if block.1 && inputs.len() == 1 {
            // Handle a block with macro parameters
            let s = &inputs[0].as_str()[start + 1..end - 1];
            let mut loc = block.0.loc;
            loc.column += 1;
            let token = Token::from_static_str(s, loc);
            block.0.source = Some(Box::new((split_macros(&token), memory.get_local())));
        }
        Some(BlockItem::Block(block.0))
    },
    VariableDefinition => None,
    "@:log" token => None,
    MacroDefinition => None,
}

/// A single [`BlockItem`]. Macro parameter handling is deferred to the top level.
BlockItem: Option<(BlockItem, HasMacroParams)> = {
    // key = value assignment
    <key:Key> <cmp:cmp> <value:Value> => {
        Some((BlockItem::Field(Field(key.0, cmp.get_cmp(), BV::Value(value.0))), key.1 || value.1))
    },
    // key = { block } definition
    <key:Key> <cmp:cmp> <block:Block> => {
        Some((BlockItem::Field(Field(key.0, cmp.get_cmp(), BV::Block(block.0))), key.1 || block.1))
    },
    // Error handling: key = =
    <key:Key> <cmp1:cmp> <cmp2:cmp> => {
        let cmp1 = cmp1.get_cmp();
        if cmp1 == Comparator::Equals(Eq::Single) {
            // Special handling to allow `{ OPERATOR = >= }` in macro arguments.
            let value = cmp2.into_token();
            Some((BlockItem::Field(Field(key.0, cmp1, BV::Value(value))), key.1))
        } else {
            let msg = format!("double comparator `{}`", cmp2.get_cmp());
            err(ErrorKey::ParseError).msg(msg).loc(cmp2.into_token()).push();
            None
        }
    },
    // loose value
    <value:Value> => Some((BlockItem::Value(value.0), value.1)),
    // loose block
    <block:Block> => Some((BlockItem::Block(block.0), block.1)),
    VariableDefinition => None,
    "@:log" token => None,
    MacroDefinition => None,
}

/// The right-hand side of assignments, or loose values.
Value: (Token, HasMacroParams) = {
    <token:token> => (token.into_token(), false),
    <var:var> => {
        let token = var.into_token();
        // SAFETY: The @ is guaranteed by the lexer
        let name = token.as_str().strip_prefix('@').unwrap();
        if name.contains('!') {
            // Check for a `!` to avoid looking up macros in gui code that uses `@icon!` syntax.
            (token, false)
        } else if let Some(t) = memory.get_variable(name) {
            // TODO: use loc.link here and in @:load_variable?
            (Token::from_static_str(t.as_str(), token.loc), false)
        } else {
            let msg = format!("reader variable {name} not defined");
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&token).push();
            (token, false)
        }
    },
    <directive:"@:load_variable"> <name:token> => {
        let name = name.into_token();
        if let Some(t) = memory.get_variable(name.as_str()) {
            (Token::from_static_str(t.as_str(), directive.get_loc()), false)
        } else {
            let msg = format!("reader variable {name} not defined");
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
            (name, false)
        }
    },
    <token:Calculation> => (token, false),
    <param:param> => (param.into_token(), true),
}

/// The left-hand side of assignments.
/// @var reader variables are not handled here;
/// they get a separate alternative in `BlockItem` or `FileItem`.
Key: (Token, HasMacroParams) = {
    <token:token> => (token.into_token(), false),
    <token:Calculation> => (token, false),
    <param:param> => (param.into_token(), true),
    // Edge case: using `@:load_variable` to name the key
    <directive:"@:load_variable"> <name:token> => {
        let name = name.into_token();
        if let Some(t) = memory.get_variable(name.as_str()) {
            (Token::from_static_str(t.as_str(), directive.get_loc()), false)
        } else {
            let msg = format!("reader variable {name} not defined");
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
            (name, false)
        }
    },
}

/// Definition of a reader variable, either with the `@name` syntax or `@:register_variable`.
VariableDefinition: () = {
    // @variable = value definition
    <var:var> <cmp:cmp> <value:Value> => {
        if value.1 {
            let msg = "cannot use $-substitutions when defining reader variables";
            err(ErrorKey::Macro).msg(msg).loc(value.0).push();
        } else {
            define_var(memory, &var.into_token(), cmp.get_cmp(), value.0);
        }
    },
    // same as above but using @:register_variable
    "@:register_variable" <name:token> <cmp:cmp> <value:Value> => {
        if value.1 {
            let msg = "cannot use $-substitutions when defining reader variables";
            err(ErrorKey::Macro).msg(msg).loc(value.0).push();
        } else {
            define_var(memory, &name.into_token(), cmp.get_cmp(), value.0);
        }
    },
}

MacroDefinition: () = {
    "@:define" <name:token> <cmp:cmp> <start:@L> <mut block:Block> <end:@R> => {
        let name = name.into_token();
        if !block.1 {
            let msg = "reader macros without $ parameters can't be inserted (as of CK3 1.13)";
            err(ErrorKey::Bugs).msg(msg).loc(&name).push();
        } else {
            let s = &inputs[0].as_str()[start + 1..end - 1];
            let mut loc = block.0.loc;
            loc.column += 1;
            let token = Token::from_static_str(s, loc);
            block.0.source = Some(Box::new((split_macros(&token), memory.get_local())));
        }
        if !matches!(cmp.get_cmp(), Comparator::Equals(Eq::Single)) {
            let msg = "expected `=`";
            err(ErrorKey::ReaderDirectives).msg(msg).loc(cmp.into_token()).push();
        }
        if memory.has_block(name.as_str()) {
            let msg = format!("`{name}` is already defined");
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
        } else if !name.as_str().starts_with(|c: char| c.is_ascii_alphabetic()) {
            let msg = "macro names must start with an ascii letter";
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
        } else if !name.as_str().chars().all(|c: char| c.is_alphanumeric() || c == '_') {
            let msg = "macro names must be alphanumeric or `_`";
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
        } else {
            memory.define_block(name.to_string(), block.0);
        }
    }
}

MacroInsert: Vec<BlockItem> = {
    "@:insert" <name:token> <cmp:cmp> <block:Block> => {
        let name = name.into_token();
        if !matches!(cmp.get_cmp(), Comparator::Equals(Eq::Single)) {
            let msg = format!("expected `{name} =`");
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
        }
        if let Some(defined) = memory.get_block(name.as_str()) {
            let parms = defined.macro_parms();
            if parms.is_empty() {
                let msg = "reader macros without $ parameters can't be inserted (as of CK3 1.13)";
                err(ErrorKey::Bugs).msg(msg).loc(&name).push();
                return Vec::new();
            }
            // parameter errors are reported with `err` because the game parser gets confused.
            let mut args: Vec<(&str, Token)> = Vec::new();
            for (parm, arg) in block.0.iter_assignments() {
                if args.iter().any(|(p, _)| parm.is(p)) {
                    let msg = format!("duplicate macro argument `{parm}`");
                    err(ErrorKey::ReaderDirectives).msg(msg).loc(parm).push();
                } else if !parms.contains(&parm.as_str()) {
                    let msg = format!("this macro does not need parameter `{parm}`");
                    err(ErrorKey::ReaderDirectives).msg(msg).loc(parm).push();
                } else {
                    args.push((parm.as_str(), arg.clone()));
                }
            }
            for parm in parms {
                if !args.iter().any(|(p, _)| &parm == p) {
                    let msg = format!("this macro needs argument `{parm}`");
                    err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
                    return Vec::new();
                }
            }
            // SAFETY: we already checked that this block has macro parameters
            let mut expansion = defined.expand_macro(&args, name.loc, memory.as_global()).unwrap();
            expansion.drain().collect()
        } else {
            let msg = "reader macro `{name}` is not defined";
            err(ErrorKey::ReaderDirectives).msg(msg).loc(&name).push();
            Vec::new()
        }
    },
    "@:insert" <name:token> => {
        let name = name.into_token();
        let msg = "reader macros without $ parameters can't be inserted (as of CK3 1.13)";
        err(ErrorKey::Bugs).msg(msg).loc(&name).push();
        Vec::new()
    },
}

/// A `@[ ... ]` parse-time calculation.
Calculation: Token = {
    <start:"@["> <expr:Expr> "]" => Token::new(&expr.to_string(), start.get_loc()),
}

/// Precedence 0: addition and subtraction.
Expr: f64 = {
    <x:Expr> "+" <y:Factor> => x + y,
    <x:Expr> "-" <y:Factor> => x - y,
    <Factor>,
}

/// Precedence 1: multiplication and division.
Factor: f64 = {
    <x:Factor> "*" <y:Term> => x * y,
    <x:Factor> <div:"/"> <y:Term> => {
        if y == 0.0 {
            let msg = "dividing by zero";
            err(ErrorKey::ReaderDirectives).msg(msg).loc(div.get_loc()).push();
            0.0
        } else {
            x / y
        }
    },
    <Term>,
}

/// Precedence 2: unary negation or just a value
Term: f64 = {
    // The token can be the name of a reader variable (without an @) or a literal number.
    <token:token> => get_numeric_var(memory, &token.into_token()),
    // Unary negation
    "-" <token:token> => -get_numeric_var(memory, &token.into_token()),
    // Nested expression
    "(" <Expr> ")",
}

extern {
    type Location = usize;
    type Error = LexError;

    enum Lexeme {
        token => Lexeme::General(_),
        cmp => Lexeme::Comparator(_, _),
        var => Lexeme::VariableReference(_),
        param => Lexeme::MacroParam(_),
        "{" => Lexeme::BlockStart(_),
        "}" => Lexeme::BlockEnd(_),
        "@[" => Lexeme::CalcStart(_),
        "]" => Lexeme::CalcEnd(_),
        "(" => Lexeme::OpenParen(_),
        ")" => Lexeme::CloseParen(_),
        "+" => Lexeme::Add(_),
        "-" => Lexeme::Subtract(_),
        "*" => Lexeme::Multiply(_),
        "/" => Lexeme::Divide(_),
        "@:register_variable" => Lexeme::Directive(Directive::RegisterVariable, _),
        "@:load_variable" => Lexeme::Directive(Directive::LoadVariable, _),
        "@:define" => Lexeme::Directive(Directive::Define, _),
        "@:insert" => Lexeme::Directive(Directive::Insert, _),
        "@:log" => Lexeme::Directive(Directive::Log, _),
    }
}