├── rot13_simple.in ├── rot13.in ├── brainfuck-rs ├── rust-toolchain.toml ├── Cargo.toml ├── powdr-guest │ ├── rust-toolchain.toml │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── interpreter │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── host │ ├── Cargo.toml │ └── src │ └── main.rs ├── rot13_for_compiled.in ├── hello_world.bf ├── rot13.bf ├── program_formatter.py ├── brainfuck_template.asm ├── bf_to_powdr.py ├── README.md └── brainfuck_vm.asm /rot13_simple.in: -------------------------------------------------------------------------------- 1 | 65,90 2 | -------------------------------------------------------------------------------- /rot13.in: -------------------------------------------------------------------------------- 1 | 65,90,66,91,67,92,65,90,66,91,67,92 2 | -------------------------------------------------------------------------------- /brainfuck-rs/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.77" 3 | -------------------------------------------------------------------------------- /rot13_for_compiled.in: -------------------------------------------------------------------------------- 1 | 65,90,66,91,67,92,65,90,66,91,67,92,18446744069414584320 2 | -------------------------------------------------------------------------------- /hello_world.bf: -------------------------------------------------------------------------------- 1 | ++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++. 2 | -------------------------------------------------------------------------------- /brainfuck-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["host", "interpreter"] 5 | default-members = ["host"] 6 | -------------------------------------------------------------------------------- /brainfuck-rs/powdr-guest/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-02-01" 3 | targets = ["riscv32imac-unknown-none-elf"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /brainfuck-rs/interpreter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brainfuck-interpreter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [features] 9 | std = [] 10 | -------------------------------------------------------------------------------- /brainfuck-rs/powdr-guest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brainfuck-powdr-guest" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | brainfuck-interpreter = { path = "../interpreter", default-features = false } 8 | powdr-riscv-runtime = { git = "https://github.com/powdr-labs/powdr", branch = "main" } 9 | serde = { version = "1.0", default-features = false, features = ["alloc", "derive", "rc"] } 10 | serde_cbor = { version = "0.11.2", default-features = false, features = ["alloc"] } 11 | 12 | [workspace] 13 | -------------------------------------------------------------------------------- /brainfuck-rs/powdr-guest/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![no_std] 3 | 4 | extern crate alloc; 5 | use alloc::string::String; 6 | use alloc::vec::Vec; 7 | use alloc::collections::VecDeque; 8 | 9 | use powdr_riscv_runtime::{io::read, print}; 10 | 11 | #[no_mangle] 12 | pub fn main() { 13 | let program: Vec = read(1); 14 | let inputs: VecDeque = read(2); 15 | 16 | let (_, output) = brainfuck_interpreter::run(program, inputs); 17 | let output = String::from_utf8(output).unwrap(); 18 | print!("{output}"); 19 | } 20 | -------------------------------------------------------------------------------- /brainfuck-rs/host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brainfuck-powdr-host" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | powdr = { git = "https://github.com/powdr-labs/powdr", branch = "riscv-dynamic-vadcop", features = [ 8 | "halo2", 9 | "estark-polygon", 10 | "plonky3", 11 | ] } 12 | 13 | brainfuck-interpreter = { path = "../interpreter", default-features = false, features = [ 14 | "std", 15 | ] } 16 | 17 | serde = { version = "1.0", default-features = false, features = [ 18 | "alloc", 19 | "derive", 20 | "rc", 21 | ] } 22 | serde_cbor = { version = "0.11.2", default-features = false, features = [ 23 | "alloc", 24 | ] } 25 | 26 | env_logger = "0.10.2" 27 | clap = { version = "^4.4", features = ["derive"] } 28 | log = "0.4.17" 29 | -------------------------------------------------------------------------------- /rot13.bf: -------------------------------------------------------------------------------- 1 | -,+[ 2 | -[ 3 | >>++++[>++++++++<-] 4 | 5 | <+<-[ 6 | >+>+>-[>>>] 7 | <[[>+<-]>>+>] 8 | <<<<<- 9 | ] 10 | ]>>>[-]+ 11 | >--[-[<->+++[-]]]<[ 12 | ++++++++++++<[ 13 | 14 | >-[>+>>] 15 | >[+[<+>-]>+>>] 16 | <<<<<- 17 | ] 18 | >>[<+>-] 19 | >[ 20 | -[ 21 | -<<[-]>> 22 | ]<<[<<->>-]>> 23 | ]<<[<<+>>-] 24 | ] 25 | <[-] 26 | <.[-] 27 | <-,+ 28 | ] 29 | -------------------------------------------------------------------------------- /program_formatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | 6 | def main(): 7 | if len(sys.argv) != 2 and len(sys.argv) != 3: 8 | print("Usage: program_formatter.py [input.in]") 9 | sys.exit(1) 10 | filename = sys.argv[1] 11 | with open(filename, "r") as f: 12 | program = f.read() 13 | 14 | formatted_program = [ 15 | ord(c) for c in program if c in [">", "<", "+", "-", ".", ",", "[", "]"] 16 | ] 17 | formatted_program = [len(formatted_program) + 1] + formatted_program + [0] 18 | joined_program = ", ".join(map(str, formatted_program)).replace(" ", "") 19 | 20 | input_data = [] 21 | if len(sys.argv) == 3: 22 | input_filename = sys.argv[2] 23 | with open(input_filename, "r") as f: 24 | input_data = f.read() 25 | 26 | input_data = input_data.strip("\n").split(",") 27 | input_data = [int(c) for c in input_data] 28 | 29 | input_data = [len(input_data)] + input_data 30 | joined_input = ", ".join(map(str, input_data)).replace(" ", "") 31 | joined_program += "," + joined_input 32 | 33 | print(joined_program) 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /brainfuck_template.asm: -------------------------------------------------------------------------------- 1 | use std::machines::range::Byte2; 2 | use std::machines::memory::Memory; 3 | 4 | machine Brainfuck { 5 | Byte2 byte2; 6 | Memory mem(byte2); 7 | 8 | // the program pc 9 | reg pc[@pc]; 10 | // assignment register used by instruction parameters 11 | reg X[<=]; 12 | 13 | // data pointer 14 | reg dp; 15 | // program's input counter 16 | reg in_ptr; 17 | // helper data container 18 | reg data; 19 | 20 | // iszero check for X 21 | let XIsZero = std::utils::is_zero(X); 22 | 23 | // instructions needed for Brainfuck operations 24 | 25 | instr branch_if_zero X, l: label 26 | { 27 | pc' = XIsZero * l + (1 - XIsZero) * (pc + 1) 28 | } 29 | 30 | instr jump l: label{ pc' = l } 31 | 32 | instr fail { 1 = 0 } 33 | 34 | instr inc_dp { dp' = dp + 1 } 35 | instr dec_dp { dp' = dp - 1 } 36 | 37 | // helper column 38 | col witness C; 39 | 40 | instr inc_cell 41 | link ~> C = mem.mload(dp, STEP) 42 | link ~> mem.mstore(dp, STEP, C + 1); 43 | 44 | instr dec_cell 45 | link ~> C = mem.mload(dp, STEP) 46 | link ~> mem.mstore(dp, STEP, C - 1); 47 | 48 | // memory instructions 49 | col fixed STEP(i) { i }; 50 | instr mload -> X 51 | link ~> X = mem.mload(dp, STEP); 52 | 53 | instr mstore X 54 | link ~> mem.mstore(dp, STEP, X); 55 | 56 | // compiled Brainfuck program 57 | 58 | {{ program }} 59 | } 60 | -------------------------------------------------------------------------------- /brainfuck-rs/interpreter/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | extern crate alloc; 4 | use alloc::collections::VecDeque; 5 | use alloc::vec; 6 | use alloc::vec::Vec; 7 | 8 | pub fn run(program: Vec, mut inputs: VecDeque) -> (u64, Vec) { 9 | let mut pc: usize = 0; 10 | let mut data_ptr: usize = 0; 11 | let mut loop_stack: Vec = Vec::new(); 12 | let mut memory = vec![0i64; 30000]; 13 | 14 | let mut output = vec![]; 15 | 16 | let mut instr_count = 0; 17 | loop { 18 | let op = program[pc]; 19 | 20 | if op == 0 { 21 | break; 22 | } 23 | 24 | instr_count += 1; 25 | 26 | if op == 62 { 27 | data_ptr += 1; 28 | } else if op == 60 { 29 | data_ptr -= 1; 30 | } else if op == 43 { 31 | memory[data_ptr] += 1; 32 | } else if op == 45 { 33 | memory[data_ptr] -= 1; 34 | } else if op == 44 { 35 | memory[data_ptr] = inputs.pop_front().unwrap_or(-1); 36 | } else if op == 46 { 37 | output.push(memory[data_ptr] as u8); 38 | } else if op == 91 { 39 | if memory[data_ptr] == 0 { 40 | let mut depth = 1; 41 | while depth != 0 { 42 | pc += 1; 43 | if program[pc] == 91 { 44 | depth += 1; 45 | } else if program[pc] == 93 { 46 | depth -= 1; 47 | } 48 | } 49 | } else { 50 | loop_stack.push(pc); 51 | } 52 | } else if op == 93 { 53 | pc = loop_stack.pop().unwrap() - 1; 54 | } 55 | 56 | pc += 1; 57 | } 58 | 59 | (instr_count, output) 60 | } 61 | -------------------------------------------------------------------------------- /bf_to_powdr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | 6 | 7 | def compile(program): 8 | loop_stack = [] 9 | loop_counter = 0 10 | powdr_asm = [] 11 | for instr in program: 12 | if instr == ">": 13 | powdr_asm.append("inc_dp;") 14 | elif instr == "<": 15 | powdr_asm.append("dec_dp;") 16 | elif instr == "+": 17 | powdr_asm.append("inc_cell;") 18 | elif instr == "-": 19 | powdr_asm.append("dec_cell;") 20 | elif instr == ",": 21 | powdr_asm.append( 22 | "data <=X= ${ std::prelude::Query::Input(std::convert::int(std::prover::eval(in_ptr))) };" 23 | ) 24 | powdr_asm.append("mstore data;") 25 | powdr_asm.append("in_ptr <=X= in_ptr + 1;") 26 | elif instr == ".": 27 | powdr_asm.append("data <== mload();") 28 | powdr_asm.append( 29 | "data <=X= ${ std::prelude::Query::Output(1, std::convert::int(std::prover::eval(data))) };" 30 | ) 31 | elif instr == "[": 32 | label_true = f"loop_true_{loop_counter}" 33 | label_false = f"loop_false_{loop_counter}" 34 | loop_counter += 1 35 | powdr_asm.append(f"{label_true}:") 36 | powdr_asm.append("data <== mload();") 37 | powdr_asm.append(f"branch_if_zero data, {label_false};") 38 | loop_stack.append((label_true, label_false)) 39 | elif instr == "]": 40 | (label_true, label_false) = loop_stack.pop() 41 | powdr_asm.append(f"jump {label_true};") 42 | powdr_asm.append(f"{label_false}:") 43 | 44 | powdr_asm.append("return;") 45 | return powdr_asm 46 | 47 | 48 | def main(): 49 | if len(sys.argv) != 2: 50 | print("Usage: bf_to_powdr.py ") 51 | sys.exit(1) 52 | 53 | filename = sys.argv[1] 54 | with open(filename, "r") as f: 55 | program = f.read() 56 | 57 | powdr_asm = compile(program) 58 | powdr_asm = "\n".join(powdr_asm) 59 | 60 | main_function = f"function main {{\n{powdr_asm}\n}}" 61 | 62 | asm_file_path = "brainfuck_template.asm" 63 | 64 | with open(asm_file_path, "r") as file: 65 | asm_content = file.read() 66 | 67 | compiled_asm = asm_content.replace("{{ program }}", main_function) 68 | 69 | program_name = os.path.splitext(filename)[0] 70 | compiled_file_path = f"{program_name}.asm" 71 | with open(compiled_file_path, "w") as file: 72 | file.write(compiled_asm) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # powdr-brainfuck 2 | 3 | This repository contains different Brainfuck implementations as 4 | [powdr](https://docs.powdr.org/) VMs. 5 | 6 | 1. [Brainfuck ISA and Compiler](#1-brainfuck-isa-and-compiler) 7 | 2. [Brainfuck interpreter in powdr-IR](#2-brainfuck-interpreter-in-powdr-ir) 8 | 3. [Brainfuck interpreter in powdr-Rust](#3-brainfuck-interpreter-in-powdr-rust) 9 | 10 | ## 1. Brainfuck ISA and Compiler 11 | 12 | `brainfuck_template.asm` contains a compact Brainfuck ISA definition with just 13 | enough registers and instructions. Instead of interpreting an input program, 14 | we compile Brainfuck programs to powdr-IR programs using this ISA, effectively 15 | compiling input programs to custom circuits. 16 | 17 | The compiler is in `bf_to_powdr.py`. 18 | 19 | We first compile the program to powdr-IR, which only has to be done once: 20 | 21 | ```console 22 | ./bf_to_powdr.py hello_world.bf 23 | ``` 24 | 25 | We can now run `powdr pil` and get a proof: 26 | 27 | ```console 28 | powdr pil hello_world.asm --prove-with plonky3-composite 29 | ``` 30 | 31 | ## 2. Brainfuck interpreter in powdr-IR 32 | 33 | `brainfuck_vm.asm` is a hand-written assembly interpreter that takes the 34 | Brainfuck program (with a zero at the end) and the program inputs as a single 35 | list of numbers encoded as ` ` 36 | and generates a ZK proof of execution, effectively being a zkVM. This is 37 | different from approach (2) which implements an ISA that Brainfuck programs can 38 | be transpiled to. 39 | 40 | ```console 41 | powdr pil brainfuck_vm.asm -o output -f -i "3,62,44,0,17" 42 | ``` 43 | 44 | This command compiles the interpreter and generates a witness for the program 45 | `>,` with input `[17]`. Note that the program is given as a list of the ASCII 46 | codes of the program characters. 47 | 48 | You can add `--prove-with plonky3` to the command above to generate a STARK 49 | proof of the execution, or `--prove-with halo2 --field bn254` to generate a 50 | SNARK. 51 | 52 | The script `program_formatter.py` helps to format Brainfuck programs as `powdr` inputs: 53 | 54 | ```console 55 | powdr pil brainfuck_vm.asm -i $(./program_formatter.py hello_world.bf) -o output -f 56 | ``` 57 | 58 | `Hello World!` should be printed during witness generation. 59 | 60 | TODO: 61 | 62 | - Verify public program commitment 63 | 64 | ## 3. Brainfuck interpreter in powdr-Rust 65 | 66 | The directory `brainfuck-rs` has 3 crates: 67 | 68 | - `interpreter`: A Brainfuck interpreter that runs a given program on given 69 | inputs and returns the outputs that would be printed on `stdout`. The interpreter 70 | is used by both `host` and `powdr-guest`. 71 | - `host`: The main binary, able to run the interpreter natively as well as make 72 | ZK proofs using powdr. 73 | - `powdr-guest`: The code to be proven, interfaces with `powdr`'s input API and 74 | uses the same interpreter code as `host`. 75 | 76 | To run just the native interpreter, run: 77 | 78 | ```console 79 | cargo run -r -- --program ../hello_world.bf -e 80 | ``` 81 | 82 | To run powdr's compilation and RISCV executor only, run: 83 | 84 | ```console 85 | RUST_LOG=info cargo run -r -- --program ../hello_world.bf -f 86 | ``` 87 | 88 | The `info` option also prints the trace length. 89 | 90 | To run powdr's compilation and full witness generation, run: 91 | 92 | ```console 93 | RUST_LOG=info cargo run -r -- --program ../hello_world.bf -w 94 | ``` 95 | 96 | To run powdr's compilation, full witness and proof generation, run: 97 | 98 | ```console 99 | cargo run -r -- --program ../hello_world.bf --proof 100 | ``` 101 | -------------------------------------------------------------------------------- /brainfuck-rs/host/src/main.rs: -------------------------------------------------------------------------------- 1 | use powdr::backend::BackendType; 2 | use powdr::riscv::continuations::bootloader::default_input; 3 | use powdr::riscv::{compile_rust, Runtime}; 4 | use powdr::GoldilocksField; 5 | use powdr::Pipeline; 6 | 7 | use std::collections::VecDeque; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | use std::time::Instant; 11 | 12 | use clap::Parser; 13 | 14 | #[derive(Parser)] 15 | struct Options { 16 | #[clap(short, long, required = true)] 17 | program: PathBuf, 18 | 19 | #[clap(short, long)] 20 | inputs: Option, 21 | 22 | #[clap(short, long)] 23 | execute: bool, 24 | 25 | #[clap(short, long, default_value = ".")] 26 | output: PathBuf, 27 | 28 | #[clap(short, long)] 29 | fast_tracer: bool, 30 | 31 | #[clap(short, long)] 32 | witgen: bool, 33 | 34 | #[clap(long)] 35 | proof: bool, 36 | } 37 | 38 | fn read_file_and_convert(path: PathBuf) -> Vec { 39 | let content = fs::read_to_string(path).unwrap(); 40 | let valid_chars = "><+-.,[]"; 41 | content 42 | .chars() 43 | .filter(|c| valid_chars.contains(*c)) 44 | .map(|b| b as u32) 45 | .collect() 46 | } 47 | 48 | fn read_inputs(path: PathBuf) -> VecDeque { 49 | let content = fs::read_to_string(path).unwrap(); 50 | content 51 | .split(',') 52 | .map(|x| x.trim()) 53 | .filter(|x| !x.is_empty()) 54 | .map(|x| x.parse::().unwrap()) 55 | .collect() 56 | } 57 | 58 | type F = GoldilocksField; 59 | 60 | fn main() { 61 | let mut options = Options::parse(); 62 | if options.proof { 63 | options.witgen = true; 64 | } 65 | 66 | env_logger::init(); 67 | 68 | let mut program = read_file_and_convert(options.program); 69 | program.push(0); 70 | 71 | let inputs = match options.inputs { 72 | Some(inputs) => read_inputs(inputs), 73 | None => vec![].into(), 74 | }; 75 | 76 | if options.execute { 77 | log::info!("Running native brainfuck interpreter..."); 78 | let (instr_count, output) = brainfuck_interpreter::run(program.clone(), inputs.clone()); 79 | let output = String::from_utf8(output).unwrap(); 80 | log::info!("Execution took {instr_count} BF cycles."); 81 | println!("{output}"); 82 | } 83 | 84 | if !(options.fast_tracer || options.witgen) { 85 | return; 86 | } 87 | 88 | log::info!("Compiling powdr-brainfuck..."); 89 | let (asm_file_path, asm_contents) = compile_rust::( 90 | "./powdr-guest", 91 | &options.output, 92 | true, 93 | &Runtime::base(), 94 | true, 95 | false, 96 | None, 97 | ) 98 | .ok_or_else(|| vec!["could not compile rust".to_string()]) 99 | .unwrap(); 100 | 101 | log::debug!("powdr-asm code:\n{asm_contents}"); 102 | 103 | // Create a pipeline from the asm program 104 | let mut pipeline = Pipeline::::default() 105 | .from_asm_string(asm_contents.clone(), Some(asm_file_path.clone())) 106 | .with_output(options.output.clone(), true) 107 | .add_data(2, &inputs) 108 | .add_data(1, &program); 109 | 110 | if options.fast_tracer { 111 | log::info!("Running powdr-riscv executor in fast mode..."); 112 | let start = Instant::now(); 113 | 114 | let program = pipeline.compute_analyzed_asm().unwrap().clone(); 115 | let initial_memory = powdr::riscv::continuations::load_initial_memory(&program); 116 | let (trace, _mem, _reg_mem) = powdr::riscv_executor::execute_ast::( 117 | &program, 118 | initial_memory, 119 | pipeline.data_callback().unwrap(), 120 | &default_input(&[]), 121 | usize::MAX, 122 | powdr::riscv_executor::ExecMode::Fast, 123 | None, 124 | ); 125 | 126 | let duration = start.elapsed(); 127 | log::info!("Fast executor took: {:?}", duration); 128 | log::info!("Trace length: {}", trace.len); 129 | } 130 | 131 | if options.witgen { 132 | log::info!("Running witness generation..."); 133 | let start = Instant::now(); 134 | 135 | pipeline.compute_witness().unwrap(); 136 | 137 | let duration = start.elapsed(); 138 | log::info!("Witness generation took: {:?}", duration); 139 | } 140 | 141 | if options.proof { 142 | let mut pipeline = pipeline.with_backend(BackendType::Plonky3Composite, None); 143 | log::info!("Computing proof..."); 144 | let start = Instant::now(); 145 | 146 | pipeline.compute_proof().unwrap(); 147 | 148 | let duration = start.elapsed(); 149 | log::info!("Proof generation took: {:?}", duration); 150 | } 151 | 152 | log::info!("Done."); 153 | } 154 | -------------------------------------------------------------------------------- /brainfuck_vm.asm: -------------------------------------------------------------------------------- 1 | /// The machine below implements a basic Brainfuck interpreter. 2 | /// It abuses many registers for a clearer understanding of the code, 3 | /// but it can be optimized quite a bit, by for example: 4 | /// - Re-using registers or moving them to memory 5 | /// - Re-writing some routines to use fewer rows 6 | 7 | /// Soundness considerations: 8 | /// - The opcodes are currently unconstrained, meaning the prover can provide whatever they want. 9 | /// To fix this, a public commitment to the program needs to be passed and verified in the machine below. 10 | 11 | /// Program and input/output encoding: 12 | /// The prover input is a list of numbers encoded as follows 13 | /// 14 | /// where needs to end with a 0 15 | /// Example: 16 | /// [2, 44, 0, 1, 97] 17 | /// This program has length 2, where the program is [44, 0] (read, finish) 18 | /// and the input list is [97]. 19 | /// The `.` (print) instruction treats its input as the ASCII code of a character, 20 | /// and prints that character. 21 | 22 | use std::machines::range::Byte2; 23 | use std::machines::memory::Memory; 24 | 25 | machine Brainfuck { 26 | Byte2 byte2; 27 | Memory mem(byte2); 28 | 29 | reg pc[@pc]; 30 | reg X[<=]; 31 | reg Y[<=]; 32 | reg Z[<=]; 33 | 34 | // The pc of the given Brainfuck program 35 | reg b_pc; 36 | // The current operator 37 | reg op; 38 | // Data pointer 39 | reg dp; 40 | // program's input counter 41 | reg in_ptr; 42 | // the stack of loop addresses 43 | reg loop_sp; 44 | 45 | // General purpose registers 46 | reg ret_addr; 47 | reg A; 48 | reg CNT; 49 | reg tmp1; 50 | 51 | instr jump l: label -> Y { pc' = l, Y = pc + 1} 52 | instr jump_dyn X -> Y { pc' = X, Y = pc + 1} 53 | instr branch_if_zero X, l: label { pc' = XIsZero * l + (1 - XIsZero) * (pc + 1) } 54 | instr fail { 1 = 0 } 55 | 56 | // ============== memory instructions ============== 57 | col fixed STEP(i) { i }; 58 | instr mload X -> Y 59 | link ~> Y = mem.mload(X, STEP); 60 | 61 | instr mstore X, Y 62 | link ~> mem.mstore(X, STEP, Y); 63 | 64 | // ============== iszero check for X ======================= 65 | let XIsZero = std::utils::is_zero(X); 66 | 67 | // === Brainfuck interpreter ========== 68 | function main { 69 | // calls the main entry point of the program 70 | ret_addr <== jump(__runtime_start); 71 | 72 | // exits entire program 73 | exit: 74 | return; 75 | 76 | // ==== helper routine to read the program and inputs from prover into memory 77 | read_program_and_input: 78 | // read the length of the program 79 | A <=X= ${ std::prelude::Query::Input(0) }; 80 | CNT <=X= 0; 81 | read_program_loop: 82 | branch_if_zero A - CNT, end_read_program; 83 | mstore CNT + 0 /*PROGRAM_START*/, ${ std::prelude::Query::Input(std::convert::int(std::prover::eval(CNT)) + 1) }; 84 | CNT <=X= CNT + 1; 85 | tmp1 <== jump(read_program_loop); 86 | end_read_program: 87 | read_input: 88 | CNT <=X= 0; 89 | // read input length 90 | in_ptr <=X= ${ std::prelude::Query::Input(std::convert::int(std::prover::eval(A)) + 1) }; 91 | read_input_loop: 92 | branch_if_zero in_ptr - CNT, end_read_input; 93 | mstore CNT + 10000 /*INPUT_START*/, ${ std::prelude::Query::Input(std::convert::int(std::prover::eval(CNT) + std::prover::eval(A)) + 2) }; 94 | CNT <=X= CNT + 1; 95 | tmp1 <== jump(read_input_loop); 96 | end_read_input: 97 | mstore CNT + 10000 /*INPUT_START*/, -1; 98 | A <== jump_dyn(ret_addr); 99 | // ==== end of helper routine 100 | 101 | // ==== helper routine that decodes and runs an opcode 102 | run_op: 103 | // '>' is 62 104 | branch_if_zero op - 62, routine_move_right; 105 | // '<' is 60 106 | branch_if_zero op - 60, routine_move_left; 107 | // '+' is 43 108 | branch_if_zero op - 43, routine_inc; 109 | // '-' is 45 110 | branch_if_zero op - 45, routine_dec; 111 | // ',' is 44 112 | branch_if_zero op - 44, routine_read; 113 | // '.' is 46 114 | branch_if_zero op - 46, routine_write; 115 | // '[' is 91 116 | branch_if_zero op - 91, routine_loop_start; 117 | // ']' is 93 118 | branch_if_zero op - 93, routine_loop_end; 119 | // unknown op 120 | fail; 121 | 122 | // ==== helper routine for `[` 123 | routine_loop_start: 124 | A <== mload(dp); 125 | // If the current cell is zero, find the matching ']' and set b_op 126 | // to after that. 127 | branch_if_zero A, loop_exit; 128 | loop_enter: 129 | // We're entering the loop: save the loop start pc. 130 | loop_sp <=X= loop_sp + 1; 131 | mstore loop_sp, b_pc; 132 | A <== jump(end_run_op); 133 | loop_exit: 134 | // Scope counter, needed to exit nested loops. 135 | CNT <=X= 1; 136 | A <=X= b_pc; 137 | search_for_loop_end: 138 | A <=X= A + 1; 139 | op <== mload(A); 140 | branch_if_zero op - 91, found_loop_enter; 141 | branch_if_zero op - 93, found_loop_exit; 142 | tmp1 <== jump(search_for_loop_end); 143 | found_loop_enter: 144 | // If we see a nested opening loop we increase the counter. 145 | CNT <=X= CNT + 1; 146 | tmp1 <== jump(search_for_loop_end); 147 | found_loop_exit: 148 | // If we see a closing loop and the counter is zero, we found the 149 | // matching loop. 150 | CNT <=X= CNT - 1; 151 | branch_if_zero CNT, exit_loop; 152 | tmp1 <== jump(search_for_loop_end); 153 | exit_loop: 154 | // We set the pc to the closing loop because the main interpreter 155 | // loop always increments it by 1 by default. 156 | b_pc <=X= A; 157 | A <== jump(end_run_op); 158 | // ==== end of `[` helper routine 159 | 160 | // ==== helper routine for `]` 161 | routine_loop_end: 162 | // When we see a `]`, we need to jump back to the start of the 163 | // loop. 164 | // We set `b_pc` to before the start of the loop because the 165 | // main interpreter loop always increments it by 1 by default. 166 | b_pc <== mload(loop_sp); 167 | b_pc <=X= b_pc - 1; 168 | loop_sp <=X= loop_sp - 1; 169 | A <== jump(end_run_op); 170 | // ==== end of `]` helper routine 171 | 172 | // ==== helper routine for `>` 173 | routine_move_right: 174 | dp <=X= dp + 1; 175 | A <== jump(end_run_op); 176 | // ==== end of `>` helper routine 177 | 178 | // ==== helper routine for `<` 179 | routine_move_left: 180 | dp <=X= dp - 1; 181 | A <== jump(end_run_op); 182 | // ==== end of `<` helper routine 183 | 184 | // ==== helper routine for `+` 185 | routine_inc: 186 | A <== mload(dp); 187 | mstore dp, A + 1; 188 | A <== jump(end_run_op); 189 | // ==== end of `+` helper routine 190 | 191 | // ==== helper routine for `-` 192 | routine_dec: 193 | A <== mload(dp); 194 | mstore dp, A - 1; 195 | A <== jump(end_run_op); 196 | // ==== end of `-` helper routine 197 | 198 | // ==== helper routine for `,` 199 | routine_read: 200 | A <== mload(in_ptr); 201 | in_ptr <=X= in_ptr + 1; 202 | mstore dp, A; 203 | A <== jump(end_run_op); 204 | // ==== end of `,` helper routine 205 | 206 | // ==== helper routine for `.` 207 | routine_write: 208 | A <== mload(dp); 209 | A <=X= ${ std::prelude::Query::Output(1, std::convert::int(std::prover::eval(A))) }; 210 | A <== jump(end_run_op); 211 | // ==== end of `.` helper routine 212 | 213 | end_run_op: 214 | A <== jump_dyn(ret_addr); 215 | // ==== end of opcode helper routine 216 | 217 | // ==== program entry point 218 | __runtime_start: 219 | ret_addr <== jump(read_program_and_input); 220 | 221 | // Arbitrary sizes for the memory regions 222 | // The main part is memory (dp) 223 | b_pc <=X= 0 /*PROGRAM_START*/; 224 | in_ptr <=X= 10000 /*INPUT_START*/; 225 | loop_sp <=X= 20000 /*LOOP_STACK_START*/; 226 | dp <=X= 30000 /*MEM_START*/; 227 | 228 | // ==== main interpreter loop 229 | interpreter_loop: 230 | // TODO we should also hash the program and expose as public 231 | op <== mload(b_pc); 232 | 233 | branch_if_zero op, exit; 234 | 235 | ret_addr <== jump(run_op); 236 | b_pc <=X= b_pc + 1; 237 | 238 | A <== jump(interpreter_loop); 239 | // ==== end of main interpreter loop 240 | } 241 | } 242 | --------------------------------------------------------------------------------