1use clap::Args;
2use std::{fs, thread};
3use std::io::{self, Write};
4use std::sync::{mpsc, Arc};
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7use crate::{BrainfuckReader, BrainfuckReaderError};
8use crate::cli_util::print_reader_error;
9use crate::reader::StepControl;
10
11#[derive(Args, Debug)]
12#[command(disable_help_flag = true)]
13pub struct ReadArgs {
14 #[arg(short = 'd', long = "debug")]
16 pub debug: bool,
17
18 #[arg(short = 'f', long = "file")]
20 pub file: Option<String>,
21
22 #[arg(value_name = "code", trailing_var_arg = true)]
24 pub code: Vec<String>,
25
26 #[arg(long = "timeout", value_name = "MS")]
28 pub timeout_ms: Option<u64>,
29
30 #[arg(long = "max-steps", value_name = "N")]
32 pub max_steps: Option<u64>,
33
34 #[arg(short = 'h', long = "help", action = clap::ArgAction::SetTrue)]
36 pub help: bool,
37}
38
39pub fn run(program: &str, args: ReadArgs) -> i32 {
40 if args.help {
41 usage_and_exit(program, 0);
42 }
43
44 let ReadArgs {
45 debug,
46 file,
47 code,
48 timeout_ms,
49 max_steps,
50 ..
51 } = args;
52
53 if file.is_none() && code.is_empty() {
54 usage_and_exit(program, 2);
55 }
56
57 if file.is_some() && !code.is_empty() {
58 eprintln!("{program}: cannot use positional code together with --file");
59 usage_and_exit(program, 2);
60 }
61
62 let code_str = if let Some(path) = file {
63 match fs::read_to_string(&path) {
64 Ok(s) => s,
65 Err(e) => {
66 eprintln!("{program}: failed to read code file as UTF-8: {e}");
67 let _ = io::stderr().flush();
68 return 1;
69 }
70 }
71 } else {
72 code.join("")
73 };
74
75 let timeout_ms = timeout_ms
77 .or_else(|| std::env::var("BF_TIMEOUT_MS").ok().and_then(|s| s.parse::<u64>().ok()))
78 .unwrap_or(2_000);
79 let max_steps = max_steps
80 .or_else(|| std::env::var("BF_MAX_STEPS").ok().and_then(|s| s.parse::<u64>().ok()));
81
82 let cancel = Arc::new(AtomicBool::new(false));
84 let (tx, rx) = mpsc::channel::<Result<(), BrainfuckReaderError>>();
85 let program_owned = code_str.clone();
86 let cancel_clone = cancel.clone();
87
88 thread::spawn(move || {
89 let max_steps_opt: Option<usize> = max_steps.map(|m| m as usize);
90 let mut bf = BrainfuckReader::new(program_owned);
91 let ctrl = StepControl::new(max_steps_opt, cancel_clone);
92 let res = if debug {
93 bf.run_debug_with_control(ctrl)
94 } else {
95 bf.run_with_control(ctrl)
96 };
97 let _ = tx.send(res);
98 });
99
100 let timeout = Duration::from_millis(timeout_ms);
101 let exit_code = match rx.recv_timeout(timeout) {
102 Ok(Ok(())) => 0,
103 Ok(Err(BrainfuckReaderError::StepLimitExceeded { limit })) => {
104 eprintln!("Execution aborted: step limit exceeded ({limit})");
105 let _ = io::stderr().flush();
106 1
107 }
108 Ok(Err(BrainfuckReaderError::Canceled)) => {
109 eprintln!("Execution aborted: wall-clock timeout exceeded ({timeout_ms} ms)");
110 let _ = io::stderr().flush();
111 1
112 }
113 Ok(Err(other)) => {
114 print_reader_error(Some(program), &code_str, &other);
115 let _ = io::stderr().flush();
116 1
117 }
118 Err(mpsc::RecvTimeoutError::Timeout) => {
119 cancel.store(true, Ordering::Relaxed);
120 eprintln!("Execution aborted: wall-clock timeout exceeded ({timeout_ms} ms)");
121 let _ = io::stderr().flush();
122 1
123 }
124 Err(mpsc::RecvTimeoutError::Disconnected) => 1,
125 };
126
127 println!();
128 let _ = io::stdout().flush();
129 exit_code
130}
131
132fn usage_and_exit(program: &str, code: i32) -> ! {
133 eprintln!(
134 r#"Usage:
135 {0} read [--debug|-d] "<code>"
136 {0} read [--debug|-d] --file <PATH>
137
138Options:
139 --file, -f <PATH> Read Brainfuck code from PATH instead of positional "<code>"
140 --debug, -d Print a step-by-step table of operations instead of executing
141 --help, -h Show this help
142
143Notes:
144- Input (`,`) reads a single byte from stdin; on EOF the current cell is set to 0.
145- Any characters outside of Brainfuck's ><+-.,[] will result in an error.
146
147Examples:
148- Load Brainfuck code from a file:
149 {0} read --file ./program.bf
150- Read bytes from a file as stdin (`,` will consume file input):
151 {0} read ",[.,]" < input.txt
152"#,
153 program
154 );
155 let _ = io::stderr().flush();
156 std::process::exit(code);
157}
158