use midenc_hir::{self as hir, SourceSpan, TraceTarget};
use smallvec::SmallVec;
use super::{tactics::Tactic, *};
use crate::Constraint;
#[derive(Debug)]
pub enum SolverError {
AlreadySolved,
NoSolution,
}
#[derive(Debug, Clone)]
pub struct SolverOptions {
pub trace_target: TraceTarget,
pub fuel: usize,
pub strict: bool,
}
impl Default for SolverOptions {
fn default() -> Self {
Self {
trace_target: TraceTarget::category("codegen").with_topic("solver"),
strict: true,
fuel: 40,
}
}
}
pub struct OperandMovementConstraintSolver {
context: SolverContext,
tactics: SmallVec<[Box<dyn Tactic>; 4]>,
fuel: usize,
}
impl OperandMovementConstraintSolver {
pub(crate) fn solution_requires_unsupported_stack_access(
solution: &[Action],
stack: &Stack,
) -> bool {
let mut pending = stack.clone();
for action in solution.iter().copied() {
let index = match action {
Action::Copy(index)
| Action::Swap(index)
| Action::MoveUp(index)
| Action::MoveDown(index) => index as usize,
};
let required_stack_depth: usize =
pending.iter().rev().take(index + 1).map(|v| v.stack_size()).sum();
if required_stack_depth > MASM_STACK_WINDOW_FELTS {
return true;
}
match action {
Action::Copy(index) => {
let value = pending[index as usize];
pending.push(value);
}
Action::Swap(index) => {
pending.swap(index as usize);
}
Action::MoveUp(index) => {
pending.movup(index as usize);
}
Action::MoveDown(index) => {
pending.movdn(index as usize);
}
}
}
false
}
#[cfg(test)]
pub fn new(
expected: &[hir::ValueRef],
constraints: &[Constraint],
stack: &crate::OperandStack,
) -> Result<Self, SolverError> {
Self::new_with_options(expected, constraints, stack, Default::default())
}
pub fn new_with_options(
expected: &[hir::ValueRef],
constraints: &[Constraint],
stack: &crate::OperandStack,
options: SolverOptions,
) -> Result<Self, SolverError> {
assert_eq!(expected.len(), constraints.len());
let fuel = options.fuel;
let context = SolverContext::new(expected, constraints, stack, options)?;
Ok(Self {
context,
tactics: Default::default(),
fuel,
})
}
#[inline]
fn trace_target(&self) -> &TraceTarget {
&self.context.options().trace_target
}
pub fn solve(mut self) -> Result<Vec<Action>, SolverError> {
use super::tactics::*;
if self.tactics.is_empty() {
let is_binary = self.context.arity() == 2;
if is_binary {
self.tactics.push(Box::new(TwoArgs));
} else if self.context.copies().is_empty() {
self.tactics.push(Box::new(LinearStackWindow));
self.tactics.push(Box::new(Linear));
self.tactics.push(Box::new(SwapAndMoveUp));
self.tactics.push(Box::new(MoveUpAndSwap));
self.tactics.push(Box::new(MoveDownAndSwap));
} else {
self.tactics.push(Box::new(LinearStackWindow));
self.tactics.push(Box::new(Linear));
self.tactics.push(Box::new(CopyAll));
}
}
let mut best_solution: Option<Vec<Action>> = None;
let mut builder = SolutionBuilder::new(&self.context);
while let Some(mut tactic) = self.tactics.pop() {
match tactic.apply(&mut builder) {
Ok(_) => {
if builder.is_valid() {
let solution = builder.take();
if Self::solution_requires_unsupported_stack_access(
&solution,
self.context.stack(),
) {
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"a solution was found using tactic {}, but it requires stack \
access deeper than supported by MASM; rejecting it",
tactic.name()
);
} else {
let solution_size = solution.len();
let best_size = best_solution.as_ref().map(|best| best.len());
match best_size {
Some(best_size) if best_size > solution_size => {
best_solution = Some(solution);
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"a better solution ({solution_size} vs {best_size}) was \
found using tactic {}",
tactic.name()
);
}
Some(best_size) => {
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"a solution of size {solution_size} was found using \
tactic {}, but it is no better than the best found so \
far ({best_size})",
tactic.name()
);
}
None => {
best_solution = Some(solution);
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"an initial solution of size {solution_size} was found \
using tactic {}",
tactic.name()
);
}
}
}
} else {
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"a partial solution was found using tactic {}, but is not sufficient \
on its own",
tactic.name()
);
builder.discard();
}
}
Err(_) => {
log::trace!(target: self.trace_target(), symbol = self.trace_target().relevant_symbol(); "tactic {} could not be applied", tactic.name());
builder.discard();
}
}
let remaining_fuel = self.fuel.saturating_sub(tactic.cost(&self.context));
self.fuel = remaining_fuel;
if remaining_fuel == 0 && best_solution.is_some() {
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"no more optimization fuel, using the best solution found so far"
);
break;
}
}
best_solution.take().ok_or(SolverError::NoSolution)
}
#[cfg(test)]
pub fn solve_with_tactic<T: Tactic + Default>(
self,
) -> Result<Option<Vec<Action>>, SolverError> {
use super::tactics::*;
let mut builder = SolutionBuilder::new(&self.context);
let mut tactic = <T as Default>::default();
match tactic.apply(&mut builder) {
Ok(_) => {
if builder.is_valid() {
let solution = builder.take();
if Self::solution_requires_unsupported_stack_access(
&solution,
self.context.stack(),
) {
Ok(None)
} else {
Ok(Some(solution))
}
} else {
log::trace!(
target: self.trace_target(),
symbol = self.trace_target().relevant_symbol();
"a partial solution was found using tactic {}, but is not sufficient on \
its own",
tactic.name()
);
Ok(None)
}
}
Err(_) => {
log::trace!(target: self.trace_target(), symbol = self.trace_target().relevant_symbol(); "tactic {} could not be applied", tactic.name());
Err(SolverError::NoSolution)
}
}
}
pub fn solve_and_apply(
self,
emitter: &mut crate::emit::OpEmitter<'_>,
span: SourceSpan,
) -> Result<(), SolverError> {
match self.context.arity() {
0 => Ok(()),
1 => {
let expected = self.context.expected()[0];
if let Some(current_position) = self.context.stack().position(&expected) {
if current_position > 0 {
emitter.move_operand_to_position(current_position, 0, false, span);
}
} else {
assert!(
self.context.copies().has_copies(&expected.unaliased()),
"{:?} was not found on the operand stack copies",
expected.unaliased()
);
let current_position =
self.context.stack().position(&expected.unaliased()).unwrap_or_else(|| {
panic!("{:?} was not found on the operand stack", expected.unaliased())
});
emitter.copy_operand_to_position(current_position, 0, false, span);
}
Ok(())
}
_ => {
let actions = self.solve()?;
for action in actions.into_iter() {
match action {
Action::Copy(index) => {
emitter.copy_operand_to_position(index as usize, 0, false, span);
}
Action::Swap(index) => {
emitter.swap(index, span);
}
Action::MoveUp(index) => {
emitter.movup(index, span);
}
Action::MoveDown(index) => {
emitter.movdn(index, span);
}
}
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use midenc_hir::{self as hir, Type};
use proptest::prelude::*;
use super::{super::testing, *};
fn apply_actions(mut stack: crate::OperandStack, actions: &[Action]) -> crate::OperandStack {
for action in actions.iter().copied() {
match action {
Action::Copy(index) => stack.dup(index as usize),
Action::Swap(index) => stack.swap(index as usize),
Action::MoveUp(index) => stack.movup(index as usize),
Action::MoveDown(index) => stack.movdn(index as usize),
}
}
stack
}
#[test]
fn operand_movement_constraint_solver_stack_window_fallback_full_window_top_copy() {
let problem = testing::make_problem_inputs((0..16).collect(), 16, 0b0000_0000_0000_0001);
let context = SolverContext::new(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid");
let actions = OperandMovementConstraintSolver::new_with_options(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid")
.solve()
.expect("expected solver to find a supported solution via fallback");
let pending = apply_actions(problem.stack.clone(), &actions);
for (index, expected) in problem.expected.iter().copied().enumerate() {
assert_eq!(&pending[index], &expected);
}
assert!(
!OperandMovementConstraintSolver::solution_requires_unsupported_stack_access(
&actions,
context.stack(),
),
"solver produced a solution requiring unsupported stack access: {problem:#?}"
);
}
#[test]
fn operand_movement_constraint_solver_stack_window_fallback_regression_case() {
let problem = testing::make_problem_inputs(
vec![0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 2],
4,
0b0111,
);
let solver_context = SolverContext::new(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid");
let actions = OperandMovementConstraintSolver::new_with_options(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid")
.solve()
.expect("expected solver to find a supported solution via fallback");
let pending = apply_actions(problem.stack.clone(), &actions);
for (index, expected) in problem.expected.iter().copied().enumerate() {
assert_eq!(&pending[index], &expected);
}
assert!(
!OperandMovementConstraintSolver::solution_requires_unsupported_stack_access(
&actions,
solver_context.stack(),
),
"solver produced a solution requiring unsupported stack access: {problem:#?}"
);
}
#[test]
fn operand_movement_constraint_solver_stack_window_fallback_full_window_nontrivial_permutation()
{
let problem = testing::make_problem_inputs(
vec![0, 2, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
16,
0b0000_0000_0000_0001,
);
let solver_context = SolverContext::new(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid");
let actions = OperandMovementConstraintSolver::new_with_options(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 10,
..Default::default()
},
)
.expect("expected solver context to be valid")
.solve()
.expect("expected solver to find a supported solution via fallback");
let pending = apply_actions(problem.stack.clone(), &actions);
for (index, expected) in problem.expected.iter().copied().enumerate() {
assert_eq!(&pending[index], &expected);
}
assert!(
!OperandMovementConstraintSolver::solution_requires_unsupported_stack_access(
&actions,
solver_context.stack(),
),
"solver produced a solution requiring unsupported stack access: {problem:#?}"
);
}
#[test]
fn operand_movement_constraint_solver_exhausts_tactics_when_out_of_fuel() {
let problem = testing::make_problem_inputs(vec![0, 1, 2], 3, 0b0000_0000_0000_0001);
let actions = OperandMovementConstraintSolver::new_with_options(
&problem.expected,
&problem.constraints,
&problem.stack,
SolverOptions {
fuel: 0,
..Default::default()
},
)
.expect("expected solver context to be valid")
.solve()
.expect("expected solver to find a solution even with no optimization fuel");
let pending = apply_actions(problem.stack.clone(), &actions);
for (index, expected) in problem.expected.iter().copied().enumerate() {
assert_eq!(&pending[index], &expected);
}
}
#[test]
fn operand_movement_constraint_solver_example() {
use hir::Context;
let context = Rc::new(Context::default());
let block = context.create_block_with_params(vec![Type::I32; 6]);
let block = block.borrow();
let block_args = block.arguments();
let v1 = block_args[0] as hir::ValueRef;
let v2 = block_args[1] as hir::ValueRef;
let v3 = block_args[2] as hir::ValueRef;
let v4 = block_args[3] as hir::ValueRef;
let v5 = block_args[4] as hir::ValueRef;
let v6 = block_args[5] as hir::ValueRef;
let tests = [[v2, v1, v3, v4, v5, v6], [v2, v4, v3, v1, v5, v6]];
for test in tests.into_iter() {
let mut stack = crate::OperandStack::default();
for value in test.into_iter().rev() {
stack.push(value);
}
let expected = [v1, v2, v3, v4, v5];
let constraints = [Constraint::Move; 5];
match OperandMovementConstraintSolver::new(&expected, &constraints, &stack) {
Ok(solver) => {
let result = solver.solve().expect("no solution found");
assert!(result.len() <= 3, "expected solution of 3 moves or less");
}
Err(SolverError::AlreadySolved) => panic!("already solved"),
Err(err) => panic!("invalid solver context: {err:?}"),
}
}
}
#[test]
fn operand_movement_constraint_solver_two_moves() {
use hir::Context;
let context = Rc::new(Context::default());
let block = context.create_block_with_params(vec![Type::I32; 6]);
let block = block.borrow();
let block_args = block.arguments();
let v1 = block_args[0] as hir::ValueRef;
let v2 = block_args[1] as hir::ValueRef;
let v3 = block_args[2] as hir::ValueRef;
let v4 = block_args[3] as hir::ValueRef;
let v5 = block_args[4] as hir::ValueRef;
let v6 = block_args[5] as hir::ValueRef;
let tests = [
[v5, v4, v2, v3, v1, v6],
[v4, v5, v1, v2, v3, v6],
[v5, v2, v1, v3, v4, v6],
[v1, v3, v2, v4, v5, v6],
[v5, v2, v1, v4, v3, v6],
[v1, v3, v4, v2, v5, v6],
[v4, v3, v2, v1, v5, v6],
[v4, v3, v2, v1, v5, v6],
];
for test in tests.into_iter() {
let mut stack = crate::OperandStack::default();
for value in test.into_iter().rev() {
stack.push(value);
}
let expected = [v1, v2, v3, v4, v5];
let constraints = [Constraint::Move; 5];
match OperandMovementConstraintSolver::new(&expected, &constraints, &stack) {
Ok(solver) => {
let result = solver.solve().expect("no solution found");
assert!(
result.len() <= 2,
"expected solution of 2 moves or less, got {result:?}"
);
}
Err(SolverError::AlreadySolved) => panic!("already solved"),
Err(err) => panic!("invalid solver context: {err:?}"),
}
}
}
#[test]
fn operand_movement_constraint_solver_one_move() {
use hir::Context;
let context = Rc::new(Context::default());
let block = context.create_block_with_params(vec![Type::I32; 6]);
let block = block.borrow();
let block_args = block.arguments();
let v1 = block_args[0] as hir::ValueRef;
let v2 = block_args[1] as hir::ValueRef;
let v3 = block_args[2] as hir::ValueRef;
let v4 = block_args[3] as hir::ValueRef;
let v5 = block_args[4] as hir::ValueRef;
let v6 = block_args[5] as hir::ValueRef;
let tests = [
[v2, v3, v1, v4, v5, v6],
[v4, v1, v2, v3, v5, v6],
[v4, v2, v3, v1, v5, v6],
[v2, v1, v3, v4, v5, v6],
];
for test in tests.into_iter() {
let mut stack = crate::OperandStack::default();
for value in test.into_iter().rev() {
stack.push(value);
}
let expected = [v1, v2, v3, v4, v5];
let constraints = [Constraint::Move; 5];
match OperandMovementConstraintSolver::new(&expected, &constraints, &stack) {
Ok(solver) => {
let result = solver.solve().expect("no solution found");
assert!(
result.len() <= 1,
"expected solution of 1 move or less, got {result:?}"
);
}
Err(SolverError::AlreadySolved) => panic!("already solved"),
Err(err) => panic!("invalid solver context: {err:?}"),
}
}
}
#[test]
fn operand_movement_constraint_solver_duplicate() {
use hir::Context;
testing::logger_setup();
let context = Rc::new(Context::default());
let block = context.create_block_with_params(vec![Type::I32; 6]);
let block = block.borrow();
let block_args = block.arguments();
let v7 = block_args[0] as hir::ValueRef;
let v16 = block_args[1] as hir::ValueRef;
let v32 = block_args[2] as hir::ValueRef;
let v0 = block_args[3] as hir::ValueRef;
let tests = [[v32, v7, v16, v0]];
for test in tests.into_iter() {
let mut stack = crate::OperandStack::default();
for value in test.into_iter().rev() {
stack.push(value);
}
let expected = [v7, v7, v32, v16];
let constraints = [Constraint::Copy; 4];
match OperandMovementConstraintSolver::new(&expected, &constraints, &stack) {
Ok(solver) => {
let _result = solver.solve().expect("no solution found");
}
Err(SolverError::AlreadySolved) => panic!("already solved"),
Err(err) => panic!("invalid solver context: {err:?}"),
}
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn operand_movement_constraint_solver_copy_any(problem in testing::generate_copy_any_problem()) {
testing::solve_problem(problem)?
}
#[test]
fn operand_movement_constraint_solver_copy_none(problem in testing::generate_copy_none_problem()) {
testing::solve_problem(problem)?
}
#[test]
fn operand_movement_constraint_solver_copy_all(problem in testing::generate_copy_all_problem()) {
testing::solve_problem(problem)?
}
#[test]
fn operand_movement_constraint_solver_copy_some(problem in testing::generate_copy_some_problem()) {
testing::solve_problem(problem)?
}
#[test]
fn operand_tactics_linear_proptest(problem in testing::generate_linear_problem()) {
testing::solve_problem(problem)?
}
}
}