From 0a2e5e8e22848496f3117752c0e9ef6922c1d79c Mon Sep 17 00:00:00 2001 From: Ryan Pitasky Date: Fri, 6 Sep 2024 00:15:55 -0400 Subject: [PATCH] opt: closing parenthesis optimization --- .../optimize/parentheses/maximization/1.txt | 1 + .../optimize/parentheses/maximization/2.txt | 1 + .../parentheses/stripping/after/1.txt | 1 + .../parentheses/stripping/after/2.txt | 1 + .../parentheses/stripping/after/3.txt | 1 + .../parentheses/stripping/after/4.txt | 1 + .../parentheses/stripping/after/5.txt | 1 + .../parentheses/stripping/after/6.txt | 1 + .../parentheses/stripping/before/1.txt | 1 + .../parentheses/stripping/before/2.txt | 1 + .../parentheses/stripping/before/3.txt | 1 + .../parentheses/stripping/before/4.txt | 1 + .../parentheses/stripping/before/5.txt | 1 + .../parentheses/stripping/before/6.txt | 1 + .../formulas/parenthesis-optimization.txt | 1 - ti-basic-optimizer/src/main.rs | 15 +- .../expressions/parenthesis_optimization.rs | 159 ++++++++++++++++-- ti-basic-optimizer/src/optimize/mod.rs | 12 ++ ti-basic-optimizer/src/parse/commands/mod.rs | 23 ++- 19 files changed, 199 insertions(+), 25 deletions(-) create mode 100644 test-files/snippets/optimize/parentheses/maximization/1.txt create mode 100644 test-files/snippets/optimize/parentheses/maximization/2.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/1.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/2.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/3.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/4.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/5.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/after/6.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/1.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/2.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/3.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/4.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/5.txt create mode 100644 test-files/snippets/optimize/parentheses/stripping/before/6.txt delete mode 100644 test-files/snippets/parsing/formulas/parenthesis-optimization.txt diff --git a/test-files/snippets/optimize/parentheses/maximization/1.txt b/test-files/snippets/optimize/parentheses/maximization/1.txt new file mode 100644 index 0000000..416df95 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/maximization/1.txt @@ -0,0 +1 @@ +sin(cos(|LABC(X)))*~(1+2)+3 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/maximization/2.txt b/test-files/snippets/optimize/parentheses/maximization/2.txt new file mode 100644 index 0000000..7568e71 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/maximization/2.txt @@ -0,0 +1 @@ +C and |LSH(A)=N \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/1.txt b/test-files/snippets/optimize/parentheses/stripping/after/1.txt new file mode 100644 index 0000000..a5f09d5 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/1.txt @@ -0,0 +1 @@ +(1+1 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/2.txt b/test-files/snippets/optimize/parentheses/stripping/after/2.txt new file mode 100644 index 0000000..7c1efa6 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/2.txt @@ -0,0 +1 @@ +(2+(2+2 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/3.txt b/test-files/snippets/optimize/parentheses/stripping/after/3.txt new file mode 100644 index 0000000..33b5045 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/3.txt @@ -0,0 +1 @@ +"(333) \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/4.txt b/test-files/snippets/optimize/parentheses/stripping/after/4.txt new file mode 100644 index 0000000..f8f698c --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/4.txt @@ -0,0 +1 @@ +("4444 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/5.txt b/test-files/snippets/optimize/parentheses/stripping/after/5.txt new file mode 100644 index 0000000..3efc8a4 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/5.txt @@ -0,0 +1 @@ +(5+5)+(5+5 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/after/6.txt b/test-files/snippets/optimize/parentheses/stripping/after/6.txt new file mode 100644 index 0000000..a6e703d --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/after/6.txt @@ -0,0 +1 @@ +("6"+6 \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/1.txt b/test-files/snippets/optimize/parentheses/stripping/before/1.txt new file mode 100644 index 0000000..5ebeabb --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/1.txt @@ -0,0 +1 @@ +(1+1) \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/2.txt b/test-files/snippets/optimize/parentheses/stripping/before/2.txt new file mode 100644 index 0000000..8d75736 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/2.txt @@ -0,0 +1 @@ +(2+(2+2)) \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/3.txt b/test-files/snippets/optimize/parentheses/stripping/before/3.txt new file mode 100644 index 0000000..0e8b84c --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/3.txt @@ -0,0 +1 @@ +"(333)" \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/4.txt b/test-files/snippets/optimize/parentheses/stripping/before/4.txt new file mode 100644 index 0000000..2f4d79c --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/4.txt @@ -0,0 +1 @@ +("4444") \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/5.txt b/test-files/snippets/optimize/parentheses/stripping/before/5.txt new file mode 100644 index 0000000..1e81818 --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/5.txt @@ -0,0 +1 @@ +(5+5)+(5+5) \ No newline at end of file diff --git a/test-files/snippets/optimize/parentheses/stripping/before/6.txt b/test-files/snippets/optimize/parentheses/stripping/before/6.txt new file mode 100644 index 0000000..fc7ca4d --- /dev/null +++ b/test-files/snippets/optimize/parentheses/stripping/before/6.txt @@ -0,0 +1 @@ +("6"+6) \ No newline at end of file diff --git a/test-files/snippets/parsing/formulas/parenthesis-optimization.txt b/test-files/snippets/parsing/formulas/parenthesis-optimization.txt deleted file mode 100644 index 4834770..0000000 --- a/test-files/snippets/parsing/formulas/parenthesis-optimization.txt +++ /dev/null @@ -1 +0,0 @@ -sin(cos(X))*~(1+2)+3 \ No newline at end of file diff --git a/ti-basic-optimizer/src/main.rs b/ti-basic-optimizer/src/main.rs index 81f99f9..a5b90b7 100644 --- a/ti-basic-optimizer/src/main.rs +++ b/ti-basic-optimizer/src/main.rs @@ -81,15 +81,16 @@ fn main() { Priority::Neutral }; - if let Ok(program) = loaded { - if cfg!(feature = "round-trip") { - let version = titokens::version::LATEST.clone(); + let version = titokens::version::LATEST.clone(); let config = Config { mrov: version.clone(), priority, - }; + }; + let tokenizer = Tokenizer::new(version.clone(), "en"); + + if let Ok(mut program) = loaded { + if cfg!(feature = "round-trip") { - let tokenizer = Tokenizer::new(version.clone(), "en"); let a = program.reconstruct(&config); let a_program = Program::from_tokens( &mut Tokens::from_vec(a.clone(), Some(version.clone())), @@ -108,6 +109,10 @@ fn main() { println!("{}", tokenizer.stringify(&b)); } else { println!("Loaded program successfully!"); + program.optimize(&config); + + let tokens = program.reconstruct(&config); + println!("{}", tokenizer.stringify(&tokens)); } } else { loaded.unwrap(); diff --git a/ti-basic-optimizer/src/optimize/expressions/parenthesis_optimization.rs b/ti-basic-optimizer/src/optimize/expressions/parenthesis_optimization.rs index 5be8760..2308bcb 100644 --- a/ti-basic-optimizer/src/optimize/expressions/parenthesis_optimization.rs +++ b/ti-basic-optimizer/src/optimize/expressions/parenthesis_optimization.rs @@ -5,7 +5,8 @@ use std::mem; use titokens::Token; -use crate::parse::components::{ListIndex, MatrixIndex}; +use crate::parse::commands::{Command, ControlFlow, Generic}; +use crate::parse::components::{ListIndex, MatrixIndex, StoreTarget}; use crate::parse::{ components::{Operand, Operator}, expression::Expression, @@ -16,6 +17,28 @@ impl Expression { /// /// Returns the number of parentheses that can be removed. pub fn optimize_parentheses(&mut self) -> u16 { + /* there's some extra weirdness with lists/Ans & implicit mul that I'm not yet considering. + * `Ans(1+X)` is a list access if Ans is a list. We avoid this with `(1+X)Ans` or `Ans*(1+X)` + * If X has parentheses to be removed *and those parentheses get removed*, `Ans*(1+X)` is + * shorter. Otherwise, `(1+X)Ans` is shorter or equivalent. + * + * One more note... + * If `Ans(1+X)` is a list access or ambiguously written, we conservatively default to + * parsing it as a list access- if it is actually an implicit multiplication then it will + * always execute the same way as it did in the input. Defaulting to parsing as an + * implicit multiplication may not result in the same code, because several passes depend on + * multiplication being commutative. + */ + + /* more opt ideas: consider functions where changing the arguments does not change the result + * (eg. min, max), and some functions on list literals where changing the order of the + * list elements will not change the output. + */ + + /* even more opt ideas: stuff like /2 or /5 or /10 can be .5*, .2*, .1* + * also x^^-1* if x is not 2, 5, 10, etc + */ + match self { Expression::Operator(Operator::Binary(binop)) => { let mut right = binop.right.optimize_parentheses(); @@ -81,25 +104,139 @@ impl Expression { 1 + col.optimize_parentheses() } - _=> 0, + Expression::Operand(Operand::StringLiteral(_) | Operand::ListLiteral(_)) => 1, + + _ => 0, + } + } + + /// Removes closing parenthesis, braces, and quotes from the provided reconstructed expression. + /// + /// Returns true if the expression ends in an unclosed string. + pub fn strip_closing_parenthesis(expr: &mut Vec) -> bool { + // a little tricky; `")))` should not have anything removed + let mut length_guess = 0; + let mut in_string = false; + + let mut unclosed_string = false; + // 123"123)") + + for (idx, tok) in expr.iter().enumerate() { + match tok { + Token::OneByte(0x2A) => { + // " + in_string = !in_string; + + if !in_string { + length_guess = idx - 1; + unclosed_string = true; + } + } + + Token::OneByte(0x11 | 0x07 | 0x09) if !in_string => { + // ) ] } + // nothing + } + + _ => { + length_guess = idx; + + unclosed_string = in_string; + } + } + } + + expr.truncate(length_guess + 1); + + unclosed_string + } +} + +impl Command { + pub fn optimize_parentheses(&mut self) { + match self { + Command::Generic(Generic { arguments, .. }) => { + if let Some(last) = arguments.last_mut() { + last.optimize_parentheses(); + } + } + + Command::ControlFlow(control_flow) => { + match control_flow { + ControlFlow::If(expr) | ControlFlow::While(expr) | ControlFlow::Repeat(expr) => { + expr.optimize_parentheses(); + } + ControlFlow::For(for_loop) => { + if let Some(step) = &mut for_loop.step { + step.optimize_parentheses(); + } else { + for_loop.end.optimize_parentheses(); + } + } + ControlFlow::IsGt(is_ds) | ControlFlow::DsLt(is_ds) => { + is_ds.condition.optimize_parentheses(); + } + + _ => { /* no closing parentheses savings possible */ } + } + } + + Command::Expression(expr) => { + expr.optimize_parentheses(); + } + Command::Store(expr, target) => { + expr.optimize_parentheses(); + match target { + StoreTarget::ListIndex(index) => { + index.index.optimize_parentheses(); + } + StoreTarget::MatrixIndex(index) => { + index.col.optimize_parentheses(); + } + + _ => { /* no closing parentheses possible */ } + } + } + + _ => { /* no closing parentheses possible */ } } } } #[cfg(test)] mod tests { - use crate::parse::Parse; - use test_files::load_test_data; - + use crate::parse::{Parse, Reconstruct}; + use test_files::{load_test_data, test_version}; use super::*; + #[test] fn parenthesis_optimization() { - let mut tokens = load_test_data("/snippets/parsing/formulas/parenthesis-optimization.txt"); - let mut expr = Expression::parse(tokens.next().unwrap(), &mut tokens) - .ok() - .flatten() - .unwrap(); + let cases = [("1.txt", 3), ("2.txt", 1)]; + for (case_name, expected_savings) in cases { + let mut tokens = load_test_data(&("/snippets/optimize/parentheses/maximization/".to_string() + case_name)); + let mut expr = Expression::parse(tokens.next().unwrap(), &mut tokens) + .unwrap() + .unwrap(); + + let savings = expr.optimize_parentheses(); + assert_eq!(expected_savings, savings); + + let reconstructed = expr.reconstruct(&test_version().into()); + let mut optimized = reconstructed.clone(); + Expression::strip_closing_parenthesis(&mut optimized); - assert_eq!(expr.optimize_parentheses(), 2); + assert_eq!(expected_savings, (reconstructed.len() - optimized.len()) as u16); + } + } + + #[test] + fn strip_closing_parentheses() { + for case in ["1.txt", "2.txt", "3.txt", "4.txt", "5.txt", "6.txt"] { + let mut actual = load_test_data(&("/snippets/optimize/parentheses/stripping/before/".to_string() + case)).collect::>(); + let expected = load_test_data(&("/snippets/optimize/parentheses/stripping/after/".to_string() + case)).collect::>(); + Expression::strip_closing_parenthesis(&mut actual); + + assert_eq!(actual, expected); + } } } diff --git a/ti-basic-optimizer/src/optimize/mod.rs b/ti-basic-optimizer/src/optimize/mod.rs index fbd373f..94d0999 100644 --- a/ti-basic-optimizer/src/optimize/mod.rs +++ b/ti-basic-optimizer/src/optimize/mod.rs @@ -1,5 +1,9 @@ +use crate::Config; +use crate::parse::Program; + mod expressions; mod strategies; +mod control_flow; #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] pub enum Priority { @@ -11,3 +15,11 @@ pub enum Priority { /// Disables optimizations which would increase the program's size. Size, } + +impl Program { + pub fn optimize(&mut self, config: &Config) { + for command in self.lines.iter_mut() { + command.optimize_parentheses(); + } + } +} \ No newline at end of file diff --git a/ti-basic-optimizer/src/parse/commands/mod.rs b/ti-basic-optimizer/src/parse/commands/mod.rs index abc8cfa..ff7786e 100644 --- a/ti-basic-optimizer/src/parse/commands/mod.rs +++ b/ti-basic-optimizer/src/parse/commands/mod.rs @@ -69,20 +69,27 @@ impl Parse for Command { impl Reconstruct for Command { fn reconstruct(&self, config: &Config) -> Vec { - match self { + let mut line = match self { Command::ControlFlow(x) => x.reconstruct(config), Command::Generic(x) => x.reconstruct(config), Command::DelVarChain(x) => x.reconstruct(config), Command::SetUpEditor(x) => x.reconstruct(config), Command::Expression(x) => x.reconstruct(config), Command::ProgramInvocation(x) => x.reconstruct(config), - Command::Store(x, target) => x - .reconstruct(config) - .into_iter() - .chain(once(Token::OneByte(0x04))) - .chain(target.reconstruct(config)) - .collect(), - } + Command::Store(x, target) => { + let mut expr = x + .reconstruct(config); + Expression::strip_closing_parenthesis(&mut expr); + expr.into_iter() + .chain(once(Token::OneByte(0x04))) + .chain(target.reconstruct(config)) + .collect() + }, + }; + + Expression::strip_closing_parenthesis(&mut line); + + line } }