Files
powdr/openvm/tests/apc_builder.rs
Thibaut Schaeffer 86c31acd9a Make powdr generic over the pgo method (#3188)
Introduce `PgoAdapter` which wraps `Adapter` and can call apc
generation. Removes the match statements over the pgo method. Breaking
change. Now the client can pick some pgo implementations like
`pgo::CellPgo` and use it like
`CellPgo::<MyAdapter>::new(pgo_data).generate_apcs(blocks)`. The client
can also implement `PgoAdapter<A: Adapter>` for its own pgo
implementation.
2025-08-22 13:59:23 +00:00

551 lines
15 KiB
Rust

use openvm_instructions::instruction::Instruction;
use openvm_sdk::config::SdkVmConfig;
use openvm_stark_sdk::p3_baby_bear::BabyBear;
use powdr_autoprecompiles::blocks::BasicBlock;
use powdr_autoprecompiles::evaluation::evaluate_apc;
use powdr_autoprecompiles::{build, VmConfig};
use powdr_number::BabyBearField;
use powdr_openvm::bus_interaction_handler::OpenVmBusInteractionHandler;
use powdr_openvm::extraction_utils::OriginalVmConfig;
use powdr_openvm::instruction_formatter::openvm_instruction_formatter;
use powdr_openvm::BabyBearOpenVmApcAdapter;
use powdr_openvm::ExtendedVmConfig;
use powdr_openvm::Instr;
use powdr_openvm::DEFAULT_DEGREE_BOUND;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
// Compiles a basic block to a string, listing the basic block, an evaluation of the APC, and APC constraints.
fn compile(basic_block: Vec<Instruction<BabyBear>>) -> String {
let sdk_vm_config = SdkVmConfig::builder()
.system(Default::default())
.rv32i(Default::default())
.rv32m(Default::default())
.io(Default::default())
.build();
let ext_vm_config = ExtendedVmConfig { sdk_vm_config };
let original_config = OriginalVmConfig::new(ext_vm_config);
let degree_bound = DEFAULT_DEGREE_BOUND;
let airs = original_config.airs(degree_bound.identities).unwrap();
let bus_map = original_config.bus_map();
let vm_config = VmConfig {
instruction_handler: &airs,
bus_interaction_handler: OpenVmBusInteractionHandler::<BabyBearField>::default(),
bus_map: bus_map.clone(),
};
let basic_block_str = basic_block
.iter()
.map(|inst| format!(" {}", openvm_instruction_formatter(inst)))
.collect::<Vec<_>>()
.join("\n");
let basic_block = BasicBlock {
statements: basic_block.into_iter().map(Instr).collect(),
start_pc: 0,
};
let apc = build::<BabyBearOpenVmApcAdapter>(basic_block.clone(), vm_config, degree_bound, None)
.unwrap();
let apc = apc.machine();
let evaluation = evaluate_apc(&basic_block.statements, &airs, apc);
format!(
"Instructions:\n{basic_block_str}\n\n{evaluation}\n\n{}",
apc.render(&bus_map)
)
}
/// Compare `actual` against the contents of the file at `path`.
/// If they differ, write `actual` to the file and fail the test.
fn assert_machine_output(program: Vec<Instruction<BabyBear>>, test_name: &str) {
let actual = compile(program.to_vec());
let base = Path::new("tests/apc_builder_outputs");
let file_path = base.join(format!("{test_name}.txt"));
let should_update_expectation = std::env::var("UPDATE_EXPECT")
.map(|v| v.as_str() == "1")
.unwrap_or(false);
let expected = file_path
.exists()
.then(|| fs::read_to_string(&file_path).unwrap());
match (expected, should_update_expectation) {
(Some(expected), _) if expected == actual => {
// Test succeeded.
}
(Some(expected), false) => {
// The expectation file exists, is different from "actual" and we are
// not allowed to update it.
// Test failed.
assert_eq!(
expected.trim(),
actual.trim(),
"The output of `{test_name}` does not match the expected output. \
To overwrite the expected output with the currently generated one, \
re-run the test with the environment variable `UPDATE_EXPECT=1` or \
delete the file `{test_name}.txt`.",
);
}
_ => {
// Expectation file does not exist or is different from "actual" and we are allowed to update it.
fs::create_dir_all(base).unwrap();
fs::write(&file_path, &actual).unwrap();
println!("Expected output for `{test_name}` was created. Re-run the test to confirm.");
}
}
}
mod single_instruction_tests {
use crate::assert_machine_output;
use powdr_openvm::symbolic_instruction_builder::*;
use test_log::test;
// ALU Chip instructions
#[test]
fn single_add_0() {
let program = [
// [x8] = [x0] + 5
add(8, 0, 5, 0),
];
assert_machine_output(program.to_vec(), "single_add_0");
}
#[test]
fn single_sub() {
let program = [
// [x8] = [x7] - [x5]
sub(8, 7, 5, 1),
];
assert_machine_output(program.to_vec(), "single_sub");
}
#[test]
fn single_and_0() {
let program = [
// [x8] = [x0] & 5
and(8, 0, 5, 0),
];
assert_machine_output(program.to_vec(), "single_and_0");
}
#[test]
fn single_xor() {
let program = [
// [x8] = [x7] ^ [x5]
xor(8, 7, 5, 1),
];
assert_machine_output(program.to_vec(), "single_xor");
}
#[test]
fn single_mul() {
let program = [
// [x8] = [x7] * [x5]
mul(8, 7, 5, 1, 0),
];
assert_machine_output(program.to_vec(), "single_mul");
}
// Load/Store Chip instructions
// `needs_write` can be 0 iff `rd=0` for load, but must be 1 if store.
#[test]
fn single_loadw() {
let program = [
// Load [x2 + 20]_2 into x8
loadw(8, 2, 20, 2, 1, 0),
];
assert_machine_output(program.to_vec(), "single_loadw");
}
#[test]
fn single_loadbu() {
let program = [
// Load [x2 + 21]_2 into x8
loadbu(8, 2, 21, 2, 1, 0),
];
assert_machine_output(program.to_vec(), "single_loadbu");
}
#[test]
fn single_loadhu() {
let program = [
// Load [x2 + 22]_2 but `needs_write=0`
loadhu(0, 2, 22, 2, 0, 0),
];
assert_machine_output(program.to_vec(), "single_loadhu");
}
#[test]
fn single_storew() {
let program = [
// Store [x8] into [x2 - 4]_2
storew(8, 2, 4, 2, 1, 1),
];
assert_machine_output(program.to_vec(), "single_storew");
}
#[test]
fn single_storeh() {
let program = [
// Store [x8] into [x2 - 6]_2
storeh(8, 2, 6, 2, 1, 1),
];
assert_machine_output(program.to_vec(), "single_storeh");
}
#[test]
fn single_storeb() {
let program = [
// Store [x8] into [x2 + 3]_2
storeb(8, 2, 3, 2, 1, 0),
];
assert_machine_output(program.to_vec(), "single_storeb");
}
// Load/Store Sign Extend Chip instructions
#[test]
fn single_loadh() {
let program = [
// Load [x2 + 6]_2 into x8
loadh(8, 2, 6, 2, 1, 0),
];
assert_machine_output(program.to_vec(), "single_loadh");
}
#[test]
fn single_loadb() {
let program = [
// Load [x2 + 3]_2 into x8 but `needs_write=0`
loadb(0, 2, 3, 2, 0, 0),
];
assert_machine_output(program.to_vec(), "single_loadb");
}
// Branch Eq Chip instructions
#[test]
fn single_beq() {
let program = [
// pc = pc + 2 if x8 == x5
beq(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_beq");
}
#[test]
fn single_bne() {
let program = [
// pc = pc + 2 if x8 != x5
bne(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_bne");
}
// Branch Lt Chip instructions
#[test]
fn single_blt() {
let program = [
// pc = pc + 2 if x8 < x5 (signed)
blt(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_blt");
}
#[test]
fn single_bltu() {
let program = [
// pc = pc + 2 if x8 < x5
bltu(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_bltu");
}
#[test]
fn single_bge() {
let program = [
// pc = pc + 2 if x8 >= x5 (signed)
bge(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_bge");
}
#[test]
fn single_bgeu() {
let program = [
// pc = pc + 2 if x8 >= x5
bgeu(8, 5, 2),
];
assert_machine_output(program.to_vec(), "single_bgeu");
}
// Shift Chip instructions
#[test]
fn single_srl() {
// Instruction 416 from the largest basic block of the Keccak guest program.
let program = [srl(68, 40, 25, 0)];
assert_machine_output(program.to_vec(), "single_srl");
}
#[test]
fn single_sll() {
// r68 = r40 << 3
let program = [sll(68, 40, 3, 0)];
assert_machine_output(program.to_vec(), "single_sll");
}
#[test]
fn single_sll_by_8() {
// r68 = r40 << 8
let program = [sll(68, 40, 8, 0)];
assert_machine_output(program.to_vec(), "single_sll_by_8");
}
#[test]
fn single_sra() {
// r68 = sign_extend(r40 >> val(R3))
let program = [sra(68, 40, 3, 1)];
assert_machine_output(program.to_vec(), "single_sra");
}
}
mod complex_tests {
use crate::assert_machine_output;
use powdr_openvm::symbolic_instruction_builder::*;
use test_log::test;
#[test]
fn guest_top_block() {
// Top block from `guest` with `--pgo cell`, with 4 instructions:
// Instruction { opcode: 512, args: [8, 8, 16777200, 1, 0, 0, 0] }
// Instruction { opcode: 531, args: [4, 8, 12, 1, 2, 1, 0] }
// Instruction { opcode: 576, args: [4, 0, 0, 1, 0, 0, 0] }
// Instruction { opcode: 565, args: [4, 4, 1780, 1, 0, 1, 0] }
let program = [
add(8, 8, 16777200, 0),
storew(4, 8, 12, 2, 1, 0),
auipc(4, 0, 0, 1, 0),
jalr(4, 4, 1780, 1, 0),
];
assert_machine_output(program.to_vec(), "guest_top_block");
}
#[test]
fn memcpy_block() {
// AND rd_ptr = 52, rs1_ptr = 44, rs2 = 3, rs2_as = 0
// SLTU rd_ptr = 52, rs1_ptr = 52, rs2 = 1, rs2_as = 0
// SLTU rd_ptr = 56, rs1_ptr = 56, rs2 = 1, rs2_as = 0
// OR rd_ptr = 52, rs1_ptr = 52, rs2 = 56, rs2_as = 1
// BNE 52 0 248 1 1
let program = [
and(52, 44, 3, 0),
sltu(52, 52, 1, 0),
sltu(56, 56, 1, 0),
or(52, 52, 56, 1),
bne(52, 0, 248),
];
assert_machine_output(program.to_vec(), "memcpy_block");
}
#[test]
fn stack_accesses() {
// The memory optimizer should realize that [x2 + 24] is accessed twice,
// with the same value of x2. Therefore, we can reduce it to just one access.
let program = [
// Load [x2 + 20] into x8
loadw(8, 2, 20, 2, 1, 0),
// Load [x2 + 24] into x9
loadw(9, 2, 24, 2, 1, 0),
// Store [x8] into [x2 + 24]
storew(8, 2, 24, 2, 1, 0),
];
assert_machine_output(program.to_vec(), "stack_accesses");
}
}
mod pseudo_instruction_tests {
use crate::assert_machine_output;
use powdr_openvm::symbolic_instruction_builder::*;
use test_log::test;
// Arithmetic pseudo instructions
#[test]
fn mv() {
// mv rd, rs1 expands to: addi rd, rs1, 0
let program = [
// [x8] = [x5]
add(8, 5, 0, 0),
];
assert_machine_output(program.to_vec(), "mv");
}
#[test]
fn not() {
// not rd, rs1 expands to: xori rd, rs1, -1
// -1 in 24-bit 2's complement is 0xFFFFFF
let minus_one: u32 = 0xFFFFFF;
let program = [
// [x8] = ~[x5]
xor(8, 5, minus_one, 0),
];
assert_machine_output(program.to_vec(), "not");
}
#[test]
fn neg() {
// neg rd, rs1 expands to: sub rd, x0, rs1
let program = [
// [x8] = -[x5]
sub(8, 0, 5, 1),
];
assert_machine_output(program.to_vec(), "neg");
}
// Set pseudo instructions
#[test]
fn seqz() {
// seqz rd, rs1 expands to: sltiu rd, rs1, 1
// which in our case is: sltu rd, rs1, 1 (with rs2_as = 0 for immediate)
// This sets rd = 1 if rs1 == 0, else rd = 0
let program = [
// [x8] = 1 if [x5] == 0, else 0
sltu(8, 5, 1, 0),
];
assert_machine_output(program.to_vec(), "seqz");
}
#[test]
fn snez() {
// snez rd, rs1 expands to: sltu rd, x0, rs1
let program = [
// [x8] = 1 if [x5] != 0, else 0
sltu(8, 0, 5, 1),
];
assert_machine_output(program.to_vec(), "snez");
}
#[test]
fn sltz() {
// sltz rd, rs1 expands to: slt rd, rs1, x0
let program = [
// [x8] = 1 if [x5] < 0 (signed), else 0
slt(8, 5, 0, 1),
];
assert_machine_output(program.to_vec(), "sltz");
}
#[test]
fn sgtz() {
// sgtz rd, rs1 expands to: slt rd, x0, rs1
let program = [
// [x8] = 1 if [x5] > 0 (signed), else 0
slt(8, 0, 5, 1),
];
assert_machine_output(program.to_vec(), "sgtz");
}
// Branch pseudo instructions
#[test]
fn beqz() {
// beqz rs1, offset expands to: beq rs1, x0, offset
let program = [
// pc = pc + 8 if [x5] == 0
beq(5, 0, 8),
];
assert_machine_output(program.to_vec(), "beqz");
}
#[test]
fn bnez() {
// bnez rs1, offset expands to: bne rs1, x0, offset
let program = [
// pc = pc + 8 if [x5] != 0
bne(5, 0, 8),
];
assert_machine_output(program.to_vec(), "bnez");
}
#[test]
fn blez() {
// blez rs1, offset expands to: bge x0, rs1, offset
let program = [
// pc = pc + 8 if [x5] <= 0 (signed)
bge(0, 5, 8),
];
assert_machine_output(program.to_vec(), "blez");
}
#[test]
fn bgez() {
// bgez rs1, offset expands to: bge rs1, x0, offset
let program = [
// pc = pc + 8 if [x5] >= 0 (signed)
bge(5, 0, 8),
];
assert_machine_output(program.to_vec(), "bgez");
}
#[test]
fn bltz() {
// bltz rs1, offset expands to: blt rs1, x0, offset
let program = [
// pc = pc + 8 if [x5] < 0 (signed)
blt(5, 0, 8),
];
assert_machine_output(program.to_vec(), "bltz");
}
#[test]
fn bgtz() {
// bgtz rs1, offset expands to: blt x0, rs1, offset
let program = [
// pc = pc + 8 if [x5] > 0 (signed)
blt(0, 5, 8),
];
assert_machine_output(program.to_vec(), "bgtz");
}
// Jump pseudo instructions
#[test]
fn j() {
// j offset expands to: jal x0, offset
let program = [
// pc = pc + 8
jal(0, 0, 8, 1, 0),
];
assert_machine_output(program.to_vec(), "j");
}
#[test]
fn jr() {
// jr offset expands to: jal x1, offset
let program = [
// pc = pc + 8, [x1] = pc + 4
jal(1, 0, 8, 1, 0),
];
assert_machine_output(program.to_vec(), "jr");
}
#[test]
fn ret() {
// ret expands to: jalr x0, x1, 0
let program = [
// pc = [x1] + 0
jalr(0, 1, 0, 1, 0),
];
assert_machine_output(program.to_vec(), "ret");
}
}