Skip to content

Commit

Permalink
Constraints (#84)
Browse files Browse the repository at this point in the history
* switch strop to `split` branch, upgrade mos6502

* break apart huge method in BruteForce

* add peephole checker example

* a few convenience methods on z80::Insn

* experiment on constraining search

* remove vestiges

* it's better if constraints are able to report why they're firing

* move Constrain to the func and subroutine

* BruteForce now says how many iterations it's done

* Some instructions just can't occur in a function

* constraints tweak

* better way to get return value out of emulator

* add live_out function to SdccCall1GetReturnValue trait

* better names for sdcccall(1) param & return value traits

* better way to disallow opcodes from sdccall1 functions

* clippy & fmt

* primitive register pair dataflow

* dataflow analysis considers live-out registers to improve runtime

* correction to function in opt example

* add report example

* implement basic peephole optimization

* example: more stupid code in static analysis example

* generate another function in gen: example

* more peephole optimizations and dataflow analysis

* the peephole example shows where the peephole is failing

* peephole example helps develop the peephole optimizers

* more peephole opts

* better internal API

* tidy up

---------

Co-authored-by: Sam M W <you@example.com>
  • Loading branch information
omarandlorraine and Sam M W authored Dec 14, 2024
1 parent 2fe81c8 commit 6589e8c
Show file tree
Hide file tree
Showing 27 changed files with 1,201 additions and 878 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ To see what strop could be used for:

* [opt](examples/opt.rs) optimizes an existing machine code function
* [gen](examples/gen.rs) generates a Z80 function matching the Rust function, after a fashion compiling Rust to Z80.
* [peephole](examples/peephole.rs) discovers lacunae in the peephole optimizers and other constraints

### Supported instruction sets:

Expand Down
36 changes: 27 additions & 9 deletions examples/gen.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
//! An example of a program that uses strop to optimize an existing function.
use strop::z80::SdccCall1;
use strop::BruteForce;
use strop::Disassemble;
use strop::Iterable;
use strop::StropError;

/*
fn zero(_hex: u8) -> Result<u8, StropError> {
Ok(b'0')
}
*/

fn target_function(hex: u8) -> Result<u8, StropError> {
fn dec_to_hex(hex: u8) -> Result<u8, StropError> {
match hex {
0x0 => Ok(b'0'),
0x1 => Ok(b'1'),
Expand All @@ -35,14 +31,36 @@ fn target_function(hex: u8) -> Result<u8, StropError> {
}

fn main() {
let target_function = target_function as fn(u8) -> Result<u8, StropError>;
let target_function = zero as fn(u8) -> Result<u8, StropError>;

// you can do a bruteforce search for Z80 machine code programs implementing the same function
let mut bruteforce: BruteForce<_, _, _, SdccCall1> =
strop::BruteForce::new(target_function, SdccCall1::first());
let mut bruteforce = SdccCall1::<u8, u8>::new()
// By specifying that we want a pure function, and that the function is a leaf function, we
// can constrain the search space even further
.pure()
.leaf()
.bruteforce(target_function);

let bf = bruteforce.search().unwrap();

println!("An equivalent subroutine we found by bruteforce search:");
println!("An equivalent subroutine we found by bruteforce search,");
println!("after {} iterations.", bruteforce.count);
bf.dasm();

let target_function = dec_to_hex as fn(u8) -> Result<u8, StropError>;

// you can do a bruteforce search for Z80 machine code programs implementing the same function
let mut bruteforce = SdccCall1::<u8, u8>::new()
// By specifying that we want a pure function, and that the function is a leaf function, we
// can constrain the search space even further
.pure()
.leaf()
.bruteforce(target_function)
.trace();

let bf = bruteforce.search().unwrap();

println!("An equivalent subroutine we found by bruteforce search,");
println!("after {} iterations.", bruteforce.count);
bf.dasm();
}
9 changes: 4 additions & 5 deletions examples/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use strop::Callable;
use strop::Disassemble;
use strop::Iterable;

fn target_function() -> SdccCall1 {
fn target_function() -> SdccCall1<u16, u16> {
// Construct some machine code.
//
// In a real world scenario maybe you'd read this in from assembly or something, but you can
Expand All @@ -19,15 +19,13 @@ fn target_function() -> SdccCall1 {
// This is not a terribly efficient way to encode this program; you can save a byte and some
// time with `LD HL, 7F40H` instead (a single 16-bit bit immediate load is more efficient than two
// individual 8-bit loads) -- let's see if strop figures this out!
//
// When building a SdccCall1 callable we leave off the terminating RET instruction since when
// SdccCall1 builds, it adds one

use strop::Goto;

let mc = [
Insn::new(&[0x26, 0x40]), // LD H,40H
Insn::new(&[0x2e, 0x7f]), // LD L,7FH
Insn::new(&[0xc9]), // RET
];

// This machine code is callable using the sdcccall(1) calling convention.
Expand All @@ -38,6 +36,7 @@ fn target_function() -> SdccCall1 {

fn main() {
use strop::Iterable;

let c = target_function();

// you can call this function in a few different ways
Expand All @@ -48,7 +47,7 @@ fn main() {
c.dasm();

// you can do a bruteforce search for Z80 machine code programs implementing the same function
let mut bruteforce: BruteForce<u16, u16, SdccCall1, _> =
let mut bruteforce: BruteForce<u16, u16, SdccCall1<u16, u16>, _, _> =
strop::BruteForce::new(c, SdccCall1::first());

let bf = bruteforce.search().unwrap();
Expand Down
50 changes: 50 additions & 0 deletions examples/peephole.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use strop::Disassemble;
use strop::StropError;

fn zero(_nothing: u8) -> Result<u8, StropError> {
Ok(b'0')
}

fn sdcccall1_search(target_function: fn(u8) -> Result<u8, StropError>) {
use strop::z80::SdccCall1;

let target_function = target_function as fn(u8) -> Result<u8, StropError>;

// a bruteforce search for Z80 machine code programs implementing the function
let mut bruteforce = SdccCall1::<u8, u8>::new()
// By specifying that we want a pure function, and that the function is a leaf function, we
// can constrain the search space even further
.pure()
.leaf()
.bruteforce(target_function);

// let's find the first program that implements the function!
let first = bruteforce.search().unwrap();

println!("found first:");
first.dasm();

let mut count = 0usize;

// let's find more programs that are equivalent. I'm expecting these to have some
// inefficiencies, which will point out deficiencies in the peephole optimizers and dataflow
// analysis.
loop {
let second = bruteforce.search().unwrap();

if count == 1 {
println!(
"I've discovered two or more programs that are equivalent. One's going to have dead code"
);
println!("or some other inefficency.");
}

println!("number {count}:");
second.dasm();
count += 1;
}
}

fn main() {
sdcccall1_search(zero);
}
129 changes: 89 additions & 40 deletions src/bruteforce.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
use crate::test;
use crate::test::Vals;
use crate::Callable;
use crate::Constrain;
use crate::Iterable;
use crate::StropError;

/// Performs a brute force search over a given search space `U`
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct BruteForce<
InputParameters,
ReturnValue,
T: Callable<InputParameters, ReturnValue>,
ReturnValue: Clone,
T: Callable<InputParameters, ReturnValue> + Clone,
U: Callable<InputParameters, ReturnValue> + Iterable,
Insn: Clone,
> {
target_function: T,
candidate: U,
tests: Vec<(InputParameters, ReturnValue)>,
input: std::marker::PhantomData<InputParameters>,
ret: std::marker::PhantomData<ReturnValue>,
insn: std::marker::PhantomData<Insn>,
trace_enable: bool,

/// Keeps track of how many iterations the bruteforce search has been through.
pub count: usize,
}

impl<
Insn: Clone,
InputParameters: Copy + Vals,
ReturnValue: Vals + std::cmp::PartialEq,
T: Callable<InputParameters, ReturnValue>,
U: Callable<InputParameters, ReturnValue> + Iterable + Clone,
> BruteForce<InputParameters, ReturnValue, T, U>
ReturnValue: Vals + std::cmp::PartialEq + Clone,
T: Callable<InputParameters, ReturnValue> + Clone,
U: Callable<InputParameters, ReturnValue>
+ Iterable
+ Clone
+ crate::Disassemble
+ Constrain<Insn>,
> BruteForce<InputParameters, ReturnValue, T, U, Insn>
{
/// Constructs a new `BruteForce`
pub fn new(target_function: T, initial_candidate: U) -> Self {
Expand All @@ -36,46 +48,83 @@ impl<
tests,
input: std::marker::PhantomData,
ret: std::marker::PhantomData,
insn: std::marker::PhantomData,
trace_enable: false,
count: 0,
}
}

/// Returns the next function that matches the target function
pub fn search(&mut self) -> Option<U> {
loop {
if !self.candidate.step() {
return None;
/// Enables trace: disassembles each candidate to stdout.
pub fn trace(&mut self) -> Self {
self.trace_enable = true;
self.clone()
}

/// Returns the candidate currently under consideration
pub fn candidate(&self) -> &U {
&self.candidate
}

/// Prints the current candidate to stdout
pub fn dasm(&self) {
println!("\ncandidate{}:", self.count);
self.candidate().dasm();
}

/// Advances the candidate to the next position in the search space
pub fn step(&mut self) -> bool {
self.count += 1;
if !self.candidate.step() {
if self.trace_enable {
self.dasm();
}
self.candidate.fixup();
return false;
}
if self.trace_enable {
self.dasm();
}
true
}

match test::passes(&self.candidate, &self.tests) {
Err(StropError::DidntReturn) => {
// The candidate does not pass the test case(s)
// go round the loop again
}
Err(StropError::Undefined) => {
// The candidate does not pass the test case(s)
// go round the loop again
}
Ok(false) => {
// The candidate does not pass the test case(s)
// go round the loop again
}
Ok(true) => {
// Found a candidate which passes all known test cases.
// Let's fuzz test the candidate
if let Some(test_case) =
test::fuzz(&self.target_function, &self.candidate, 5000)
{
// We've fuzzed the functions against eachother and found another test case.
// So keep hold of this new test case
self.tests.push(test_case);
} else {
// The candidate passed all known test cases and also a fuzz test, so let's say
// it's good enough and return it
return Some(self.candidate.clone());
}
/// Tests that the candidate matches the target function
pub fn test(&mut self) -> bool {
match test::passes(&self.candidate, &self.tests) {
Err(StropError::DidntReturn) => {
// The candidate does not pass the test case(s)
false
}
Err(StropError::Undefined) => {
// The candidate does not pass the test case(s)
false
}
Ok(false) => {
// The candidate does not pass the test case(s)
false
}
Ok(true) => {
// Found a candidate which passes all known test cases.
// Let's fuzz test the candidate
if let Some(test_case) = test::fuzz(&self.target_function, &self.candidate, 5000) {
// We've fuzzed the functions against eachother and found another test case.
// So keep hold of this new test case
self.tests.push(test_case);
false
} else {
// The candidate passed all known test cases and also a fuzz test, so let's say
// it's good enough and return it
true
}
}
}
}

/// Returns the next function that matches the target function
pub fn search(&mut self) -> Option<U> {
loop {
self.step();
if self.test() {
return Some(self.candidate.clone());
}
}
}
}
Loading

0 comments on commit 6589e8c

Please sign in to comment.