diff --git a/.gitignore b/.gitignore index 4a96767f..73038ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -/*.tar.gz \ No newline at end of file +/*.tar.gz +/riscof \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..04dbe77d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'riscv2zisk'", + "cargo": { + "args": [ + "build", + "--bin=riscv2zisk" + ], + "filter": { + "name": "riscv2zisk", + "kind": "bin" + } + }, + "args": ["riscof/riscof_work/rv64i_m/A/src/amoxor.w-01.S/dut/my.elf", "rom.json"], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'ziskemu'", + "cargo": { + "args": [ + "build", + "--bin=ziskemu" + ], + "filter": { + "name": "ziskemu", + "kind": "bin" + } + }, + "args": [ + "-e", + "riscof/riscof_work/rv64i_m", + "-l", + "-o", + "output.txt" + ], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0e893d9e..d6de1354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2394,12 +2394,13 @@ dependencies = [ ] [[package]] -name = "ziskos" -version = "0.1.0" - -[[package]] -name = "zisksim" +name = "ziskemu" version = "0.1.0" dependencies = [ + "clap", "riscv2zisk", ] + +[[package]] +name = "ziskos" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 52785837..bca89eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "cli", "common", "riscv/riscv2zisk", - "simulator", + "emulator", "state-machines/main", "state-machines/mem", "witness-computation", diff --git a/simulator/Cargo.toml b/emulator/Cargo.toml similarity index 50% rename from simulator/Cargo.toml rename to emulator/Cargo.toml index e8d6c6bf..e7904a84 100644 --- a/simulator/Cargo.toml +++ b/emulator/Cargo.toml @@ -1,15 +1,16 @@ [package] -name = "zisksim" +name = "ziskemu" version = "0.1.0" edition = "2021" [dependencies] +clap = { version = "4.5.9", features = ["derive", "env"] } riscv2zisk = {path="../riscv/riscv2zisk"} [lib] -name = "zisksim" +name = "ziskemu" path = "src/lib.rs" [[bin]] -name = "zisksim" -path = "src/bin/zisksim.rs" \ No newline at end of file +name = "ziskemu" +path = "src/bin/ziskemu.rs" diff --git a/emulator/src/bin/ziskemu.rs b/emulator/src/bin/ziskemu.rs new file mode 100644 index 00000000..fbe519ae --- /dev/null +++ b/emulator/src/bin/ziskemu.rs @@ -0,0 +1,153 @@ +use clap::Parser; +use riscv2zisk::{Riscv2zisk, ZiskRom}; +use std::{ + fs, + fs::metadata, + path::{Path, PathBuf}, + process, +}; +use ziskemu::{Emu, EmuOptions}; + +fn main() { + // Create a emulator options instance based on arguments or default values + let options: EmuOptions = EmuOptions::parse(); + + // Log the emulator options if requested + if options.verbose { + println!("ziskemu converts an ELF RISCV file into a ZISK rom or loads a ZISK rom file, emulates it with the provided input, and copies the output to console or a file"); + println!("{}", options); + } + + // INPUT: + // build input data either from the provided input path, or leave it empty (default input) + let input: Vec = if options.input.is_some() { + // Read input data from the provided input path + let path = std::path::PathBuf::from(options.input.clone().unwrap()); + std::fs::read(path).expect("Could not read input file") + } else { + // If no input data is provided, input will remain empty + // This normally means that input data is self-contained in the program + Vec::new() + }; + + if options.rom.is_some() && options.elf.is_some() { + eprintln!( + "Error parsing arguments: ROM file and ELF file are incompatible; use only one of them" + ); + process::exit(1); + } else if options.rom.is_some() { + process_rom_file(options.rom.clone().unwrap(), &input, &options); + } else if options.elf.is_some() { + let elf_file = options.elf.clone().unwrap(); + let md = metadata(elf_file.clone()).unwrap(); + if md.is_file() { + process_elf_file(elf_file, &input, &options); + } else if md.is_dir() { + process_directory(elf_file, &input, &options); + } + } else { + eprintln!("Error parsing arguments: ROM file or ELF file must be provided"); + process::exit(1); + } + + // Return successfully + process::exit(0); +} + +fn process_directory(directory: String, input: &[u8], options: &EmuOptions) { + let files = list_files(&directory); + for file in files { + if file.contains("dut") && file.ends_with(".elf") { + process_elf_file(file, input, options); + } + } +} + +fn process_elf_file(elf_file: String, input: &[u8], options: &EmuOptions) { + // Convert the ELF file to ZisK ROM + let rom: ZiskRom = { + // Create an instance of the RISCV -> ZisK program converter + let rv2zk = Riscv2zisk::new(elf_file, String::new()); + + // Convert program to rom + let result = rv2zk.run(); + if result.is_err() { + println!("Application error: {}", result.err().unwrap()); + process::exit(1); + } + + // Get the result + result.unwrap() + }; + + process_rom(&rom, input, options); +} + +fn process_rom_file(_rom_file: String, input: &[u8], options: &EmuOptions) { + // TODO: load from file + let rom: ZiskRom = ZiskRom::new(); + process_rom(&rom, input, options); +} + +fn process_rom(rom: &ZiskRom, input: &[u8], options: &EmuOptions) { + // Create a emulator instance with this rom and input + let mut emu = Emu::new(rom, input.to_owned(), options.clone()); + + // Run the emulation + emu.run(); + if !emu.terminated() { + println!("Emulation did not complete"); + process::exit(1); + } + + // OUTPUT: + // if requested, save output to file, or log it to console + if options.output.is_some() { + // Get the emulation output as a u8 vector + let output = emu.get_output_8(); + + // Save the output to file + let output_file = as Clone>::clone(&options.output).unwrap(); + fs::write(output_file, output).expect("Unable to write output file"); + } + // Log output to console + else { + // Get the emulation output as a u32 vector + let output = emu.get_output_32(); + + // Log the output to console + for o in &output { + println!("{:08x}", o); + } + } +} + +fn list_files(directory: &String) -> Vec { + let path = Path::new(directory); + let paths = list_files_paths(path); + let mut vec: Vec = Vec::new(); + for p in paths { + vec.push(p.display().to_string()); + } + vec +} + +fn list_files_paths(path: &Path) -> Vec { + let mut vec = Vec::new(); + _list_files(&mut vec, path); + vec +} + +fn _list_files(vec: &mut Vec, path: &Path) { + if metadata(path).unwrap().is_dir() { + let paths = fs::read_dir(path).unwrap(); + for path_result in paths { + let full_path = path_result.unwrap().path(); + if metadata(&full_path).unwrap().is_dir() { + _list_files(vec, &full_path); + } else { + vec.push(full_path); + } + } + } +} diff --git a/simulator/src/sim.rs b/emulator/src/emu.rs similarity index 76% rename from simulator/src/sim.rs rename to emulator/src/emu.rs index bf891abb..3ccbb8ba 100644 --- a/simulator/src/sim.rs +++ b/emulator/src/emu.rs @@ -1,4 +1,4 @@ -use crate::{SimContext, SimOptions}; +use crate::{EmuContext, EmuOptions}; use riscv2zisk::{ ZiskOperations, ZiskRom, OUTPUT_ADDR, SRC_C, SRC_IMM, SRC_IND, SRC_MEM, SRC_SP, SRC_STEP, STORE_IND, STORE_MEM, STORE_NONE, SYS_ADDR, @@ -12,51 +12,55 @@ const REG_NAMES: [&str; 32] = [ "t5", "t6", ]; -/// ZisK simulator structure, containing the ZisK rom, the list of ZisK operations, and the +/// ZisK emulator structure, containing the ZisK rom, the list of ZisK operations, and the /// execution context -pub struct Sim { +pub struct Emu<'a> { /// ZisK rom, containing the program to execute, which is constant for this program except for /// the input data - pub rom: ZiskRom, + pub rom: &'a ZiskRom, /// ZisK operations (c, flag) = f(a, b), one per supported opcode operations: ZiskOperations, /// Context, where the state of the execution is stored and modified at every execution step - ctx: SimContext, + ctx: EmuContext, + + /// Emulator options + options: EmuOptions, } -/// ZisK simulator structure implementation -impl Sim { - //// ZisK simulator structure constructor - pub fn new(rom: ZiskRom, input: Vec) -> Sim { +/// ZisK emulator structure implementation +impl Emu<'_> { + //// ZisK emulator structure constructor + pub fn new(rom: &ZiskRom, input: Vec, options: EmuOptions) -> Emu { // Initialize an empty instance - let mut sim = Sim { ctx: SimContext::new(input), operations: ZiskOperations::new(), rom }; + let mut emu = + Emu { ctx: EmuContext::new(input), operations: ZiskOperations::new(), rom, options }; // Create a new read section for every RO data entry of the rom - for i in 0..sim.rom.ro_data.len() { - sim.ctx.mem.add_read_section(sim.rom.ro_data[i].from, &sim.rom.ro_data[i].data); + for i in 0..emu.rom.ro_data.len() { + emu.ctx.mem.add_read_section(emu.rom.ro_data[i].from, &emu.rom.ro_data[i].data); } // Get registers - //sim.get_regs(); // TODO: ask Jordi + //emu.get_regs(); // TODO: ask Jordi - sim + emu } - /// Performs one single step of the simulation + /// Performs one single step of the emulation pub fn step(&mut self) { - // Get a mutable reference to the simulation context + // Get a mutable reference to the emulation context let ctx = &mut self.ctx; // Get the ZisK instruction corresponding to the current program counter if !self.rom.insts.contains_key(&ctx.pc) { - panic!("Sim::step() cound not find a rom instruction for pc={}={:x}", ctx.pc, ctx.pc); + panic!("Emu::step() cound not find a rom instruction for pc={}={:x}", ctx.pc, ctx.pc); } let inst = &self.rom.insts[&ctx.pc]; - //println!("Sim::step() executing step={} pc={:x} inst={}", ctx.step, ctx.pc, - // inst.i.to_string()); println!("Sim::step() step={} pc={}", ctx.step, ctx.pc); + //println!("Emu::step() executing step={} pc={:x} inst={}", ctx.step, ctx.pc, + // inst.i.to_string()); println!("Emu::step() step={} pc={}", ctx.step, ctx.pc); // If this is the last instruction, stop executing if inst.i.end == 1 { @@ -77,7 +81,7 @@ impl Sim { SRC_IMM => ctx.a = inst.i.a_offset_imm0 | (inst.i.a_use_sp_imm1 << 32), SRC_STEP => ctx.a = ctx.step, SRC_SP => ctx.a = ctx.sp, - _ => panic!("Sim::step() Invalid a_src={} pc={}", inst.i.a_src, ctx.pc), + _ => panic!("Emu::step() Invalid a_src={} pc={}", inst.i.a_src, ctx.pc), } // Build the value of the b register based on the source specified by the current @@ -99,12 +103,12 @@ impl Sim { } ctx.b = ctx.mem.read(addr, inst.i.ind_width); } - _ => panic!("Sim::step() Invalid b_src={} pc={}", inst.i.b_src, ctx.pc), + _ => panic!("Emu::step() Invalid b_src={} pc={}", inst.i.b_src, ctx.pc), } // Check the instruction opcode range if inst.i.op > 0xFF { - panic!("Sim::step() invalid opcode={}", inst.i.op); + panic!("Emu::step() invalid opcode={}", inst.i.op); } // Get the ZisK operation for this opcode @@ -128,7 +132,7 @@ impl Sim { addr += ctx.sp as i64; } ctx.mem.write(addr as u64, val as u64, 8); - //println!{"Sim::step() step={} pc={} writing to memory addr={} val={}", ctx.step, + //println!{"Emu::step() step={} pc={} writing to memory addr={} val={}", ctx.step, // ctx.pc, addr, val as u64}; } STORE_IND => { @@ -143,10 +147,10 @@ impl Sim { } addr += ctx.a as i64; ctx.mem.write(addr as u64, val as u64, inst.i.ind_width); - //println!{"Sim::step() step={} pc={} writing to memory addr={} val={}", ctx.step, + //println!{"Emu::step() step={} pc={} writing to memory addr={} val={}", ctx.step, // ctx.pc, addr, val as u64}; } - _ => panic!("Sim::step() Invalid store={} pc={}", inst.i.store, ctx.pc), + _ => panic!("Emu::step() Invalid store={} pc={}", inst.i.store, ctx.pc), } // Set SP, if specified by the current instruction @@ -165,6 +169,13 @@ impl Sim { ctx.pc = (ctx.pc as i64 + inst.i.jmp_offset2) as u64; } + // Log the step, if requested + if self.options.log_step { + println!( + "step={} pc={} op={}={} a={} b={} c={} flag={}", + ctx.step, ctx.pc, inst.i.op, inst.i.op_str, ctx.a, ctx.b, ctx.c, ctx.flag + ); + } // Increment step counter ctx.step += 1; } @@ -195,23 +206,42 @@ impl Sim { output } + /// Get the output as a vector of u8 + pub fn get_output_8(&self) -> Vec { + let ctx = &self.ctx; + let n = ctx.mem.read(OUTPUT_ADDR, 4); + let mut addr = OUTPUT_ADDR + 4; + let mut output: Vec = Vec::new(); + for _i in 0..n { + output.push(ctx.mem.read(addr, 1) as u8); + output.push(ctx.mem.read(addr + 1, 1) as u8); + output.push(ctx.mem.read(addr + 2, 1) as u8); + output.push(ctx.mem.read(addr + 3, 1) as u8); + addr += 4; + } + output + } + /// Run the whole program - pub fn run(&mut self, opts: SimOptions) { + pub fn run(&mut self) { // While not done while !self.ctx.end { - //println!("Sim::run() step={} ctx.pc={}", self.ctx.step, self.ctx.pc); // 2147483828 + //println!("Emu::run() step={} ctx.pc={}", self.ctx.step, self.ctx.pc); // 2147483828 // Check trace PC if self.ctx.tracerv_on && (self.ctx.pc % 4 == 0) { self.ctx.trace_pc = self.ctx.pc; } - // Log simulation step, if requested - if (opts.print_step != 0) && ((self.ctx.step % opts.print_step) == 0) { + // Log emulation step, if requested + if self.options.print_step.is_some() && + (self.options.print_step.unwrap() != 0) && + ((self.ctx.step % self.options.print_step.unwrap()) == 0) + { println!("step={}", self.ctx.step); } // Stop the execution if we exceeded the specified running conditions - if (self.ctx.pc as i64 == opts.to) || (self.ctx.step >= opts.max_steps) { + if self.ctx.step >= self.options.max_steps { break; } @@ -247,7 +277,7 @@ impl Sim { self.ctx.tracerv_step += 1; } - //println!("Sim::run() done ctx.pc={}", self.ctx.pc); // 2147483828 + //println!("Emu::run() done ctx.pc={}", self.ctx.pc); // 2147483828 } } @@ -286,7 +316,7 @@ impl Sim { self.ctx.tracerv.clone() } - /// Returns if the simulation ended + /// Returns if the emulation ended pub fn terminated(&self) -> bool { self.ctx.end } diff --git a/simulator/src/sim_context.rs b/emulator/src/emu_context.rs similarity index 78% rename from simulator/src/sim_context.rs rename to emulator/src/emu_context.rs index 7a7e8619..76226401 100644 --- a/simulator/src/sim_context.rs +++ b/emulator/src/emu_context.rs @@ -1,8 +1,8 @@ use crate::Mem; use riscv2zisk::{INPUT_ADDR, MAX_INPUT_SIZE, RAM_ADDR, RAM_SIZE, ROM_ENTRY}; -/// ZisK simulator context data container, storing the state of the simulation -pub struct SimContext { +/// ZisK emulator context data container, storing the state of the emuulation +pub struct EmuContext { pub mem: Mem, pub a: u64, pub b: u64, @@ -19,11 +19,11 @@ pub struct SimContext { pub trace_pc: u64, } -/// RisK simulator context implementation -impl SimContext { - /// RisK simulator context constructor - pub fn new(input: Vec) -> SimContext { - let mut ctx = SimContext { +/// RisK emulator context implementation +impl EmuContext { + /// RisK emulator context constructor + pub fn new(input: Vec) -> EmuContext { + let mut ctx = EmuContext { mem: Mem::new(), a: 0, b: 0, @@ -42,7 +42,7 @@ impl SimContext { // Check the input data size is inside the proper range if input.len() > (MAX_INPUT_SIZE - 8) as usize { - panic!("SimContext::new() input size too big size={}", input.len()); + panic!("EmuContext::new() input size too big size={}", input.len()); } // Create a new empty vector diff --git a/emulator/src/emu_options.rs b/emulator/src/emu_options.rs new file mode 100644 index 00000000..a7b43bec --- /dev/null +++ b/emulator/src/emu_options.rs @@ -0,0 +1,46 @@ +use clap::Parser; +use std::fmt; + +/// ZisK emulator options structure +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct EmuOptions { + /// Sets the Zisk ROM data file path + #[clap(short, long, value_name = "ROM_FILE")] + pub rom: Option, + /// Sets the ELF data file path, to be converted to ZisK ROM data + #[clap(short, long, value_name = "ELF_FILE")] + pub elf: Option, + /// Sets the input data file path + #[clap(short, long, value_name = "INPUT_FILE")] + pub input: Option, + /// Sets the output data file path + #[clap(short, long, value_name = "OUTPUT_FILE")] + pub output: Option, + /// Sets the maximum number of steps to execute + #[clap(short = 'n', long, value_name = "MAX_STEPS", default_value = "100000000")] + pub max_steps: u64, + /// Sets the print step period in number of steps + #[clap(short, long, value_name = "PRINT_STEP", default_value = "0")] + pub print_step: Option, + /// Sets the trace output file + #[clap(short, long, value_name = "TRACE_FILE")] + pub trace: Option, + /// Sets the verbose mode + #[clap(short, long, value_name = "VERBOSE", default_value = "false")] + pub verbose: bool, + /// Sets the log step mode + #[clap(short, long, value_name = "LOG_STEP", default_value = "false")] + pub log_step: bool, +} + +impl fmt::Display for EmuOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ROM: {:?}\nELF: {:?}\nINPUT: {:?}\nMAX_STEPS: {}\nPRINT_STEP: {:?}\nTRACE: {:?}\nOUTPUT: {:?}\nVERBOSE: {}", + self.rom, self.elf, self.input, self.max_steps, self.print_step, self.trace, self.output, self.verbose + ) + } +} diff --git a/emulator/src/lib.rs b/emulator/src/lib.rs new file mode 100644 index 00000000..fd11bb16 --- /dev/null +++ b/emulator/src/lib.rs @@ -0,0 +1,11 @@ +mod mem; +pub use mem::*; +mod mem_section; +pub use mem_section::*; + +mod emu; +pub use emu::*; +mod emu_context; +pub use emu_context::*; +mod emu_options; +pub use emu_options::*; diff --git a/simulator/src/mem.rs b/emulator/src/mem.rs similarity index 100% rename from simulator/src/mem.rs rename to emulator/src/mem.rs diff --git a/simulator/src/mem_section.rs b/emulator/src/mem_section.rs similarity index 100% rename from simulator/src/mem_section.rs rename to emulator/src/mem_section.rs diff --git a/simulator/src/bin/zisksim.rs b/simulator/src/bin/zisksim.rs deleted file mode 100644 index 9c1b789a..00000000 --- a/simulator/src/bin/zisksim.rs +++ /dev/null @@ -1,123 +0,0 @@ -use riscv2zisk::Riscv2zisk; -use std::{ - env, fs, - fs::metadata, - path::{Path, PathBuf}, - process, -}; -use zisksim::{Sim, SimOptions}; - -fn _list_files(vec: &mut Vec, path: &Path) { - if metadata(path).unwrap().is_dir() { - let paths = fs::read_dir(path).unwrap(); - for path_result in paths { - let full_path = path_result.unwrap().path(); - if metadata(&full_path).unwrap().is_dir() { - _list_files(vec, &full_path); - } else { - vec.push(full_path); - } - } - } -} - -fn list_files(path: &Path) -> Vec { - let mut vec = Vec::new(); - _list_files(&mut vec, path); - vec -} - -fn main() { - //println!("zisk_tester converts an ELF RISCV file into a ZISK ASM program, simulates it, and - // copies the output to console"); - - // Get program arguments - let args: Vec = env::args().collect(); - - // Check program arguments length - if args.len() != 2 { - eprintln!("Error parsing arguments: number of arguments should be 1. Usage: zisk_tester "); - process::exit(1); - } - - let argument = &args[1]; - let mut multiple_files = false; - let md = metadata(argument).unwrap(); - let mut elf_files: Vec = Vec::new(); - if md.is_file() { - elf_files.push(argument.clone()); - } else if md.is_dir() { - multiple_files = true; - let path = Path::new(argument); - let files = list_files(path); - for file in files { - let file_name = file.display().to_string(); - if file_name.contains("dut") && file_name.ends_with(".elf") { - elf_files.push(file_name.to_string().clone()); - println!("found DUT ELF file: {}", file_name); - } - } - } - - let elf_files_len = elf_files.len(); - - if multiple_files { - println!("Going to process {} ELF files", elf_files_len); - } - - //const FIRST_ELF_FILE: u64 = 0; - - for (elf_file_counter, elf_file) in elf_files.into_iter().enumerate() { - // Get the input parameters: ELF (RISCV) file name (input data) - //let elf_file = args[1].clone(); - let zisk_file = String::new(); - - if multiple_files { - println!("ELF file {}/{}: {}", elf_file_counter, elf_files_len, elf_file); - } - /*if (FIRST_ELF_FILE > 0) && (elf_file_counter < FIRST_ELF_FILE) { - println!("Skipping file {}", elf_file); - continue; - }*/ - - // Create an instance of the program converter - let rv2zk = Riscv2zisk::new(elf_file, zisk_file); - - // Convert program to rom - let result = rv2zk.run(); - if result.is_err() { - println!("Application error: {}", result.err().unwrap()); - process::exit(1); - } - let rom = result.unwrap(); - - // Create an empty input - let input: Vec = Vec::new(); - - // Create a simulator instance with this rom and input - let mut sim = Sim::new(rom, input); - - // Create a simulator options instance with the default values - let sim_options = SimOptions::new(); - - // Run the simulations - sim.run(sim_options); - if !sim.terminated() { - println!("Simulation did not complete"); - process::exit(1); - } - - if !multiple_files { - // Get the simulation outpus as a u32 vector - let output = sim.get_output_32(); - - // Log the output in console - for o in &output { - println!("{:08x}", o); - } - } - } - - // Return successfully - process::exit(0); -} diff --git a/simulator/src/lib.rs b/simulator/src/lib.rs deleted file mode 100644 index 7a4b8027..00000000 --- a/simulator/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod mem; -pub use mem::*; -mod mem_section; -pub use mem_section::*; - -mod sim; -pub use sim::*; -mod sim_context; -pub use sim_context::*; -mod sim_options; -pub use sim_options::*; diff --git a/simulator/src/sim_options.rs b/simulator/src/sim_options.rs deleted file mode 100644 index f728af08..00000000 --- a/simulator/src/sim_options.rs +++ /dev/null @@ -1,21 +0,0 @@ -/// ZisK simulator options structure -pub struct SimOptions { - pub to: i64, - pub max_steps: u64, - pub print_step: u64, -} - -/// Default constructor for SimOptions structure -impl Default for SimOptions { - fn default() -> Self { - Self::new() - } -} - -/// ZisK simulator options structure implementation -impl SimOptions { - /// Zisk simulator options constructor - pub fn new() -> SimOptions { - SimOptions { to: -1, max_steps: 1_000_000, print_step: 0 } - } -}