From 69ada55ce989797bbef834ac178def5404bbd587 Mon Sep 17 00:00:00 2001 From: PabstMirror Date: Mon, 2 Sep 2024 17:51:11 -0500 Subject: [PATCH] sqf: Optimizer (#743) --- bin/src/commands/build.rs | 9 +- bin/src/commands/dev.rs | 9 +- bin/src/modules/sqf.rs | 13 +- bin/tests/bravo/addons/main/config.cpp | 7 +- bin/tests/bravo/addons/main/test.sqf | 11 + bin/tests/build.rs | 3 +- hls/src/sqf/locate.rs | 2 +- .../analyze/lints/s07_select_parse_number.rs | 1 + libs/sqf/src/compiler/mod.rs | 12 +- libs/sqf/src/compiler/optimizer/mod.rs | 367 ++++++++++++++++++ libs/sqf/src/compiler/serializer/display.rs | 2 +- libs/sqf/src/compiler/serializer/mod.rs | 5 +- libs/sqf/src/lib.rs | 7 +- libs/sqf/tests/optimizer.rs | 93 +++++ libs/sqf/tests/optimizer/test_1/source.sqf | 6 + 15 files changed, 531 insertions(+), 16 deletions(-) create mode 100644 bin/tests/bravo/addons/main/test.sqf create mode 100644 libs/sqf/src/compiler/optimizer/mod.rs create mode 100644 libs/sqf/tests/optimizer.rs create mode 100644 libs/sqf/tests/optimizer/test_1/source.sqf diff --git a/bin/src/commands/build.rs b/bin/src/commands/build.rs index 4128e4ca..90aa3eb8 100644 --- a/bin/src/commands/build.rs +++ b/bin/src/commands/build.rs @@ -42,6 +42,12 @@ pub fn add_args(cmd: Command) -> Command { .help("Use ArmaScriptCompiler instead of HEMTT's SQF compiler") .action(ArgAction::SetTrue), ) + .arg( + clap::Arg::new("expopti") + .long("expopti") + .help("Use SQFC Optimizer") + .action(ArgAction::SetTrue), + ) } #[must_use] @@ -93,6 +99,7 @@ pub fn executor(ctx: Context, matches: &ArgMatches) -> Executor { let mut executor = Executor::new(ctx); let use_asc = matches.get_one::("asc") == Some(&true); + let use_optimizer = matches.get_one::("expopti") == Some(&true); executor.collapse(Collapse::No); @@ -100,7 +107,7 @@ pub fn executor(ctx: Context, matches: &ArgMatches) -> Executor { if matches.get_one::("no-rap") != Some(&true) { executor.add_module(Box::::default()); } - executor.add_module(Box::new(SQFCompiler::new(!use_asc))); + executor.add_module(Box::new(SQFCompiler::new(!use_asc, use_optimizer))); #[cfg(not(target_os = "macos"))] if use_asc { executor.add_module(Box::::default()); diff --git a/bin/src/commands/dev.rs b/bin/src/commands/dev.rs index d3ac8b15..9f137611 100644 --- a/bin/src/commands/dev.rs +++ b/bin/src/commands/dev.rs @@ -52,6 +52,12 @@ pub fn add_args(cmd: Command) -> Command { .help("Use ArmaScriptCompiler instead of HEMTT's SQF compiler") .action(ArgAction::SetTrue), ) + .arg( + clap::Arg::new("expopti") + .long("expopti") + .help("Use SQFC Optimizer") + .action(ArgAction::SetTrue), + ) .arg( clap::Arg::new("no-rap") .long("no-rap") @@ -131,6 +137,7 @@ pub fn context( } let use_asc = matches.get_one::("asc") == Some(&true); + let use_optimizer = matches.get_one::("expopti") == Some(&true); let mut executor = Executor::new(ctx); @@ -140,7 +147,7 @@ pub fn context( if rapify && matches.get_one::("no-rap") != Some(&true) { executor.add_module(Box::::default()); } - executor.add_module(Box::new(SQFCompiler::new(!use_asc))); + executor.add_module(Box::new(SQFCompiler::new(!use_asc, use_optimizer))); #[cfg(not(target_os = "macos"))] if use_asc { executor.add_module(Box::::default()); diff --git a/bin/src/modules/sqf.rs b/bin/src/modules/sqf.rs index d1edf1b3..32b829c8 100644 --- a/bin/src/modules/sqf.rs +++ b/bin/src/modules/sqf.rs @@ -19,14 +19,16 @@ use super::Module; #[derive(Default)] pub struct SQFCompiler { pub compile: bool, + pub optimize: bool, pub database: Option>, } impl SQFCompiler { #[must_use] - pub const fn new(compile: bool) -> Self { + pub const fn new(compile: bool, optimize: bool) -> Self { Self { compile, + optimize, database: None, } } @@ -100,7 +102,8 @@ impl Module for SQFCompiler { if !codes.failed() { if self.compile { let mut out = entry.with_extension("sqfc")?.create_file()?; - sqf.compile_to_writer(&processed, &mut out)?; + let sqf_to_write = if self.optimize { sqf.optimize() } else { sqf }; + sqf_to_write.compile_to_writer(&processed, &mut out)?; } counter.fetch_add(1, Ordering::Relaxed); } @@ -136,7 +139,11 @@ impl Module for SQFCompiler { info!( "{} {} sqf files", if self.compile { - "Compiled" + if self.optimize { + "Compiled [Optimized]" + } else { + "Compiled" + } } else { "Validated" }, diff --git a/bin/tests/bravo/addons/main/config.cpp b/bin/tests/bravo/addons/main/config.cpp index 46806cb9..b7970e90 100644 --- a/bin/tests/bravo/addons/main/config.cpp +++ b/bin/tests/bravo/addons/main/config.cpp @@ -1,3 +1,6 @@ -class MyMod { - name = "yes it is my mod"; +class CfgPatches { + class MyMod { + name = "yes it is my mod"; + requiredVersion = 2.00; + }; }; diff --git a/bin/tests/bravo/addons/main/test.sqf b/bin/tests/bravo/addons/main/test.sqf new file mode 100644 index 00000000..c18b1eae --- /dev/null +++ b/bin/tests/bravo/addons/main/test.sqf @@ -0,0 +1,11 @@ +params ["_a", ["_b", configNull], ["_c", 0, [0]]]; +params [["_cannot", []]]; + +x = 180 / 3.1413; +y = "A" + toUpper "b"; + +_a = -5; +_b = sqrt 2; +_c = sqrt -1; + +toUpper "🌭"; diff --git a/bin/tests/build.rs b/bin/tests/build.rs index a6e81587..7e098315 100644 --- a/bin/tests/build.rs +++ b/bin/tests/build.rs @@ -15,5 +15,6 @@ fn build_alpha() { fn build_bravo() { std::env::set_current_dir(format!("{}/tests/bravo", env!("CARGO_MANIFEST_DIR"))).unwrap(); hemtt::execute(&cli().get_matches_from(vec!["hemtt", "script", "test"])).unwrap(); - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "release", "--in-test"])).unwrap(); + hemtt::execute(&cli().get_matches_from(vec!["hemtt", "release", "--in-test", "--expopti"])) + .unwrap(); } diff --git a/hls/src/sqf/locate.rs b/hls/src/sqf/locate.rs index 2bc474cd..7ed0cb52 100644 --- a/hls/src/sqf/locate.rs +++ b/hls/src/sqf/locate.rs @@ -74,7 +74,7 @@ fn locate_expression( None } } - Expression::Array(experssions, _) => { + Expression::Array(experssions, _) | Expression::ConsumeableArray(experssions, _) => { for expression in experssions.iter() { if let Some(exp) = locate_expression(processed, expression, offset) { return Some(exp); diff --git a/libs/sqf/src/analyze/lints/s07_select_parse_number.rs b/libs/sqf/src/analyze/lints/s07_select_parse_number.rs index c0507a42..d6c39398 100644 --- a/libs/sqf/src/analyze/lints/s07_select_parse_number.rs +++ b/libs/sqf/src/analyze/lints/s07_select_parse_number.rs @@ -95,6 +95,7 @@ impl LintRunner for Runner { Expression::Code(_) | Expression::Number(_, _) | Expression::Array(_, _) + | Expression::ConsumeableArray(_, _) | Expression::Variable(_, _) => false, Expression::String(_, _, _) | Expression::Boolean(_, _) => true, Expression::NularCommand(cmd, _) => safe_command(cmd.as_str(), database), diff --git a/libs/sqf/src/compiler/mod.rs b/libs/sqf/src/compiler/mod.rs index e22f9736..91b67b42 100644 --- a/libs/sqf/src/compiler/mod.rs +++ b/libs/sqf/src/compiler/mod.rs @@ -7,6 +7,7 @@ //! The main entrypoint to this is the [`Statements`][crate::Statements] struct, which can be //! converted to a serializable [`Compiled`] via [`Statements::compile`][crate::Statements]. +pub mod optimizer; pub mod serializer; use std::{ops::Range, sync::Arc}; @@ -175,6 +176,9 @@ impl Expression { file_line: 0, }, )); + } else if let Constant::ConsumeableArray(_) = &constant { + // Only safe because we know this array will be consumed on use and won't be modifieable + instructions.push(Instruction::Push(ctx.add_constant(constant)?)); } else { instructions.push(Instruction::Push(ctx.add_constant(constant)?)); } @@ -183,7 +187,8 @@ impl Expression { push_constant(constant, instructions, ctx)?; } None => match *self { - Self::Array(ref array, ref location) => { + Self::Array(ref array, ref location) + | Self::ConsumeableArray(ref array, ref location) => { let array_len = array .len() .try_into() @@ -257,6 +262,11 @@ impl Expression { .map(|value| value.clone().compile_constant(processed, ctx)) .collect::>>>()? .map(Constant::Array), + Self::ConsumeableArray(ref array, ..) => array + .iter() + .map(|value| value.clone().compile_constant(processed, ctx)) + .collect::>>>()? + .map(Constant::ConsumeableArray), Self::NularCommand(ref command, ..) if command.is_constant() => { let command = try_normalize_name(&command.name)?; debug_assert_ne!( diff --git a/libs/sqf/src/compiler/optimizer/mod.rs b/libs/sqf/src/compiler/optimizer/mod.rs new file mode 100644 index 00000000..bc6b433b --- /dev/null +++ b/libs/sqf/src/compiler/optimizer/mod.rs @@ -0,0 +1,367 @@ +/// Optimizes sqf by evaulating expressions when possible and looking for arrays that can be consumed +/// +/// `ToDo`: Any command that "consumes" an array could be upgraded +/// e.g. x = y vectorAdd [0,0,1]; +/// +use crate::{BinaryCommand, Expression, Statement, Statements, UnaryCommand}; +use std::ops::Range; +use tracing::{trace, warn}; + +impl Statements { + #[must_use] + pub fn optimize(mut self) -> Self { + self.content = self.content.into_iter().map(Statement::optimise).collect(); + self + } +} + +impl Statement { + #[must_use] + pub fn optimise(self) -> Self { + match self { + Self::AssignGlobal(left, expression, right) => { + Self::AssignGlobal(left, expression.optimize(), right) + } + Self::AssignLocal(left, expression, right) => { + Self::AssignLocal(left, expression.optimize(), right) + } + Self::Expression(expression, right) => Self::Expression(expression.optimize(), right), + } + } +} + +impl Expression { + #[must_use] + #[allow(clippy::too_many_lines)] + fn optimize(self) -> Self { + match &self { + Self::Code(code) => Self::Code(code.clone().optimize()), + Self::Array(array_old, range) => { + let array_new = array_old.iter().map(|e| e.clone().optimize()).collect(); + Self::Array(array_new, range.clone()) + } + Self::UnaryCommand(op_type, right, range) => { + let right_o = right.clone().optimize(); + match op_type { + UnaryCommand::Minus => { + fn op(r: f32) -> f32 { + -r + } + if let Some(eval) = self.op_uni_float(op_type, range, &right_o, op) { + return eval; + } + } + UnaryCommand::Named(op_name) => match op_name.to_lowercase().as_str() { + "tolower" | "toloweransi" => { + fn op(r: &str) -> String { + r.to_ascii_lowercase() + } + if let Some(eval) = self.op_uni_string(op_type, range, &right_o, op) { + return eval; + } + } + "toupper" | "toupperansi" => { + fn op(r: &str) -> String { + r.to_ascii_uppercase() + } + if let Some(eval) = self.op_uni_string(op_type, range, &right_o, op) { + return eval; + } + } + "sqrt" => { + fn op(r: f32) -> f32 { + r.sqrt() + } + if let Some(eval) = self.op_uni_float(op_type, range, &right_o, op) { + return eval; + } + } + "params" => { + if let Self::Array(a_array, a_range) = &right_o { + if a_array.iter().all(Self::is_safe_param) { + trace!( + "optimizing [U:{}] ({}) => ConsumeableArray", + op_type.as_str(), + self.source() + ); + return Self::UnaryCommand( + UnaryCommand::Named(op_name.clone()), + Box::new(Self::ConsumeableArray( + a_array.clone(), + a_range.clone(), + )), + range.clone(), + ); + } + } + } + _ => {} + }, + _ => {} + } + Self::UnaryCommand(op_type.clone(), Box::new(right_o), range.clone()) + } + Self::BinaryCommand(op_type, left, right, range) => { + let left_o = left.clone().optimize(); + let right_o = right.clone().optimize(); + match op_type { + #[allow(clippy::single_match)] + BinaryCommand::Named(op_name) => match op_name.to_lowercase().as_str() { + "params" => { + if let Self::Array(a_array, a_range) = &right_o { + if a_array.iter().all(Self::is_safe_param) { + trace!( + "optimizing [B:{}] ({}) => ConsumeableArray", + op_type.as_str(), + self.source() + ); + return Self::BinaryCommand( + BinaryCommand::Named(op_name.clone()), + Box::new(left_o), + Box::new(Self::ConsumeableArray( + a_array.clone(), + a_range.clone(), + )), + range.clone(), + ); + } + } + } + _ => {} + }, + BinaryCommand::Add => { + { + fn op(l: f32, r: f32) -> f32 { + l + r + } + if let Some(eval) = + self.op_bin_float(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + { + fn op(l: &str, r: &str) -> String { + format!("{l}{r}") + } + if let Some(eval) = + self.op_bin_string(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + } + BinaryCommand::Sub => { + fn op(l: f32, r: f32) -> f32 { + l - r + } + if let Some(eval) = self.op_bin_float(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + BinaryCommand::Mul => { + fn op(l: f32, r: f32) -> f32 { + l * r + } + if let Some(eval) = self.op_bin_float(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + BinaryCommand::Div => { + fn op(l: f32, r: f32) -> f32 { + l / r + } + if let Some(eval) = self.op_bin_float(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + BinaryCommand::Rem | BinaryCommand::Mod => { + fn op(l: f32, r: f32) -> f32 { + l % r + } + if let Some(eval) = self.op_bin_float(op_type, range, &left_o, &right_o, op) + { + return eval; + } + } + BinaryCommand::Else => { + if let Self::Code(_) = left_o { + if let Self::Code(_) = right_o { + return Self::ConsumeableArray( + vec![left_o, right_o], + range.clone(), + ); + } + } + } + _ => {} + } + Self::BinaryCommand( + op_type.clone(), + Box::new(left_o), + Box::new(right_o), + range.clone(), + ) + } + _ => self, + } + } + + /* + Don't present a consumable array that could be modified: Check if param will return an array as a default value + sqfc = { + params [["_a", []]]; + x = _a; + }; + call sqfc; + x pushBack 5; + call sqfc + x is now [5] - the const has been modified + */ + #[must_use] + fn is_safe_param(&self) -> bool { + #[allow(clippy::single_match)] + match self { + Self::Array(array, _) => { + if let Some(param_default) = array.get(1) { + if param_default.is_array() { + return false; + } + } + } + _ => {} + } + true // every other check (for constness) will be handled by the serializer + } + + // Boilerplate for uniary and binary ops + #[must_use] + fn op_uni_string( + &self, + op_type: &UnaryCommand, + range: &Range, + right: &Self, + op: fn(&str) -> String, + ) -> Option { + if let Self::String(right_string, _, ref right_wrapper) = right { + if right_string.is_ascii() { + let new_string = op(right_string.as_ref()); + trace!( + "optimizing [U:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_string + ); + return Some(Self::String( + new_string.into(), + range.clone(), + right_wrapper.clone(), + )); + } + warn!( + "Skipping Optimization because unicode [U:{}] ({}) => {}", + op_type.as_str(), + self.source(), + right_string.to_string() + ); + } + None + } + #[must_use] + fn op_uni_float( + &self, + op_type: &UnaryCommand, + range: &Range, + right: &Self, + op: fn(f32) -> f32, + ) -> Option { + if let Self::Number(crate::Scalar(right_number), _) = right { + let new_number = op(*right_number); + if new_number.is_finite() { + trace!( + "optimizing [U:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_number + ); + return Some(Self::Number(crate::Scalar(new_number), range.clone())); + } + warn!( + "Skipping Optimization because NaN [U:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_number + ); + } + None + } + #[must_use] + fn op_bin_string( + &self, + op_type: &BinaryCommand, + range: &Range, + left: &Self, + right: &Self, + op: fn(&str, &str) -> String, + ) -> Option { + if let Self::String(left_string, _, ref _left_wrapper) = left { + if let Self::String(right_string, _, ref right_wrapper) = right { + if right_string.is_ascii() && left_string.is_ascii() { + let new_string = op(left_string.as_ref(), right_string.as_ref()); + trace!( + "optimizing [B:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_string + ); + return Some(Self::String( + new_string.into(), + range.clone(), + right_wrapper.clone(), + )); + } + warn!( + "Skipping Optimization because unicode [B:{}] ({}) => {}", + op_type.as_str(), + self.source(), + right_string.to_string() + ); + } + } + None + } + #[must_use] + fn op_bin_float( + &self, + op_type: &BinaryCommand, + range: &Range, + left: &Self, + right: &Self, + op: fn(f32, f32) -> f32, + ) -> Option { + if let Self::Number(crate::Scalar(left_number), _) = left { + if let Self::Number(crate::Scalar(right_number), _) = right { + let new_number = op(*left_number, *right_number); + if new_number.is_finite() { + trace!( + "optimizing [B:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_number + ); + return Some(Self::Number(crate::Scalar(new_number), range.clone())); + } + warn!( + "Skipping Optimization because NaN [B:{}] ({}) => {}", + op_type.as_str(), + self.source(), + new_number + ); + } + } + None + } +} diff --git a/libs/sqf/src/compiler/serializer/display.rs b/libs/sqf/src/compiler/serializer/display.rs index edc648e5..9e2f31a8 100644 --- a/libs/sqf/src/compiler/serializer/display.rs +++ b/libs/sqf/src/compiler/serializer/display.rs @@ -84,7 +84,7 @@ impl<'a> fmt::Display for DisplayConstant<'a> { Constant::String(string) => write!(f, "{string:?}")?, Constant::Scalar(scalar) => write!(f, "{scalar:?}")?, Constant::Boolean(boolean) => write!(f, "{boolean}")?, - Constant::Array(array) => { + Constant::Array(array) | Constant::ConsumeableArray(array) => { f.write_str("[")?; for (i, constant) in array.iter().enumerate() { if i != 0 { diff --git a/libs/sqf/src/compiler/serializer/mod.rs b/libs/sqf/src/compiler/serializer/mod.rs index 973805c7..de01e842 100644 --- a/libs/sqf/src/compiler/serializer/mod.rs +++ b/libs/sqf/src/compiler/serializer/mod.rs @@ -264,6 +264,7 @@ pub enum Constant { Scalar(f32), Boolean(bool), Array(Vec), + ConsumeableArray(Vec), NularCommand(Arc), } @@ -283,7 +284,7 @@ impl Constant { Self::String(..) => 1, Self::Scalar(..) => 2, Self::Boolean(..) => 3, - Self::Array(..) => 4, + Self::Array(..) | Self::ConsumeableArray(..) => 4, Self::NularCommand(..) => 5, } } @@ -309,7 +310,7 @@ impl Constant { Self::Boolean(value) => { writer.write_u8(u8::from(value))?; } - Self::Array(ref array) => { + Self::Array(ref array) | Self::ConsumeableArray(ref array) => { let array_len = try_truncate_or(array.len(), SerializeError::ArrayTooLong)?; writer.write_u32::(array_len)?; for constant in array { diff --git a/libs/sqf/src/lib.rs b/libs/sqf/src/lib.rs index 49492481..fb240601 100644 --- a/libs/sqf/src/lib.rs +++ b/libs/sqf/src/lib.rs @@ -168,6 +168,7 @@ pub enum Expression { Number(Scalar, Range), Boolean(bool, Range), Array(Vec, Range), + ConsumeableArray(Vec, Range), NularCommand(NularCommand, Range), UnaryCommand(UnaryCommand, Box, Range), BinaryCommand(BinaryCommand, Box, Box, Range), @@ -198,7 +199,7 @@ impl Expression { } Self::Number(number, _) => number.0.to_string(), Self::Boolean(boolean, _) => boolean.to_string(), - Self::Array(array, _) => { + Self::ConsumeableArray(array, _) | Self::Array(array, _) => { let mut out = String::new(); out.push('['); for (i, element) in array.iter().enumerate() { @@ -282,7 +283,7 @@ impl Expression { pub fn span(&self) -> Range { match self { Self::Code(code) => code.span(), - Self::Array(_, span) => span.start - 1..span.end, + Self::ConsumeableArray(_, span) | Self::Array(_, span) => span.start - 1..span.end, Self::String(_, span, _) | Self::Number(_, span) | Self::Boolean(_, span) @@ -297,7 +298,7 @@ impl Expression { pub fn full_span(&self) -> Range { match self { Self::Code(code) => code.span(), - Self::Array(_, _) => self.span(), + Self::ConsumeableArray(_, _) | Self::Array(_, _) => self.span(), Self::String(_, span, _) | Self::Number(_, span) | Self::Boolean(_, span) diff --git a/libs/sqf/tests/optimizer.rs b/libs/sqf/tests/optimizer.rs new file mode 100644 index 00000000..59a3280e --- /dev/null +++ b/libs/sqf/tests/optimizer.rs @@ -0,0 +1,93 @@ +#![allow(clippy::unwrap_used)] + +pub use float_ord::FloatOrd as Scalar; +use hemtt_preprocessor::Processor; +use hemtt_sqf::{parser::database::Database, Statements}; +use hemtt_workspace::LayerType; + +const ROOT: &str = "tests/optimizer/"; + +fn get_statements(dir: &str) -> Statements { + let folder = std::path::PathBuf::from(ROOT).join(dir); + let workspace = hemtt_workspace::Workspace::builder() + .physical(&folder, LayerType::Source) + .finish(None, false, &hemtt_common::config::PDriveOption::Disallow) + .unwrap(); + let source = workspace.join("source.sqf").unwrap(); + let processed = Processor::run(&source).unwrap(); + hemtt_sqf::parser::run(&Database::a3(false), &processed).unwrap() +} + +#[cfg(test)] +mod tests { + use hemtt_sqf::{Expression, Scalar, Statement, UnaryCommand}; + + use crate::get_statements; + + #[test] + pub fn test_1() { + let sqf = get_statements("test_1").optimize(); + println!("sqf: {:?}", sqf); + assert!(sqf.content().len() == 6); + + { + // -5; + let Statement::Expression(e, _) = sqf.content()[0].clone() else { + panic!(); + }; + let Expression::Number(value, _) = e else { + panic!(); + }; + assert_eq!(value, Scalar(-5.0)); + } + { + // "A" + "B"; + let Statement::Expression(e, _) = sqf.content()[1].clone() else { + panic!(); + }; + let Expression::String(value, _, _) = e else { + panic!(); + }; + assert_eq!(*value, *"AB"); + } + { + // 1 + 1; + let Statement::Expression(e, _) = sqf.content()[2].clone() else { + panic!(); + }; + let Expression::Number(value, _) = e else { + panic!(); + }; + assert_eq!(value, Scalar(2.0)); + } + { + // z + z; + let Statement::Expression(e, _) = sqf.content()[3].clone() else { + panic!(); + }; + assert!(matches!(e, Expression::BinaryCommand(..))); + } + { + // params ["_a", "_b"]; + let Statement::Expression(e, _) = sqf.content()[4].clone() else { + panic!(); + }; + let Expression::UnaryCommand(cmd, arg_right, _) = e else { + panic!(); + }; + assert!(matches!(cmd, UnaryCommand::Named(..))); + assert!(matches!(*arg_right, Expression::ConsumeableArray(..))); + } + { + // params ["_a", "_b", ["_c", []]]; + let Statement::Expression(e, _) = sqf.content()[5].clone() else { + panic!(); + }; + let Expression::UnaryCommand(cmd, arg_right, _) = e else { + panic!(); + }; + assert!(matches!(cmd, UnaryCommand::Named(..))); + assert!(matches!(*arg_right, Expression::Array(..))); + } + } +} diff --git a/libs/sqf/tests/optimizer/test_1/source.sqf b/libs/sqf/tests/optimizer/test_1/source.sqf new file mode 100644 index 00000000..930f5c8f --- /dev/null +++ b/libs/sqf/tests/optimizer/test_1/source.sqf @@ -0,0 +1,6 @@ +-5; +"A" + "B"; +1 + 1; +z + z; +params ["_a", "_b"]; +params ["_a", "_b", ["_c", []]];