Compare commits

...

3 Commits

23
Cargo.lock generated

@ -15,15 +15,32 @@ dependencies = [
name = "basic-to-mindustry"
version = "0.1.0"
dependencies = [
"pretty_assertions",
"regex",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "memchr"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "regex"
version = "1.9.5"
@ -52,3 +69,9 @@ name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

@ -7,3 +7,6 @@ edition = "2021"
[dependencies]
regex = "1.9.5"
[dev-dependencies]
pretty_assertions = "1.4.0"

@ -101,26 +101,33 @@ pub(crate) fn format_unary_operator(operator: UnaryOperator) -> &'static str {
#[derive(Clone, Debug)]
pub struct Config {
pub builtin_functions: HashMap<String, (String, usize)>,
pub builtin_functions: HashMap<String, (String, bool, usize)>,
}
impl Default for Config {
fn default() -> Self {
macro_rules! builtin_function {
( $name:expr, $target_name:expr, $n_args:expr ) => {
(String::from($name), (String::from($target_name), $n_args))
( $name:expr, $target_name:expr, $mutating:expr, $n_args:expr ) => {
(
String::from($name),
(String::from($target_name), $mutating, $n_args),
)
};
}
Self {
builtin_functions: HashMap::from([
builtin_function!("print_flush", "printflush", 1),
builtin_function!("print_message_mission", "message mission", 0),
builtin_function!("read", "read", 3),
builtin_function!("write", "write", 3),
builtin_function!("wait", "wait", 1),
builtin_function!("set_flag", "setflag", 2),
builtin_function!("get_flag", "getflag", 2),
builtin_function!("spawn", "spawn", 6),
builtin_function!("print_flush", "printflush", false, 1),
// TODO: write a special case for message
builtin_function!("print_message_mission", "message mission", false, 0),
builtin_function!("read", "read", true, 3),
// TODO: don't use a generic operation here
builtin_function!("write", "write", false, 3),
builtin_function!("wait", "wait", false, 1),
// TODO: don't use a generic operation here either
builtin_function!("set_flag", "setflag", false, 2),
builtin_function!("get_flag", "getflag", true, 2),
// TODO: same thing
builtin_function!("spawn", "spawn", false, 6),
]),
}
}

@ -35,8 +35,18 @@ pub enum MindustryOperation {
JumpIf(String, Operator, Operand, Operand),
Operator(String, Operator, Operand, Operand),
UnaryOperator(String, UnaryOperator, Operand),
// TODO: add RandOperator
Set(String, Operand),
/// A generic operation, with the following invariants:
/// - all of the operands are read-only
/// - there is no external dependency to other variables
/// - no external variable is modified
Generic(String, Vec<Operand>),
/// A generic, mutating operation `(name, out_name, operands)`, with the following invariants:
/// - all of the operands are read-only
/// - there is no external dependency to other variables, except `out_name`
/// - only `out_name` is modified
GenericMut(String, String, Vec<Operand>),
}
impl MindustryOperation {
@ -48,6 +58,9 @@ impl MindustryOperation {
Self::Generic(_name, operands) => {
operands.iter().collect::<Vec<_>>().into_boxed_slice()
}
Self::GenericMut(_name, _out_name, operands) => {
operands.iter().collect::<Vec<_>>().into_boxed_slice()
}
_ => Box::new([]),
}
}
@ -58,9 +71,24 @@ impl MindustryOperation {
Self::Operator(_target, _operator, lhs, rhs) => vec![lhs, rhs],
Self::Set(_target, value) => vec![value],
Self::Generic(_name, operands) => operands.iter_mut().collect::<Vec<_>>(),
Self::GenericMut(_name, _out_name, operands) => operands.iter_mut().collect::<Vec<_>>(),
_ => vec![],
}
}
fn mutates(&self, var_name: &str) -> bool {
match self {
MindustryOperation::JumpLabel(_)
| MindustryOperation::Jump(_)
| MindustryOperation::JumpIf(_, _, _, _)
| MindustryOperation::Generic(_, _) => false,
MindustryOperation::Operator(out_name, _, _, _)
| MindustryOperation::UnaryOperator(out_name, _, _)
| MindustryOperation::Set(out_name, _)
| MindustryOperation::GenericMut(_, out_name, _) => out_name == var_name,
}
}
}
#[derive(Debug, Clone)]
@ -244,10 +272,17 @@ pub fn translate_ast(
}
}
Instr::CallBuiltin(name, arguments) => {
let argument_names = (0..arguments.len())
let Some((target_name, mutating, _)) = config.builtin_functions.get(name) else {
unreachable!("CallBuilting constructed with unknown function name");
};
let mutating = *mutating;
let first_index = mutating as usize;
let argument_names = (first_index..arguments.len())
.map(|_| namer.temporary())
.collect::<Vec<_>>();
for (i, argument) in arguments.iter().enumerate() {
for (i, argument) in arguments.iter().skip(first_index).enumerate() {
res.append(&mut translate_expression(
argument,
namer,
@ -255,7 +290,23 @@ pub fn translate_ast(
));
}
if let Some((target_name, _)) = config.builtin_functions.get(name) {
if mutating {
let BasicAstExpression::Variable(out_name) = arguments[0].clone() else {
unreachable!(
"First argument to {} isn't a variable, got {:?}",
name, arguments[0]
);
};
res.push(MindustryOperation::GenericMut(
target_name.clone(),
out_name,
argument_names
.into_iter()
.map(|name| Operand::Variable(name))
.collect(),
));
} else {
res.push(MindustryOperation::Generic(
target_name.clone(),
argument_names
@ -354,6 +405,14 @@ impl std::fmt::Display for MindustryProgram {
}
write!(f, "\n")?;
}
MindustryOperation::GenericMut(name, out_name, operands) => {
write!(f, "{}", name)?;
write!(f, " {}", out_name)?;
for operand in operands {
write!(f, " {}", operand)?;
}
write!(f, "\n")?;
}
}
}

@ -1,407 +0,0 @@
use std::collections::HashSet;
use super::*;
/// Optimizes away unnecessary `sets`
pub fn optimize_set_use(program: MindustryProgram) -> MindustryProgram {
let mut res = MindustryProgram::new();
let instructions = program.0;
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
// Find and replace references to constants
// TODO: multiple rounds?
for (use_index, instruction) in instructions.iter().enumerate() {
let optimizable_operands = instruction
.operands()
.iter()
.filter_map(|operand| match operand {
Operand::Variable(name) => {
if tmp_regex.is_match(name) {
Some(name)
} else {
None
}
}
_ => None,
})
// PERF: check when it would be better to deduplicate operands
// .collect::<HashSet<_>>()
// .into_iter()
.filter_map(|name| {
for (index, instr) in instructions[0..use_index].iter().enumerate().rev() {
match instr {
MindustryOperation::Set(set_name, value) if set_name == name => {
return Some((name.clone(), value.clone(), index))
}
MindustryOperation::Operator(op_name, _op, _lhs, _rhs)
if op_name == name =>
{
// Not optimizable
break;
}
MindustryOperation::JumpLabel(_label) => {
// Note: jump labels mark boundaries for constants. For instance:
// ```
// set __tmp_1 "this is not a constant"
// jump_label:
// set __tmp_2 "this is a constant"
// op add result __tmp_1 __tmp_2
// ```
//
// gets optimized to:
// ```
// set __tmp_1 "this is not a constant"
// jump_label:
// op add result __tmp_1 "this is a constant"
// ```
//
// A more complex algorithm could be used to check the flow of the program,
// but this usecase isn't needed yet.
break;
}
_ => {}
}
}
None
})
.filter(|(_name, value, set_index)| {
// Don't optimize operands that refer to a mutating variable (either mutable @-variables or instructions that get updated in-between)
if let Operand::Variable(assigned_var) = value {
if matches!(
assigned_var.as_str(),
"@this" | "@thisx" | "@thisy" | "@links"
) || is_unit_constant(assigned_var.as_str())
{
return true;
}
if assigned_var.starts_with('@') {
return false;
}
for instr_between in &instructions[*set_index..use_index] {
match instr_between {
MindustryOperation::Set(var_name, _)
| MindustryOperation::Operator(var_name, _, _, _) => {
if var_name == assigned_var {
return false;
}
}
_ => {}
}
}
true
} else {
true
}
})
.collect::<Vec<_>>();
if optimizable_operands.len() > 0 {
let mut instruction = instruction.clone();
for operand in instruction.operands_mut() {
if let Operand::Variable(use_name) = operand {
if let Some((_, optimized_into, _)) = optimizable_operands
.iter()
.find(|(set_name, _, _)| set_name == use_name)
{
*operand = optimized_into.clone();
}
}
}
res.push(instruction);
} else {
res.push(instruction.clone());
}
}
optimize_dead_code(res)
}
fn is_unit_constant(name: &str) -> bool {
matches!(
name,
"@stell"
| "@locus"
| "@precept"
| "@vanquish"
| "@conquer"
| "@merui"
| "@cleroi"
| "@anthicus"
| "@tecta"
| "@collaris"
| "@elude"
| "@avert"
| "@obviate"
| "@quell"
| "@disrupt"
)
}
// TODO:
// - optimize jump(1)-label(2)-...instr-label(1) into ...instr-jump(2)
// - shorten temporary variable names
/// Tries to merge the condition in an `op` into the `jump` itself
pub fn optimize_jump_op(program: MindustryProgram) -> MindustryProgram {
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
let mut res = MindustryProgram::new();
let instructions = program.0;
for (index, instruction) in instructions.iter().enumerate() {
match instruction {
MindustryOperation::JumpIf(label, operator, lhs, rhs) => {
let (truthiness, var_name) = match (
operator,
replace_constants(lhs.clone()),
replace_constants(rhs.clone()),
) {
(Operator::Neq, Operand::Variable(var_name), Operand::Integer(0))
| (Operator::Eq, Operand::Variable(var_name), Operand::Integer(1))
| (Operator::Neq, Operand::Integer(0), Operand::Variable(var_name))
| (Operator::Eq, Operand::Integer(1), Operand::Variable(var_name)) => {
(true, var_name)
}
(Operator::Eq, Operand::Variable(var_name), Operand::Integer(0))
| (Operator::Neq, Operand::Variable(var_name), Operand::Integer(1))
| (Operator::Eq, Operand::Integer(0), Operand::Variable(var_name))
| (Operator::Neq, Operand::Integer(1), Operand::Variable(var_name)) => {
(false, var_name)
}
_ => {
res.push(instruction.clone());
continue;
}
};
if !tmp_regex.is_match(&var_name) {
res.push(instruction.clone());
continue;
}
let mut last_op = None;
for prev_instruction in instructions[0..=index].iter().rev().skip(1) {
match prev_instruction {
MindustryOperation::Operator(name, operator, lhs, rhs)
if *name == var_name && is_condition_op(*operator) =>
{
last_op = Some((*operator, lhs.clone(), rhs.clone()));
}
MindustryOperation::JumpLabel(_) => break,
_ => {}
}
}
let Some(last_op) = last_op else {
res.push(instruction.clone());
continue
};
let (operator, lhs, rhs) = if truthiness {
last_op
} else {
(
match last_op.0 {
Operator::Gt => Operator::Lte,
Operator::Lt => Operator::Gte,
Operator::Gte => Operator::Lt,
Operator::Lte => Operator::Gt,
Operator::Eq => Operator::Neq,
Operator::Neq => Operator::Eq,
_ => unreachable!(),
},
last_op.1,
last_op.2,
)
};
res.push(MindustryOperation::JumpIf(
label.clone(),
operator,
lhs,
rhs,
));
}
_ => {
res.push(instruction.clone());
}
}
}
return optimize_dead_code(res);
fn replace_constants(value: Operand) -> Operand {
if let Operand::Variable(var) = &value {
match var.as_str() {
"true" => Operand::Integer(1),
"false" | "null" => Operand::Integer(0),
_ => value,
}
} else {
value
}
}
fn is_condition_op(op: Operator) -> bool {
matches!(
op,
Operator::Neq
| Operator::Eq
| Operator::Lt
| Operator::Lte
| Operator::Gt
| Operator::Gte
)
}
}
/// Tries to remove unnecessary `jump always` instructions
pub fn optimize_jump_always(mut program: MindustryProgram) -> MindustryProgram {
let instructions = &mut program.0;
let mut substitutions = Vec::new();
// Detect `label`-`jump always` pairs
for (index, instruction) in instructions.iter().enumerate() {
let MindustryOperation::JumpLabel(label_from) = instruction else {
continue
};
for future_instruction in instructions[index..].iter() {
match future_instruction {
MindustryOperation::JumpLabel(_) => {}
MindustryOperation::Jump(label_to) => {
substitutions.push((label_from.clone(), label_to.clone()));
break;
}
_ => break,
}
}
}
// Apply transitivity to the pairs
let substitutions = substitutions
.iter()
.map(|(from, to)| {
let mut new_to = to;
let mut history = vec![to];
loop {
let mut found = false;
for (other_from, other_to) in substitutions.iter() {
if other_from == new_to {
// Leave cycles untouched
if history.contains(&other_to) {
return (from.clone(), to.clone());
}
new_to = other_to;
history.push(other_to);
found = true;
break;
}
}
if !found {
break;
}
}
(from.clone(), to.clone())
})
.collect::<Vec<_>>();
for instruction in instructions.iter_mut() {
match instruction {
MindustryOperation::Jump(label) => {
if let Some((_, new_label)) = substitutions.iter().find(|(from, _)| from == label) {
*label = new_label.clone();
}
}
MindustryOperation::JumpIf(label, _, _, _) => {
if let Some((_, new_label)) = substitutions.iter().find(|(from, _)| from == label) {
*label = new_label.clone();
}
}
_ => {}
}
}
optimize_dead_code(program)
}
fn optimize_dead_code(program: MindustryProgram) -> MindustryProgram {
let instructions = program.0;
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
let label_regex = Regex::new(r"__label_[0-9]+").unwrap();
let mut res = MindustryProgram::new();
let mut needed_vars = HashSet::new();
let mut needed_labels = HashSet::new();
let mut push_var = |operand: &Operand| match operand {
Operand::Variable(name) => {
needed_vars.insert(name.clone());
}
_ => {}
};
for instruction in instructions.iter() {
match instruction {
MindustryOperation::JumpLabel(_) => {}
MindustryOperation::Jump(label) => {
needed_labels.insert(label.clone());
}
MindustryOperation::JumpIf(label, _, lhs, rhs) => {
needed_labels.insert(label.clone());
push_var(lhs);
push_var(rhs);
}
MindustryOperation::Operator(_, _, lhs, rhs) => {
push_var(lhs);
push_var(rhs);
}
MindustryOperation::UnaryOperator(_, _, value) => {
push_var(value);
}
MindustryOperation::Set(_, value) => {
push_var(value);
}
MindustryOperation::Generic(_, values) => {
values.iter().for_each(&mut push_var);
}
}
}
// Remove unneeded `set`s and `op`s
for instruction in instructions.iter() {
match instruction {
MindustryOperation::Set(name, _) | MindustryOperation::Operator(name, _, _, _) => {
if tmp_regex.is_match(name) {
if needed_vars.contains(name) {
res.push(instruction.clone());
}
// else don't push
} else {
res.push(instruction.clone());
}
}
MindustryOperation::JumpLabel(label) => {
if label_regex.is_match(label) {
if needed_labels.contains(label) {
res.push(instruction.clone());
}
// else don't push
} else {
res.push(instruction.clone());
}
}
_ => {
res.push(instruction.clone());
continue;
}
};
}
res
}

@ -0,0 +1,129 @@
use super::*;
/// Optimizes away unnecessary `sets`
pub fn optimize_constant(program: MindustryProgram) -> MindustryProgram {
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
// Returns true if the variable is safe to substitute into an operand
let is_safe_variable = |name: &str| -> bool {
if matches!(name, "@this" | "@thisx" | "@thisy" | "@links") || is_unit_constant(name) {
return true;
}
if name.starts_with('@') {
false
} else {
true
}
};
let res = replace_if(program, |instructions, instruction, use_index| {
let optimizable_operands = instruction
.operands()
.iter()
.filter_map(|operand| match operand {
Operand::Variable(name) if tmp_regex.is_match(name) => Some(name),
_ => None,
})
// PERF: check when it would be better to deduplicate operands
// .collect::<HashSet<_>>()
// .into_iter()
.filter_map(|name| {
lookbehind(instructions, use_index, |instr| {
match instr {
MindustryOperation::Set(set_name, value) if set_name == name => {
Lookaround::Stop((name.clone(), value.clone()))
}
MindustryOperation::Operator(op_name, _op, _lhs, _rhs)
if op_name == name =>
{
Lookaround::Abort
}
MindustryOperation::JumpLabel(_label) => {
// Note: jump labels mark boundaries for constants. For instance:
// ```
// set __tmp_1 "this is not a constant"
// jump_label:
// set __tmp_2 "this is a constant"
// op add result __tmp_1 __tmp_2
// ```
//
// gets optimized to:
// ```
// set __tmp_1 "this is not a constant"
// jump_label:
// op add result __tmp_1 "this is a constant"
// ```
//
// A more complex algorithm could be used to check the flow of the program,
// but this usecase isn't needed yet.
Lookaround::Abort
}
_ => Lookaround::Continue,
}
})
.map(|(index, (name, value))| (name, value, index))
})
.filter(|(_name, value, set_index)| {
// Don't optimize operands that refer to a mutating variable (either mutable @-variables or instructions that get updated in-between)
if let Operand::Variable(assigned_var) = value {
if !is_safe_variable(&assigned_var) {
return false;
}
for instr_between in &instructions[*set_index..use_index] {
if instr_between.mutates(&assigned_var) {
return false;
}
}
true
} else {
true
}
})
.map(|(name, value, _index)| (name, value))
.collect::<Vec<_>>();
if optimizable_operands.len() == 0 {
return None
}
let mut instruction = instruction.clone();
for operand in instruction.operands_mut() {
if let Operand::Variable(use_name) = operand {
if let Some((_, optimized_into)) = optimizable_operands
.iter()
.find(|(set_name, _)| set_name == use_name)
{
*operand = optimized_into.clone();
}
}
}
Some(vec![instruction])
});
optimize_dead_code(res)
}
// TODO: add serpulo units
fn is_unit_constant(name: &str) -> bool {
matches!(
name,
"@stell"
| "@locus"
| "@precept"
| "@vanquish"
| "@conquer"
| "@merui"
| "@cleroi"
| "@anthicus"
| "@tecta"
| "@collaris"
| "@elude"
| "@avert"
| "@obviate"
| "@quell"
| "@disrupt"
)
}

@ -0,0 +1,74 @@
use super::*;
pub(crate) fn optimize_dead_code(program: MindustryProgram) -> MindustryProgram {
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
let label_regex = Regex::new(r"__label_[0-9]+").unwrap();
let mut needed_vars = HashSet::new();
let mut needed_labels = HashSet::new();
let mut push_var = |operand: &Operand| match operand {
Operand::Variable(name) => {
needed_vars.insert(name.clone());
}
_ => {}
};
for instruction in program.0.iter() {
match instruction {
MindustryOperation::JumpLabel(_) => {}
MindustryOperation::Jump(label) => {
needed_labels.insert(label.clone());
}
MindustryOperation::JumpIf(label, _, lhs, rhs) => {
needed_labels.insert(label.clone());
push_var(lhs);
push_var(rhs);
}
MindustryOperation::Operator(_, _, lhs, rhs) => {
push_var(lhs);
push_var(rhs);
}
MindustryOperation::UnaryOperator(_, _, value) => {
push_var(value);
}
MindustryOperation::Set(_, value) => {
push_var(value);
}
MindustryOperation::Generic(_, values) => {
values.iter().for_each(&mut push_var);
}
MindustryOperation::GenericMut(_, _, values) => {
values.iter().for_each(&mut push_var);
}
}
}
// Remove unneeded `set`s and `op`s
replace_if(program, |_instructions, instruction, _index| {
match instruction {
MindustryOperation::Set(name, _) | MindustryOperation::Operator(name, _, _, _) => {
if !tmp_regex.is_match(name) {
return None
}
if needed_vars.contains(name) {
return None
}
return Some(vec![])
}
MindustryOperation::JumpLabel(label) => {
if !label_regex.is_match(label) {
return None
}
if needed_labels.contains(label) {
return None
}
return Some(vec![])
}
_ => None
}
})
}

@ -0,0 +1,196 @@
use super::*;
/// Tries to merge the condition in an `op` into the `jump` itself
pub fn optimize_jump_op(program: MindustryProgram) -> MindustryProgram {
let tmp_regex = Regex::new(r"__tmp_[0-9]+$").unwrap();
let mut res = MindustryProgram::new();
let instructions = program.0;
for (index, instruction) in instructions.iter().enumerate() {
match instruction {
MindustryOperation::JumpIf(label, operator, lhs, rhs) => {
let (truthiness, var_name) = match (
operator,
replace_constants(lhs.clone()),
replace_constants(rhs.clone()),
) {
(Operator::Neq, Operand::Variable(var_name), Operand::Integer(0))
| (Operator::Eq, Operand::Variable(var_name), Operand::Integer(1))
| (Operator::Neq, Operand::Integer(0), Operand::Variable(var_name))
| (Operator::Eq, Operand::Integer(1), Operand::Variable(var_name)) => {
(true, var_name)
}
(Operator::Eq, Operand::Variable(var_name), Operand::Integer(0))
| (Operator::Neq, Operand::Variable(var_name), Operand::Integer(1))
| (Operator::Eq, Operand::Integer(0), Operand::Variable(var_name))
| (Operator::Neq, Operand::Integer(1), Operand::Variable(var_name)) => {
(false, var_name)
}
_ => {
res.push(instruction.clone());
continue;
}
};
if !tmp_regex.is_match(&var_name) {
res.push(instruction.clone());
continue;
}
// Find the last operation defining `var_name`
let Some((_, last_op)) =
lookbehind(
&*instructions,
index,
|prev_instruction| match prev_instruction {
MindustryOperation::Operator(name, operator, lhs, rhs)
if *name == var_name && is_condition_op(*operator) =>
{
Lookaround::Stop((*operator, lhs.clone(), rhs.clone()))
}
MindustryOperation::JumpLabel(_) => Lookaround::Abort,
x if x.mutates(&var_name) => Lookaround::Abort,
_ => Lookaround::Continue,
},
)
else {
res.push(instruction.clone());
continue;
};
let (operator, lhs, rhs) = if truthiness {
last_op
} else {
(
match last_op.0 {
Operator::Gt => Operator::Lte,
Operator::Lt => Operator::Gte,
Operator::Gte => Operator::Lt,
Operator::Lte => Operator::Gt,
Operator::Eq => Operator::Neq,
Operator::Neq => Operator::Eq,
_ => unreachable!(),
},
last_op.1,
last_op.2,
)
};
res.push(MindustryOperation::JumpIf(
label.clone(),
operator,
lhs,
rhs,
));
}
_ => {
res.push(instruction.clone());
}
}
}
return optimize_dead_code(res);
fn replace_constants(value: Operand) -> Operand {
if let Operand::Variable(var) = &value {
match var.as_str() {
"true" => Operand::Integer(1),
"false" | "null" => Operand::Integer(0),
_ => value,
}
} else {
value
}
}
fn is_condition_op(op: Operator) -> bool {
matches!(
op,
Operator::Neq
| Operator::Eq
| Operator::Lt
| Operator::Lte
| Operator::Gt
| Operator::Gte
)
}
}
/// Tries to remove unnecessary `jump always` instructions
pub fn optimize_jump_always(mut program: MindustryProgram) -> MindustryProgram {
let instructions = &mut program.0;
let mut substitutions = Vec::new();
// Detect `label`-`jump always` pairs
for (index, instruction) in instructions.iter().enumerate() {
let MindustryOperation::JumpLabel(label_from) = instruction else {
continue;
};
if let Some((_, label_to)) =
lookahead(
&*instructions,
index,
|future_instruction| match future_instruction {
MindustryOperation::JumpLabel(_) => Lookaround::Continue,
MindustryOperation::Jump(label_to) => Lookaround::Stop(label_to),
_ => Lookaround::Abort,
},
)
{
substitutions.push((label_from.clone(), label_to.clone()));
}
}
// Apply transitivity to the pairs
let substitutions = substitutions
.iter()
.map(|(from, to)| {
let mut new_to = to;
let mut history = vec![to];
loop {
let mut found = false;
for (other_from, other_to) in substitutions.iter() {
if other_from == new_to {
// Leave cycles untouched
if history.contains(&other_to) {
return (from.clone(), to.clone());
}
new_to = other_to;
history.push(other_to);
found = true;
break;
}
}
if !found {
break;
}
}
(from.clone(), to.clone())
})
.collect::<Vec<_>>();
for instruction in instructions.iter_mut() {
match instruction {
MindustryOperation::Jump(label) => {
if let Some((_, new_label)) = substitutions.iter().find(|(from, _)| from == label) {
*label = new_label.clone();
}
}
MindustryOperation::JumpIf(label, _, _, _) => {
if let Some((_, new_label)) = substitutions.iter().find(|(from, _)| from == label) {
*label = new_label.clone();
}
}
_ => {}
}
}
optimize_dead_code(program)
}

@ -0,0 +1,54 @@
//! Lookaround helpers for quickly implementing optimizations
use super::*;
pub(crate) enum Lookaround<T> {
Stop(T),
Abort,
Continue,
}
pub(crate) fn lookahead<'a, I, F, T>(
instructions: I,
from: usize,
mut callback: F,
) -> Option<(usize, T)>
where
I: IntoIterator<Item = &'a MindustryOperation>,
F: FnMut(&'a MindustryOperation) -> Lookaround<T>,
{
for (index, instruction) in instructions.into_iter().enumerate().skip(from) {
match callback(instruction) {
Lookaround::Stop(value) => {
return Some((index, value));
}
Lookaround::Abort => return None,
Lookaround::Continue => {}
}
}
None
}
pub(crate) fn lookbehind<'a, I, F, T>(
instructions: I,
from: usize,
mut callback: F,
) -> Option<(usize, T)>
where
I: IntoIterator<Item = &'a MindustryOperation>,
<I as IntoIterator>::IntoIter: ExactSizeIterator + DoubleEndedIterator,
F: FnMut(&'a MindustryOperation) -> Lookaround<T>,
{
for (index, instruction) in instructions.into_iter().enumerate().take(from).rev() {
match callback(instruction) {
Lookaround::Stop(value) => {
return Some((index, value));
}
Lookaround::Abort => return None,
Lookaround::Continue => {}
}
}
None
}

@ -0,0 +1,24 @@
use std::collections::HashSet;
use super::*;
mod constant;
pub use constant::*;
mod jump;
pub use jump::*;
mod dead;
pub(crate) use dead::optimize_dead_code;
mod lookaround;
pub(crate) use lookaround::*;
mod replace;
pub(crate) use replace::*;
// TODO:
// - optimize jump(1)-label(2)-...instr-label(1) into ...instr-jump(2)
// - shorten temporary variable names
// - jump normalization
// - variable normalization

@ -0,0 +1,22 @@
use super::*;
pub(crate) fn replace_if<F>(program: MindustryProgram, callback: F) -> MindustryProgram
where
F: for<'c> Fn(&'c Vec<MindustryOperation>, &'c MindustryOperation, usize) -> Option<Vec<MindustryOperation>>
{
let mut res = MindustryProgram::new();
for (index, instruction) in program.0.iter().enumerate() {
match callback(&program.0, instruction, index) {
Some(mut to_replace) => {
res.0.append(&mut to_replace);
}
None => {
res.push(instruction.clone());
}
}
}
res
}

@ -1,6 +1,6 @@
use basic_to_mindustry::{
common::Config,
compile::{optimize_jump_always, optimize_jump_op, optimize_set_use, translate_ast, Namer},
compile::{optimize_constant, optimize_jump_always, optimize_jump_op, translate_ast, Namer},
parse::{build_ast, tokenize},
};
@ -13,9 +13,9 @@ fn main() {
let parsed = build_ast(&tokens, &config).unwrap();
let transformed = translate_ast(&parsed, &mut Namer::default(), &config);
// println!("{}", transformed);
println!("{}", transformed);
let optimized = optimize_set_use(transformed);
let optimized = optimize_constant(transformed);
let optimized = optimize_jump_op(optimized);
let optimized = optimize_jump_always(optimized);

@ -189,7 +189,15 @@ pub fn build_ast(tokens: &[BasicToken], config: &Config) -> Result<BasicAstBlock
}
let lowercase_fn_name = fn_name.to_lowercase();
if let Some((_, n_args)) = config.builtin_functions.get(&lowercase_fn_name) {
if let Some((_, mutating, n_args)) =
config.builtin_functions.get(&lowercase_fn_name)
{
if *mutating {
let BasicAstExpression::Variable(_) = &arguments[0] else {
return Err(ParseError::ExpectedVariable);
};
}
if arguments.len() != *n_args {
return Err(ParseError::InvalidArgumentCount(
lowercase_fn_name,

@ -6,3 +6,13 @@ pub use ast::*;
#[cfg(test)]
mod test;
#[derive(PartialEq, Clone, Debug)]
pub enum ParseError {
InvalidToken(String),
UnexpectedToken(BasicToken),
MissingToken(BasicToken),
InvalidArgumentCount(String, usize, usize),
ExpectedVariable,
ExpectedOperand,
}

@ -1,3 +1,4 @@
use super::ParseError;
use crate::common::*;
use regex::Regex;
@ -22,15 +23,6 @@ pub enum BasicToken {
Operator(Operator),
}
#[derive(PartialEq, Clone, Debug)]
pub enum ParseError {
InvalidToken(String),
UnexpectedToken(BasicToken),
MissingToken(BasicToken),
InvalidArgumentCount(String, usize, usize),
ExpectedOperand,
}
/// Transforms a raw string into a sequence of `BasicToken`s
pub fn tokenize(raw: &str) -> Result<Vec<BasicToken>, ParseError> {
macro_rules! match_token {

@ -1,14 +1,17 @@
use std::path::Path;
#![feature(fs_try_exists)]
use std::path::{Path, PathBuf};
use basic_to_mindustry::common::Config;
use basic_to_mindustry::compile::{optimize_set_use, translate_ast};
use basic_to_mindustry::compile::{optimize_constant, translate_ast, optimize_jump_op, optimize_jump_always};
use basic_to_mindustry::parse::{build_ast, tokenize};
#[test]
fn test_examples() {
let config = Config::default();
for entry in Path::new("./examples/").read_dir().unwrap() {
let Ok(entry) = entry else { continue };
fn read_basic_examples() -> impl Iterator<Item=(String, String)> {
Path::new("./examples/").read_dir().unwrap().filter_map(|entry| {
let Ok(entry) = entry else {
return None;
};
if entry
.file_name()
.into_string()
@ -19,17 +22,67 @@ fn test_examples() {
let file = std::fs::read_to_string(entry.path()).unwrap_or_else(|e| {
panic!("Error opening {:?}: {:?}", file_name, e);
});
Some((file_name, file))
} else {
None
}
})
}
let tokenized = tokenize(&file).unwrap_or_else(|e| {
panic!("Error tokenizing {:?}: {:?}", file_name, e);
});
let parsed = build_ast(&tokenized, &config).unwrap_or_else(|e| {
panic!("Error parsing {:?}: {:?}", file_name, e);
});
let translated = translate_ast(&parsed, &mut Default::default(), &config);
let optimized = optimize_set_use(translated);
#[test]
fn test_examples() {
let config = Config::default();
for (file_name, file) in read_basic_examples() {
let tokenized = tokenize(&file).unwrap_or_else(|e| {
panic!("Error tokenizing {:?}: {:?}", file_name, e);
});
let parsed = build_ast(&tokenized, &config).unwrap_or_else(|e| {
panic!("Error parsing {:?}: {:?}", file_name, e);
});
let translated = translate_ast(&parsed, &mut Default::default(), &config);
let optimized = optimize_constant(translated);
let _ = optimized;
let _ = optimized;
}
}
// TODO: implement proper equality of `MindustryProgram`s and parse the expected results instead
#[test]
fn test_examples_opt() {
let config = Config::default();
for (file_name, file) in read_basic_examples() {
let Some(program_name) = PathBuf::from(file_name.clone()).file_stem().and_then(|stem| stem.to_str()).map(|s| s.to_string()) else {
panic!("Basic program in examples/ has an invalid filename: {}", file_name);
};
let opt_0 = format!("tests/examples/{}.0.mlog", program_name);
if !std::fs::try_exists(&opt_0).unwrap() {
continue
}
let opt_0 = std::fs::read_to_string(opt_0).unwrap_or_else(|e| {
panic!("Couldn't open tests/examples/{}.0.mlog: {:?}", program_name, e);
});
let tokenized = tokenize(&file).unwrap_or_else(|e| {
panic!("Error tokenizing {:?}: {:?}", file_name, e);
});
let parsed = build_ast(&tokenized, &config).unwrap_or_else(|e| {
panic!("Error parsing {:?}: {:?}", file_name, e);
});
let translated = translate_ast(&parsed, &mut Default::default(), &config);
pretty_assertions::assert_eq!(opt_0.trim(), format!("{}", translated).trim());
let optimized = optimize_jump_always(optimize_jump_op(optimize_constant(translated)));
let opt_1 = std::fs::read_to_string(format!("tests/examples/{}.1.mlog", program_name)).unwrap_or_else(|e| {
panic!("Couldn't open tests/examples/{}.1.mlog: {:?}", program_name, e);
});
pretty_assertions::assert_eq!(opt_1.trim(), format!("{}", optimized).trim());
}
}

@ -0,0 +1,6 @@
set X 0
line__20:
set main__tmp_0 X
set main__tmp_1 1
op add X main__tmp_0 main__tmp_1
jump line__20 always 0 0

@ -0,0 +1,4 @@
set X 0
line__20:
op add X X 1
jump line__20 always 0 0

@ -0,0 +1,43 @@
set x 0
start:
set main__tmp_0 x
set main__tmp_1 1
op add x main__tmp_0 main__tmp_1
set main__tmp_5 x
set main__tmp_6 3
op mod main__tmp_3 main__tmp_5 main__tmp_6
set main__tmp_4 0
op equal main__tmp_2 main__tmp_3 main__tmp_4
jump main__label_0_else notEqual main__tmp_2 true
set main__tmp_10 x
set main__tmp_11 5
op mod main__tmp_8 main__tmp_10 main__tmp_11
set main__tmp_9 0
op equal main__tmp_7 main__tmp_8 main__tmp_9
jump main__label_2_else notEqual main__tmp_7 true
set main__tmp_12 "fizzbuzz"
print main__tmp_12
jump main__label_3_endif always 0 0
main__label_2_else:
set main__tmp_13 "fizz"
print main__tmp_13
main__label_3_endif:
jump main__label_1_endif always 0 0
main__label_0_else:
set main__tmp_17 x
set main__tmp_18 5
op mod main__tmp_15 main__tmp_17 main__tmp_18
set main__tmp_16 0
op equal main__tmp_14 main__tmp_15 main__tmp_16
jump main__label_4_else notEqual main__tmp_14 true
set main__tmp_19 "buzz"
print main__tmp_19
jump main__label_5_endif always 0 0
main__label_4_else:
set main__tmp_20 x
print main__tmp_20
main__label_5_endif:
main__label_1_endif:
set main__tmp_21 message1
printflush main__tmp_21
jump start always 0 0

@ -0,0 +1,23 @@
set x 0
start:
op add x x 1
op mod main__tmp_3 x 3
jump main__label_0_else notEqual main__tmp_3 0
op mod main__tmp_8 x 5
jump main__label_2_else notEqual main__tmp_8 0
print "fizzbuzz"
jump main__label_1_endif always 0 0
main__label_2_else:
print "fizz"
jump main__label_1_endif always 0 0
main__label_0_else:
op mod main__tmp_15 x 5
jump main__label_4_else notEqual main__tmp_15 0
print "buzz"
jump main__label_5_endif always 0 0
main__label_4_else:
print x
main__label_5_endif:
main__label_1_endif:
printflush message1
jump start always 0 0
Loading…
Cancel
Save