From 2ef9df2329bb729c5085ec74d344634bd600573e Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Sun, 2 Aug 2026 13:46:25 -0500 Subject: [PATCH] sqf: Add optimization to bypass single use vars --- libs/sqf/src/compiler/optimizer/mod.rs | 4 +- libs/sqf/src/compiler/optimizer/statements.rs | 179 ++++++++++++++++++ libs/sqf/tests/optimizer.rs | 3 + libs/sqf/tests/optimizer/statement_1.sqf | 10 + libs/sqf/tests/optimizer/statement_2.sqf | 7 + .../optimizer__simple_statement_1.snap | 92 +++++++++ .../optimizer__simple_statement_2.snap | 81 ++++++++ 7 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 libs/sqf/src/compiler/optimizer/statements.rs create mode 100644 libs/sqf/tests/optimizer/statement_1.sqf create mode 100644 libs/sqf/tests/optimizer/statement_2.sqf create mode 100644 libs/sqf/tests/snapshots/optimizer__simple_statement_1.snap create mode 100644 libs/sqf/tests/snapshots/optimizer__simple_statement_2.snap diff --git a/libs/sqf/src/compiler/optimizer/mod.rs b/libs/sqf/src/compiler/optimizer/mod.rs index 2d03896b..64bd01aa 100644 --- a/libs/sqf/src/compiler/optimizer/mod.rs +++ b/libs/sqf/src/compiler/optimizer/mod.rs @@ -6,12 +6,14 @@ use std::{ops::Range, sync::Arc}; #[allow(unused_imports)] use tracing::{trace, warn}; +mod statements; + impl Statements { /// optimize Statements #[must_use] pub fn optimize(mut self) -> Self { self.content = self.content.into_iter().map(Statement::optimize).collect(); - self + self.reduce_vars() } } diff --git a/libs/sqf/src/compiler/optimizer/statements.rs b/libs/sqf/src/compiler/optimizer/statements.rs new file mode 100644 index 00000000..26c65d26 --- /dev/null +++ b/libs/sqf/src/compiler/optimizer/statements.rs @@ -0,0 +1,179 @@ +use std::sync::OnceLock; + +use crate::{Expression, Statement, Statements}; +#[allow(unused_imports)] +use tracing::{trace, warn}; + +static CALL_COMMANDS: OnceLock> = OnceLock::new(); + +#[must_use] +fn code_call_commands() -> &'static [String] { + CALL_COMMANDS.get_or_init(|| { + let mut commands = vec![ + "configclasses".to_lowercase(), // these take strings which are evaluated as code + "configproperties".to_lowercase(), + "isnil".to_lowercase(), // these are missing wiki params info + "then".to_lowercase(), + "foreach".to_lowercase(), + "foreachreversed".to_lowercase(), + "switch".to_lowercase(), + ]; + let database = crate::parser::database::Database::a3(false); // todo: pass this in + for (name, command) in database.wiki().commands().iter() { + if command.syntax().iter().any(|s| { + s.params() + .iter() + .any(|p| *p.typ() == arma3_wiki::model::Value::Code) + }) { + commands.push(name.to_lowercase()); + } + } + commands + }) +} + +impl Statements { + #[must_use] + pub fn reduce_vars(mut self) -> Self { + #[must_use] + fn is_target_var(expression: &Expression, t_var: &str) -> bool { + matches!(expression, Expression::Variable(e_var, _) if e_var.eq_ignore_ascii_case(t_var)) + } + #[must_use] + fn get_replacment_statment(original: &Statement, new_expression: Expression) -> Statement { + match original { + Statement::AssignGlobal(var, _, range) => { + Statement::AssignGlobal(var.clone(), new_expression, range.clone()) + } + Statement::AssignLocal(var, _, range) => { + Statement::AssignLocal(var.clone(), new_expression, range.clone()) + } + Statement::Expression(_, range) => { + Statement::Expression(new_expression, range.clone()) + } + } + } + #[must_use] + fn check_expression(expression: &Expression, vars_used: &mut Vec) -> bool { + #[must_use] + fn is_code_call_command(command: &str) -> bool { + code_call_commands() + .iter() + .any(|cmd| cmd.eq_ignore_ascii_case(command)) + } + match expression { + Expression::Code(..) + | Expression::Boolean(..) + | Expression::Number(..) + | Expression::String(..) => true, + Expression::Variable(var, _) => { + vars_used.push(var.to_lowercase()); + true + } + Expression::NularCommand(n_cmd, _) => !is_code_call_command(n_cmd.as_str()), + Expression::UnaryCommand(u_cmd, rhs, _) => { + check_expression(rhs, vars_used) && !is_code_call_command(u_cmd.as_str()) + } + Expression::BinaryCommand(b_cmd, lhs, rhs, _) => { + check_expression(lhs, vars_used) + && check_expression(rhs, vars_used) + && !is_code_call_command(b_cmd.as_str()) + } + Expression::Array(vec, _) | Expression::ConsumeableArray(vec, _) => { + vec.iter().all(|e| check_expression(e, vars_used)) + } + } + } + + let mut index = self.content.len().saturating_sub(1); + let mut vars_used: Vec = Vec::new(); + + // reverse order, stopping before index 0 because we access [index -1] + while index > 0 { + let this_statement = &self.content[index]; + let (Statement::AssignGlobal(_, cur_exp, _) + | Statement::AssignLocal(_, cur_exp, _) + | Statement::Expression(cur_exp, _)) = this_statement; + + if !check_expression(cur_exp, &mut vars_used) { + println!( + "{index}: stopping optimization because expression {:?} is not safe", + cur_exp.command_name() + ); + break; + } + + // See if statement above this one is a `private _var` assignment + let Statement::AssignLocal(var, above_exp, above_range) = &self.content[index - 1] + else { + index -= 1; + continue; + }; + let target_var = var.to_lowercase(); + + if vars_used.iter().filter(|v| *v == &target_var).count() != 1 { + println!("{index}: skipping because {var} is used more than once bellow"); + index -= 1; + continue; + } + + let replacement = match cur_exp { + Expression::Variable(_, _) => { + if is_target_var(cur_exp, &target_var) { + Some(Statement::Expression( + above_exp.clone(), + above_range.clone(), + )) + } else { + None + } + } + Expression::UnaryCommand(u_cmd, rhs, source) => { + if is_target_var(rhs, &target_var) { + Some(get_replacment_statment( + this_statement, + Expression::UnaryCommand( + u_cmd.clone(), + Box::new(above_exp.clone()), + source.clone(), + ), + )) + } else { + None + } + } + Expression::BinaryCommand(b_cmd, lhs, rhs, source) => { + if is_target_var(lhs, &target_var) { + Some(get_replacment_statment( + this_statement, + Expression::BinaryCommand( + b_cmd.clone(), + Box::new(above_exp.clone()), + rhs.clone(), + source.clone(), + ), + )) + } else { + None + } + } + _ => None, + }; + + if let Some(replacement) = replacement { + println!( + "{index}: optimizing {:?} to bypass {target_var}", + cur_exp.command_name() + ); + trace!( + "{index}: optimizing {:?} to bypass {target_var}", + cur_exp.command_name() + ); + self.content.remove(index); + self.content[index - 1] = replacement; + } + index -= 1; + } + self + } +} diff --git a/libs/sqf/tests/optimizer.rs b/libs/sqf/tests/optimizer.rs index 6ffb630f..0921103a 100644 --- a/libs/sqf/tests/optimizer.rs +++ b/libs/sqf/tests/optimizer.rs @@ -23,6 +23,9 @@ optimize!(string_case); optimize!(chain); optimize!(to_string); +optimize!(statement_1); +optimize!(statement_2); + const ROOT: &str = "tests/optimizer/"; fn optimize(file: &str) -> Statements { diff --git a/libs/sqf/tests/optimizer/statement_1.sqf b/libs/sqf/tests/optimizer/statement_1.sqf new file mode 100644 index 00000000..6611af23 --- /dev/null +++ b/libs/sqf/tests/optimizer/statement_1.sqf @@ -0,0 +1,10 @@ +private _a = 1; +x = _a + _a; // skip + +private _b = 2; +y = _b + 1; // reduce + +private _c = 3; +z = _c + 1; // skip + +_c diff --git a/libs/sqf/tests/optimizer/statement_2.sqf b/libs/sqf/tests/optimizer/statement_2.sqf new file mode 100644 index 00000000..9896b7ce --- /dev/null +++ b/libs/sqf/tests/optimizer/statement_2.sqf @@ -0,0 +1,7 @@ +private _time = diag_tickTime; +start = _time + 1; // skip because of call + +[] call unknown; + +private _pos = getPos player; +_pos vectorAdd offset // reduce diff --git a/libs/sqf/tests/snapshots/optimizer__simple_statement_1.snap b/libs/sqf/tests/snapshots/optimizer__simple_statement_1.snap new file mode 100644 index 00000000..c5f770ce --- /dev/null +++ b/libs/sqf/tests/snapshots/optimizer__simple_statement_1.snap @@ -0,0 +1,92 @@ +--- +source: libs/sqf/tests/optimizer.rs +expression: optimize(stringify! (statement_1)) +--- +Statements { + content: [ + AssignLocal( + "_a", + Number( + FloatOrd( + 1.0, + ), + 13..14, + ), + 0..14, + ), + AssignGlobal( + "x", + BinaryCommand( + Add, + Variable( + "_a", + 20..22, + ), + Variable( + "_a", + 25..27, + ), + 23..24, + ), + 16..27, + ), + AssignGlobal( + "y", + BinaryCommand( + Add, + Number( + FloatOrd( + 2.0, + ), + 44..45, + ), + Number( + FloatOrd( + 1.0, + ), + 56..57, + ), + 54..55, + ), + 47..57, + ), + AssignLocal( + "_c", + Number( + FloatOrd( + 3.0, + ), + 74..75, + ), + 61..75, + ), + AssignGlobal( + "z", + BinaryCommand( + Add, + Variable( + "_c", + 81..83, + ), + Number( + FloatOrd( + 1.0, + ), + 86..87, + ), + 84..85, + ), + 77..87, + ), + Expression( + Variable( + "_c", + 91..93, + ), + 91..93, + ), + ], + source: "private _a = 1;\nx = _a + _a; \n\nprivate _b = 2;\ny = _b + 1; \n\nprivate _c = 3;\nz = _c + 1; \n\n_c\n", + span: 0..93, + issues: [], +} diff --git a/libs/sqf/tests/snapshots/optimizer__simple_statement_2.snap b/libs/sqf/tests/snapshots/optimizer__simple_statement_2.snap new file mode 100644 index 00000000..e2812998 --- /dev/null +++ b/libs/sqf/tests/snapshots/optimizer__simple_statement_2.snap @@ -0,0 +1,81 @@ +--- +source: libs/sqf/tests/optimizer.rs +expression: optimize(stringify! (statement_2)) +--- +Statements { + content: [ + AssignLocal( + "_time", + NularCommand( + NularCommand { + name: "diag_tickTime", + }, + 16..29, + ), + 0..29, + ), + AssignGlobal( + "start", + BinaryCommand( + Add, + Variable( + "_time", + 39..44, + ), + Number( + FloatOrd( + 1.0, + ), + 47..48, + ), + 45..46, + ), + 31..48, + ), + Expression( + BinaryCommand( + Named( + "call", + ), + Array( + [], + 53..54, + ), + Variable( + "unknown", + 60..67, + ), + 55..59, + ), + 52..67, + ), + Expression( + BinaryCommand( + Named( + "vectorAdd", + ), + UnaryCommand( + Named( + "getPos", + ), + NularCommand( + NularCommand { + name: "player", + }, + 92..98, + ), + 85..91, + ), + Variable( + "offset", + 115..121, + ), + 105..114, + ), + 100..121, + ), + ], + source: "private _time = diag_tickTime;\nstart = _time + 1; \n\n[] call unknown;\n\nprivate _pos = getPos player;\n_pos vectorAdd offset \n", + span: 0..121, + issues: [], +}