WIP: offchain-start #14

Draft
GlowingOutlaw wants to merge 10 commits from offchain-start into main
6 changed files with 423 additions and 0 deletions
Showing only changes of commit 64a81e4110 - Show all commits
+8
View File
@@ -7,6 +7,14 @@ edition = "2021"
name = "gp_tester" name = "gp_tester"
path = "src/bin/gp_tester.rs" path = "src/bin/gp_tester.rs"
[[bin]]
name = "gp_spec_tester"
path = "src/bin/gp_spec_tester.rs"
[[bin]]
name = "symreg_experiment"
path = "src/bin/symreg_experiment.rs"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
+36
View File
@@ -0,0 +1,36 @@
use anyhow::Result;
use rand::thread_rng;
use rand::Rng;
// Suppose you have these in your library:
use offchain::gp::generate_spec::ranmdom_code_fixed;
use offchain::gp::mutation::mutate_by_index;
use offchain::compiler::ast::UntypedAst;
fn main() -> Result<()> {
let mut rng = thread_rng();
// We'll produce, say, 3 random ASTs ("subjects").
let num_subjects = 3;
let max_points = 6;
for i in 0..num_subjects {
// Generate a random AST
let original = ranmdom_code_fixed(&mut rng, max_points);
println!("=== Subject #{} ===", i);
println!("Original AST:\n{:#?}", original);
// Let's do 2 successive mutations on it
let mut prev = original.clone();
for mut_idx in 0..2 {
let mutated = mutate_by_index(&prev, &mut rng, max_points);
println!("Mutation #{} result:\n{:#?}", mut_idx, mutated);
prev = mutated; // so the next iteration mutates from the last result
}
println!("=== End of Subject #{} ===\n", i);
}
println!("Done generating & mutating random ASTs!");
Ok(())
}
+170
View File
@@ -0,0 +1,170 @@
// src/bin/symreg_experiment.rs
use anyhow::Result;
use rand::{thread_rng, Rng};
use offchain::compiler::ast::{UntypedAst, Push3Ast};
use offchain::helpers::artifact::get_creation_code;
use offchain::runner::revm_runner::{
EvmRunner,
Push3InterpreterInputs,
Push3InterpreterOutputs
};
use offchain::gp::generate_spec::{ranmdom_code_fixed};
use offchain::gp::mutation::mutate_by_index;
use offchain::compiler::push3_describtor::make_sublist_descriptor;
use ethers::types::U256;
use ethers::abi::{encode, Token};
// 1) Generate (x, y) samples for f(x) = 3x^2 + x + 3
fn generate_samples() -> Vec<(i32, i32)> {
let mut samples = Vec::new();
for x in -5..=5 {
let y = 3 * x * x + x + 3;
samples.push((x, y));
}
samples
}
// 2) Evaluate a single AST on a single input `x`, returning the integer
// that ends up on top of the final int stack (or 0 if empty).
fn evaluate_ast_on_x(
runner: &mut EvmRunner,
ast: &UntypedAst,
x: i32,
) -> i32 {
// a) Convert AST => push3 bytecode
let code_bytes = ast.to_bytecode();
let code_len = code_bytes.len() as u32;
// b) Build a sublist descriptor
let descriptor = make_sublist_descriptor(0, code_len);
// c) Provide x in `init_int_stack`
let inputs = Push3InterpreterInputs {
code: code_bytes,
init_code_stack: Vec::new(),
init_exec_stack: vec![descriptor],
init_int_stack: vec![x as i128],
};
// d) Actually run the interpreter
let outputs = match runner.run_interpreter(&inputs) {
Ok(o) => o,
Err(_) => {
// If the call fails or reverts, return 0
return 0;
}
};
// e) If final_int_stack is empty => 0, else top item
if outputs.final_int_stack.is_empty() {
0
} else {
*outputs.final_int_stack.last().unwrap() as i32
}
}
// 3) Evaluate an AST on all (x, y) samples => compute MSE
fn evaluate_fitness(
runner: &mut EvmRunner,
ast: &UntypedAst,
samples: &[(i32, i32)]
) -> f64 {
let mut error_sum = 0.0;
for &(x, target_y) in samples {
let predicted = evaluate_ast_on_x(runner, ast, x);
let diff = (predicted - target_y) as f64;
error_sum += diff * diff;
}
error_sum / samples.len() as f64
}
fn main() -> Result<()> {
// A) Generate samples for f(x)=3x^2 + x + 3
let samples = generate_samples();
// B) Create an ephemeral EVM runner (Push3Interpreter)
let creation_hex_filename = "../onchain/out/Push3Interpreter.sol/Push3Interpreter.json";
let creation_bytes = get_creation_code(creation_hex_filename)?;
let mut runner = EvmRunner::new(creation_bytes)?;
// C) Setup GP parameters
let pop_size = 1000; // population size
let max_points = 15; // max "size" for random code
let generations = 100; // how many generations we run
let early_stop_threshold = 1.0; // if error < this => we can stop
let mut rng = thread_rng();
// D) Initialize random population
let mut population: Vec<UntypedAst> = (0..pop_size)
.map(|_| ranmdom_code_fixed(&mut rng, max_points))
.collect();
// E) Main GP Loop
for gen in 0..generations {
// 1) Evaluate each => store (ast, error)
let mut scored: Vec<(UntypedAst, f64)> = population.into_iter()
.map(|ast| {
let err = evaluate_fitness(&mut runner, &ast, &samples);
(ast, err)
})
.collect();
// 2) Sort ascending by error
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
// 3) Print best error
let best_err = scored[0].1;
println!("Generation {gen}, best error = {best_err}");
// 4) Check if best is below threshold => early stop
if best_err < early_stop_threshold {
println!("Best error < {early_stop_threshold}, stopping early!");
population = scored.into_iter().map(|(a, _)| a).collect();
break;
}
// 5) Keep best half
let half = scored.len() / 2;
let survivors = &scored[..half];
// 6) Refill population: half mutated, half fresh random
let mut new_pop = Vec::with_capacity(scored.len());
// a) keep survivors
for &(ref ast, _) in survivors {
new_pop.push(ast.clone());
}
// b) how many to add?
let remainder_size = pop_size - new_pop.len();
// we do half mutated, half new random
let mutated_count = remainder_size / 2;
let random_count = remainder_size - mutated_count;
// c) add mutated
for _ in 0..mutated_count {
let idx = rng.gen_range(0..half);
let mutated = mutate_by_index(&survivors[idx].0, &mut rng, max_points);
new_pop.push(mutated);
}
// d) add brand-new random
for _ in 0..random_count {
let fresh = ranmdom_code_fixed(&mut rng, max_points);
new_pop.push(fresh);
}
// store for next generation
population = new_pop;
}
// F) Final Print
println!("\n=== Final Population ===");
for (i, ast) in population.iter().enumerate() {
let err = evaluate_fitness(&mut runner, ast, &samples);
println!("Subject #{i}, err={err}, ast={:?}", ast);
}
Ok(())
}
+121
View File
@@ -0,0 +1,121 @@
use crate::compiler::ast::{UntypedAst, OpCode};
use rand::Rng;
use rand::prelude::SliceRandom;
#[derive(Clone, Debug)]
pub enum InstructionAtom {
/// A normal opcode, e.g. `Plus`, `Minus`, `Dup`, etc.
Opcode(OpCode),
/// An ephemeral random constant placeholder.
/// If chosen, we generate a random literal on the spot.
EphemeralInt,
// If you want ephemeral floats, booleans, etc., add more variants
}
/// A small struct to hold our entire “instruction set.”
#[derive(Clone, Debug)]
pub struct InstructionSet {
pub atoms: Vec<InstructionAtom>,
}
impl InstructionSet {
pub fn new_default() -> Self {
// Example: Some opcodes + ephemeral constant
// Tweak as you like
use InstructionAtom::*;
use OpCode::*;
Self {
atoms: vec![
Opcode(Noop),
Opcode(Plus),
Opcode(Minus),
Opcode(Mult),
Opcode(Dup),
Opcode(Pop),
EphemeralInt,
],
}
}
/// Pick a random atom from this set.
/// If it's `EphemeralInt`, we produce `UntypedAst::IntLiteral(...)`.
/// If it's `Opcode(...)`, we produce `UntypedAst::Instruction(...)`.
pub fn random_atom_as_ast(&self, rng: &mut impl Rng) -> UntypedAst {
let idx = rng.gen_range(0..self.atoms.len());
match &self.atoms[idx] {
InstructionAtom::Opcode(op) => UntypedAst::Instruction(op.clone()),
InstructionAtom::EphemeralInt => {
// For ephemeral int, produce a random literal in some range
let val = rng.gen_range(-10..10);
UntypedAst::IntLiteral(val)
}
}
}
}
pub fn ranmdom_code_fixed(rng: &mut impl Rng, max_points: usize) -> UntypedAst {
let instr_set = InstructionSet::new_default();
return random_code(rng, &instr_set, max_points);
}
/// The "entry point": random_code(max_points).
/// 1) Choose actual_points in [1..=max_points].
/// 2) Return the result of random_code_with_size.
pub fn random_code(rng: &mut impl Rng, instr_set: &InstructionSet, max_points: usize) -> UntypedAst {
let actual_points = rng.gen_range(1..=max_points);
random_code_with_size(rng, instr_set, actual_points)
}
/// The main logic:
/// If points == 1 => pick a single instruction/ephemeral
/// Else =>
/// - We "decompose" (points - 1) among sub-codes
/// - For each sub-points, we recursively call random_code_with_size
/// - We return a Sublist of those children, possibly in random order
pub fn random_code_with_size(
rng: &mut impl Rng,
instr_set: &InstructionSet,
points: usize,
) -> UntypedAst {
use UntypedAst::*;
// Base case
if points == 1 {
// Choose a random "atom" from instr_set
return instr_set.random_atom_as_ast(rng);
}
// If `points > 1`, let's produce a Sublist
// We'll break (points - 1) into sub-points via `decompose`
let subpoints_list = decompose(rng, points - 1, points - 1);
// subpoints_list is e.g. [2, 3, 5] and sums to (points-1)
// Then for each "subpoints", we do random_code_with_size
let mut sub_asts: Vec<UntypedAst> = subpoints_list
.into_iter()
.map(|sp| random_code_with_size(rng, instr_set, sp))
.collect();
// The spec says "Return a list containing the results, in random order"
// so let's shuffle sub_asts
sub_asts.shuffle(rng);
Sublist(sub_asts)
}
/// Decompose a number into random parts.
///
/// - If number is 1 or max_parts is 1 => return [number].
/// - Otherwise pick a random split "this_part" and recurse on the remainder.
fn decompose(rng: &mut impl Rng, number: usize, max_parts: usize) -> Vec<usize> {
if number == 1 || max_parts == 1 {
return vec![number];
}
// pick a random "this_part" in [1 .. (number-1)]
let this_part = rng.gen_range(1..number);
let mut remainder = decompose(rng, number - this_part, max_parts - 1);
let mut result = vec![this_part];
result.append(&mut remainder);
result
}
+2
View File
@@ -1,3 +1,5 @@
pub mod population; pub mod population;
pub mod genetic_ops; pub mod genetic_ops;
pub mod generate; pub mod generate;
pub mod generate_spec;
pub mod mutation;
+86
View File
@@ -0,0 +1,86 @@
use rand::Rng;
use crate::compiler::ast::UntypedAst;
use crate::gp::generate_spec::ranmdom_code_fixed; // or push3-based generator
/// A "path" is a list of indices leading from the root to a child.
/// For example, [] is the root, [0] is the root's first child, [0,1] is that child's second child, etc.
pub type Path = Vec<usize>;
/// Enumerate all nodes in a DFS order (root first).
/// For each node, we store the `Path` (list of child indices).
pub fn enum_nodes_dfs(ast: &UntypedAst) -> Vec<Path> {
let mut paths = Vec::new();
dfs_helper(ast, &mut paths, &mut vec![]);
paths
}
/// Recursive helper: `current_path` is a stack we push child indices onto.
/// `paths` is the global list of final results.
fn dfs_helper(ast: &UntypedAst, paths: &mut Vec<Path>, current_path: &mut Vec<usize>) {
// Record the current path as a valid node
paths.push(current_path.clone());
match ast {
UntypedAst::Sublist(children) => {
// For each child, push its index, recurse, then pop
for (i, child) in children.iter().enumerate() {
current_path.push(i);
dfs_helper(child, paths, current_path);
current_path.pop();
}
}
UntypedAst::IntLiteral(_) | UntypedAst::Instruction(_) => {
// Leaf node => no children
}
}
}
/// Replace the node at `path` in `original` with `replacement`, returning a new AST.
/// If `path` is empty => we replace the root entirely.
pub fn replace_subtree(
original: &UntypedAst,
path: &[usize],
replacement: UntypedAst
) -> UntypedAst {
if path.is_empty() {
// means we’re replacing the root
return replacement;
}
// otherwise, we need to walk the sublist chain
match original {
UntypedAst::Sublist(children) => {
// let path_head = path[0], path_tail = path[1..]
let (first_idx, tail_path) = (path[0], &path[1..]);
let mut new_children = children.clone();
new_children[first_idx] = replace_subtree(&children[first_idx], tail_path, replacement);
UntypedAst::Sublist(new_children)
}
// If the path is not empty but we’re not in a Sublist => error or do nothing
UntypedAst::IntLiteral(_) | UntypedAst::Instruction(_) => {
// Possibly, if the path is invalid, we ignore. Or we treat it as "replace anyway."
// Here, we might treat it as an error or a no-op.
original.clone()
}
}
}
/// Produces a new AST by choosing exactly one node in `original` at random
/// (by enumerating them), and replacing it with a new random subtree.
pub fn mutate_by_index(
original: &UntypedAst,
rng: &mut impl Rng,
max_points: usize,
) -> UntypedAst {
// 1) Enumerate all nodes => get a vector of `Path`
let all_paths = enum_nodes_dfs(original);
// pick a random path
let idx = rng.gen_range(0..all_paths.len());
let chosen_path = &all_paths[idx];
// 2) Generate a new subtree with our "fixed" random code generator
let new_subtree = ranmdom_code_fixed(rng, max_points);
// 3) Replace the subtree at `chosen_path` in `original` with `new_subtree`
replace_subtree(original, chosen_path, new_subtree)
}